#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Memory Context CLI (WU-1234, WU-1292)
 *
 * Generate deterministic, formatted context injection blocks for delegation prompts.
 * Outputs structured markdown with sections for project profile, summaries,
 * WU context, and discoveries.
 *
 * Usage:
 *   pnpm mem:context --wu WU-XXXX [options]
 *
 * Options:
 *   --max-size <bytes>            Maximum context size in bytes (default: 4096)
 *   --delegation-context-max-size <bytes>  Alias for --max-size (for config parity)
 *   --spawn-context-max-size <bytes>  Deprecated: hard-fails with migration guidance
 *   --lane <lane>                 Filter project memories by lane (WU-1292)
 *   --max-recent-summaries <n>    Limit recent summaries included (WU-1292)
 *   --max-project-nodes <n>       Limit project nodes included (WU-1292)
 *   --format <json|human>         Output format (default: human)
 *   --quiet                       Suppress header/footer output
 *
 * Includes audit logging to .lumenflow/telemetry/tools.ndjson.
 *
 * @see {@link packages/@lumenflow/memory/src/mem-context-core.ts} - Core logic
 * @see {@link packages/@lumenflow/cli/__tests__/mem-context.test.ts} - Tests
 */
import fs from 'node:fs/promises';
import path from 'node:path';
import { generateContext } from '@lumenflow/memory/context';
import { loadSignals } from '@lumenflow/memory/signal';
import { DelegationRegistryStore, DELEGATION_PATTERNS } from '@lumenflow/core';
import { collectActiveSessionRecords, collectSessionCrossLinkIndex, resolveSessionForDelegation, } from './session-cross-link.js';
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/arg-parser';
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/wu-constants';
import { createError, ErrorCodes } from '@lumenflow/core/error-handler';
import { runCLI } from './cli-entry-point.js';
/**
 * Log prefix for mem:context output
 */
const LOG_PREFIX = '[mem:context]';
/**
 * Tool name for audit logging
 */
const TOOL_NAME = 'mem:context';
const MEM_CONTEXT_ERRORS = {
    DEPRECATED_SPAWN_CONTEXT_FLAG: 'Deprecated flag --spawn-context-max-size is not supported. Use --delegation-context-max-size instead.',
};
/**
 * Valid output formats
 */
const VALID_FORMATS = ['json', 'human'];
/**
 * CLI argument options specific to mem:context
 */
const CLI_OPTIONS = {
    maxSize: {
        name: 'maxSize',
        flags: '-m, --max-size <bytes>',
        description: 'Maximum context size in bytes (default: 4096)',
    },
    delegationContextMaxSize: {
        name: 'delegationContextMaxSize',
        flags: '--delegation-context-max-size <bytes>',
        description: 'Alias for --max-size (for config parity with delegation_context_max_size)',
    },
    deprecatedSpawnContextMaxSize: {
        name: 'spawnContextMaxSize',
        flags: '--spawn-context-max-size <bytes>',
        description: '(Deprecated) Use --delegation-context-max-size instead',
    },
    lane: {
        name: 'lane',
        flags: '-l, --lane <lane>',
        description: 'Filter project memories by lane (e.g., "Framework: CLI")',
    },
    maxRecentSummaries: {
        name: 'maxRecentSummaries',
        flags: '--max-recent-summaries <count>',
        description: 'Maximum number of recent summaries to include (default: 5)',
    },
    maxProjectNodes: {
        name: 'maxProjectNodes',
        flags: '--max-project-nodes <count>',
        description: 'Maximum number of project nodes to include (default: 10)',
    },
    format: {
        name: 'format',
        flags: '-f, --format <format>',
        description: 'Output format: json or human (default: human)',
    },
    baseDir: {
        name: 'baseDir',
        flags: '-d, --base-dir <path>',
        description: 'Base directory (defaults to current directory)',
    },
    quiet: {
        name: 'quiet',
        flags: '-q, --quiet',
        description: 'Suppress header/footer output, only show context block',
    },
    // WU-2755 (ADR-014 extension 3): coordination roster rendering.
    includeRoster: {
        name: 'includeRoster',
        flags: '--include-roster',
        description: 'Legacy alias; roster auto-composes by default when active sessions exist',
    },
    roster: {
        name: 'roster',
        flags: '--no-roster',
        description: 'Disable automatic coordination roster composition',
    },
    initiative: {
        name: 'initiative',
        flags: '--initiative <id>',
        description: 'Initiative ID to scope the roster to',
    },
};
function formatRoleDescriptorCompact(role) {
    if (!role)
        return '-';
    const alias = role.role_alias ? ` (${role.role_alias})` : '';
    return `${role.lifecycle_role}/${role.specialty_profile}${alias}`;
}
function formatOwnershipScopeCompact(scope) {
    if (!scope)
        return '-';
    const parts = [];
    if (scope.initiative_id)
        parts.push(scope.initiative_id);
    if (scope.wu_ids && scope.wu_ids.length > 0)
        parts.push(`wu:${scope.wu_ids.join(',')}`);
    if (scope.lane)
        parts.push(`lane:${scope.lane}`);
    if (scope.responsibility_kinds.length > 0) {
        parts.push(`kind:${scope.responsibility_kinds.join(',')}`);
    }
    return parts.length === 0 ? '-' : parts.join(' | ');
}
async function buildCoordinationRoster(options) {
    const store = new DelegationRegistryStore(path.join(options.baseDir, '.lumenflow', 'state'));
    let activeDelegations = [];
    try {
        await store.load();
    }
    catch {
        activeDelegations = [];
    }
    if (activeDelegations.length === 0) {
        activeDelegations = store.getPendingDelegations();
    }
    // When caller supplies an initiative scope, filter via ownership_scope.
    // Delegation IDs are opaque and do not encode initiative membership.
    const scoped = options.initiativeId
        ? activeDelegations.filter((delegation) => delegation.ownership_scope?.initiative_id === options.initiativeId)
        : activeDelegations;
    const sessionIndex = collectSessionCrossLinkIndex(options.baseDir);
    const activeSessions = collectActiveSessionRecords(options.baseDir);
    // Preload signals once so we can group per-delegation.
    const allSignals = await loadSignals(options.baseDir, { compatibilityMode: 'skip-legacy' });
    const entries = [...scoped]
        .sort((left, right) => left.targetWuId.localeCompare(right.targetWuId) || left.id.localeCompare(right.id))
        .map((d) => {
        const session = resolveSessionForDelegation(d, sessionIndex);
        const delegationSignals = allSignals
            .filter((s) => s.delegation_id === d.id)
            .slice(-3)
            .map((s) => ({
            id: s.id,
            message: s.message,
            role: s.role,
            target_role: s.target_role,
            created_at: s.created_at,
        }));
        return {
            delegation_id: d.id,
            target_wu_id: d.targetWuId,
            lane: d.lane,
            requested_role: formatRoleDescriptorCompact(d.requested_role),
            actual_role: formatRoleDescriptorCompact(d.actual_role),
            ...(session?.display_name ? { display_name: session.display_name } : {}),
            lifecycle_role: session?.lifecycle_role,
            specialty_profile: session?.specialty_profile,
            current_wu: session?.wu_id,
            last_heartbeat: session?.last_heartbeat,
            ownership_scope: formatOwnershipScopeCompact(d.ownership_scope),
            status: d.status,
            ...(delegationSignals.length > 0 ? { recent_signals: delegationSignals } : {}),
        };
    });
    for (const session of activeSessions) {
        if (!session.session_id) {
            continue;
        }
        const alreadyRepresented = entries.some((entry) => entry.current_wu === session.wu_id || entry.delegation_id === session.delegation_id);
        if (alreadyRepresented) {
            continue;
        }
        entries.push({
            delegation_id: session.delegation_id ?? session.session_id,
            target_wu_id: session.wu_id,
            lane: session.lane,
            requested_role: '-',
            actual_role: '-',
            ...(session.display_name ? { display_name: session.display_name } : {}),
            lifecycle_role: session.lifecycle_role,
            specialty_profile: session.specialty_profile,
            current_wu: session.wu_id,
            last_heartbeat: session.last_heartbeat,
            ownership_scope: '-',
            status: 'pending',
        });
    }
    return entries.length > 0 ? { entries } : undefined;
}
function isValidInitiativeId(id) {
    return /^INIT-\d+$/.test(id);
}
function isValidDelegationId(id) {
    return DELEGATION_PATTERNS.DELEGATION_ID.test(id);
}
// Reference the regex exports so tsc keeps them in the import graph even when
// future refactors shuffle usages. Cheap and defensive.
void isValidDelegationId;
/**
 * Write audit log entry for tool execution
 *
 * @param baseDir - Base directory
 * @param entry - Audit log entry
 */
async function writeAuditLog(baseDir, entry) {
    try {
        const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
        const logDir = path.dirname(logPath);
        await fs.mkdir(logDir, { recursive: true });
        const line = `${JSON.stringify(entry)}\n`;
        await fs.appendFile(logPath, line, 'utf-8');
    }
    catch {
        // Audit logging is non-fatal - silently ignore errors
    }
}
/**
 * Validate and parse a positive integer argument
 *
 * @param value - Raw argument value
 * @param optionName - Name of the option for error messages
 * @returns Parsed integer value
 * @throws If value is invalid
 */
function parsePositiveInt(value, optionName) {
    if (!value) {
        return undefined;
    }
    const parsed = parseInt(value, 10);
    if (isNaN(parsed) || parsed <= 0) {
        throw createError(ErrorCodes.INVALID_ARGUMENT, `Invalid ${optionName} value: "${value}". Must be a positive integer.`);
    }
    return parsed;
}
/**
 * WU-1292: Validate lane argument
 *
 * @param lane - Lane argument
 * @returns Validated lane
 * @throws If lane is empty string
 */
function validateLane(lane) {
    if (lane === undefined) {
        return undefined;
    }
    if (lane === '') {
        throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid --lane value: lane cannot be empty.');
    }
    return lane;
}
/**
 * Validate format argument
 *
 * @param format - Format argument
 * @returns Validated format
 * @throws If format is invalid
 */
function validateFormat(format) {
    if (!format) {
        return 'human';
    }
    if (!VALID_FORMATS.includes(format)) {
        throw createError(ErrorCodes.INVALID_ARGUMENT, `Invalid --format value: "${format}". Valid formats: ${VALID_FORMATS.join(', ')}`);
    }
    return format;
}
/**
 * Print output in human-readable format
 *
 * @param result - Generate context result
 * @param wuId - WU ID
 * @param quiet - Whether to suppress headers
 */
function printHumanFormat(result, wuId, quiet) {
    if (!quiet) {
        console.log(`${LOG_PREFIX} Context for ${wuId}:`);
        console.log('');
    }
    if (result.contextBlock === '') {
        if (!quiet) {
            console.log('  (no memories found - empty context block)');
            console.log('');
        }
        return;
    }
    console.log(result.contextBlock);
    if (!quiet) {
        console.log('');
        console.log(`${LOG_PREFIX} ${result.stats.totalNodes} node(s) included${result.stats.truncated ? ' (truncated)' : ''}`);
    }
}
/**
 * Print output in JSON format
 *
 * @param result - Generate context result
 * @param wuId - WU ID
 */
function printJsonFormat(result, wuId) {
    const output = {
        wuId,
        contextBlock: result.contextBlock,
        stats: result.stats,
    };
    console.log(JSON.stringify(output, null, 2));
}
/**
 * Main CLI entry point
 */
export async function main() {
    const args = createWUParser({
        name: 'mem-context',
        description: 'Generate context injection block for delegation prompts',
        options: [
            WU_OPTIONS.wu,
            CLI_OPTIONS.maxSize,
            CLI_OPTIONS.delegationContextMaxSize,
            CLI_OPTIONS.deprecatedSpawnContextMaxSize,
            CLI_OPTIONS.lane,
            CLI_OPTIONS.maxRecentSummaries,
            CLI_OPTIONS.maxProjectNodes,
            CLI_OPTIONS.format,
            CLI_OPTIONS.baseDir,
            CLI_OPTIONS.quiet,
            // WU-2755: coordination roster flags.
            CLI_OPTIONS.includeRoster,
            CLI_OPTIONS.roster,
            CLI_OPTIONS.initiative,
        ],
        required: ['wu'],
    });
    const baseDir = args.baseDir || process.cwd();
    const startedAt = new Date().toISOString();
    const startTime = Date.now();
    let maxSize;
    let lane;
    let maxRecentSummaries;
    let maxProjectNodes;
    let format;
    // Validate arguments
    try {
        if (args.spawnContextMaxSize) {
            throw createError(ErrorCodes.DEPRECATED_API, MEM_CONTEXT_ERRORS.DEPRECATED_SPAWN_CONTEXT_FLAG);
        }
        // --delegation-context-max-size is an alias for --max-size (for config parity)
        // If both are provided, --delegation-context-max-size takes precedence.
        const maxSizeArg = args.delegationContextMaxSize || args.maxSize;
        maxSize = parsePositiveInt(maxSizeArg, '--max-size');
        lane = validateLane(args.lane);
        maxRecentSummaries = parsePositiveInt(args.maxRecentSummaries, '--max-recent-summaries');
        maxProjectNodes = parsePositiveInt(args.maxProjectNodes, '--max-project-nodes');
        format = validateFormat(args.format);
    }
    catch (err) {
        const error = err;
        console.error(`${LOG_PREFIX} Error: ${error.message}`);
        process.exit(EXIT_CODES.ERROR);
    }
    let result = {
        success: false,
        contextBlock: '',
        stats: { totalNodes: 0, byType: {}, truncated: false, size: 0 },
    };
    let error = null;
    try {
        // WU-2755: compose coordination roster if requested.
        let coordinationRoster;
        if (args.roster !== false) {
            if (args.initiative && !isValidInitiativeId(args.initiative)) {
                throw createError(ErrorCodes.INVALID_ARGUMENT, `Invalid --initiative value: "${args.initiative}". Expected INIT-XXX.`);
            }
            coordinationRoster = await buildCoordinationRoster({
                baseDir,
                wuId: args.wu,
                initiativeId: args.initiative,
            });
        }
        const raw = await generateContext(baseDir, {
            wuId: args.wu,
            maxSize,
            lane,
            maxRecentSummaries,
            maxProjectNodes,
            coordinationRoster,
        });
        result = raw;
    }
    catch (err) {
        const e = err;
        error = e.message;
    }
    const durationMs = Date.now() - startTime;
    // Write audit log entry
    await writeAuditLog(baseDir, {
        tool: TOOL_NAME,
        status: error ? 'failed' : 'success',
        startedAt,
        completedAt: new Date().toISOString(),
        durationMs,
        input: {
            baseDir,
            wuId: args.wu,
            maxSize,
            lane,
            maxRecentSummaries,
            maxProjectNodes,
            includeRoster: args.includeRoster,
            roster: args.roster,
            initiative: args.initiative,
            format,
            quiet: args.quiet,
        },
        output: result.success
            ? {
                contextSize: result.contextBlock.length,
                stats: result.stats,
            }
            : null,
        error: error ? { message: error } : null,
    });
    if (error) {
        console.error(`${LOG_PREFIX} Error: ${error}`);
        process.exit(EXIT_CODES.ERROR);
    }
    // Print output based on format
    if (format === 'json') {
        printJsonFormat(result, args.wu);
    }
    else {
        printHumanFormat(result, args.wu, !!args.quiet);
    }
}
// WU-1537: Use import.meta.main + runCLI for consistent EPIPE and error handling
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=mem-context.js.map