#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Agent Session Start CLI
 *
 * Starts an agent session for tracking WU execution.
 *
 * Usage:
 *   pnpm agent:session --wu WU-1234 --tier 2
 */
import { existsSync, readFileSync } from 'node:fs';
import { isIP } from 'node:net';
import { join } from 'node:path';
import { Command } from 'commander';
import { startSession, getCurrentSession } from '@lumenflow/agent';
import { CONFIG_FILES, ENV_VARS, EXIT_CODES } from '@lumenflow/core/wu-constants';
import { createError, ErrorCodes, ProcessExitError } from '@lumenflow/core/error-handler';
import { parseYAML } from '@lumenflow/core/wu-yaml';
import chalk from 'chalk';
import { buildAgentSessionEnrolledEvent, buildAgentSessionStalledEvent, } from './agent-runtime-enrollment-events.js';
import { resolveConductorEventSink } from './kernel-event-sync/lifecycle-emitters.js';
import { runCLI } from './cli-entry-point.js';
const REACHABILITY_CLASS = {
    REVERSE_TUNNEL: 'reverse-tunnel',
    PUBLIC: 'public',
    LAN: 'lan',
    UNREACHABLE: 'unreachable',
};
const REACHABILITY_CLASS_VALUES = [
    REACHABILITY_CLASS.REVERSE_TUNNEL,
    REACHABILITY_CLASS.PUBLIC,
    REACHABILITY_CLASS.LAN,
    REACHABILITY_CLASS.UNREACHABLE,
];
const ENVIRONMENT_KEY = {
    AGENT_HTTP_URL: ENV_VARS.AGENT_HTTP_URL,
    REACHABLE_HTTP_URL: ENV_VARS.REACHABLE_HTTP_URL,
    REACHABILITY_CLASS: ENV_VARS.AGENT_REACHABILITY_CLASS,
    HEALTH_ENDPOINT: ENV_VARS.AGENT_HEALTH_ENDPOINT,
    TLS_FINGERPRINT: ENV_VARS.AGENT_TLS_FINGERPRINT,
};
const HTTP_HEADER = {
    AUTHORIZATION: 'authorization',
    CONTENT_TYPE: 'content-type',
};
const CONTENT_TYPE_JSON = 'application/json';
const WORKSPACE_AGENT_HOST_ROUTE_PREFIX = '/api/v1/workspaces';
const WORKSPACE_AGENT_HOST_ROUTE_SUFFIX = 'agent-hosts';
const PRIVATE_HOSTNAME_SUFFIXES = ['.internal', '.lan', '.local'];
const PRIVATE_IPV4_PREFIXES = ['10.', '127.', '169.254.', '192.168.'];
const PRIVATE_IPV4_SECOND_OCTET_172_MIN = 16;
const PRIVATE_IPV4_SECOND_OCTET_172_MAX = 31;
const PRIVATE_IPV6_PREFIXES = ['::1', 'fc', 'fd', 'fe80'];
const REVERSE_TUNNEL_SUFFIXES = [
    '.gradio.live',
    '.loca.lt',
    '.ngrok-free.app',
    '.ngrok.app',
    '.ngrok.io',
    '.trycloudflare.com',
    '.tunnelmole.net',
];
function asNonEmptyString(value) {
    return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
}
function normalizeEndpoint(endpoint) {
    return endpoint.endsWith('/') ? endpoint.slice(0, endpoint.length - 1) : endpoint;
}
function normalizeHttpUrl(value) {
    const normalizedValue = asNonEmptyString(value);
    if (!normalizedValue) {
        return null;
    }
    try {
        return new URL(normalizedValue).toString().replace(/\/$/, '');
    }
    catch {
        return null;
    }
}
function normalizeReachabilityClass(value) {
    if (typeof value !== 'string') {
        return null;
    }
    const normalized = value.trim().toLowerCase();
    return REACHABILITY_CLASS_VALUES.find((entry) => entry === normalized) ?? null;
}
function isPrivateIpv4Address(hostname) {
    if (PRIVATE_IPV4_PREFIXES.some((prefix) => hostname.startsWith(prefix))) {
        return true;
    }
    const octets = hostname.split('.');
    if (octets.length !== 4 || octets[0] !== '172') {
        return false;
    }
    const secondOctet = Number.parseInt(octets[1] ?? '', 10);
    return (Number.isFinite(secondOctet) &&
        secondOctet >= PRIVATE_IPV4_SECOND_OCTET_172_MIN &&
        secondOctet <= PRIVATE_IPV4_SECOND_OCTET_172_MAX);
}
function isPrivateIpv6Address(hostname) {
    const normalizedHostname = hostname.toLowerCase();
    return PRIVATE_IPV6_PREFIXES.some((prefix) => normalizedHostname.startsWith(prefix));
}
function inferReachabilityClass(reachableHttpUrl, explicitReachabilityClass) {
    if (explicitReachabilityClass) {
        return explicitReachabilityClass;
    }
    if (!reachableHttpUrl) {
        return REACHABILITY_CLASS.UNREACHABLE;
    }
    const hostname = new URL(reachableHttpUrl).hostname.toLowerCase();
    if (REVERSE_TUNNEL_SUFFIXES.some((suffix) => hostname.endsWith(suffix))) {
        return REACHABILITY_CLASS.REVERSE_TUNNEL;
    }
    if (hostname === 'localhost' ||
        PRIVATE_HOSTNAME_SUFFIXES.some((suffix) => hostname.endsWith(suffix))) {
        return REACHABILITY_CLASS.LAN;
    }
    const ipKind = isIP(hostname);
    if (ipKind === 4 && isPrivateIpv4Address(hostname)) {
        return REACHABILITY_CLASS.LAN;
    }
    if (ipKind === 6 && isPrivateIpv6Address(hostname)) {
        return REACHABILITY_CLASS.LAN;
    }
    return REACHABILITY_CLASS.PUBLIC;
}
function resolveHealthEndpoint(explicitHealthEndpoint, reachableHttpUrl, reachabilityClass) {
    if (reachabilityClass === REACHABILITY_CLASS.UNREACHABLE) {
        return null;
    }
    const normalizedHealthEndpoint = normalizeHttpUrl(explicitHealthEndpoint);
    if (normalizedHealthEndpoint) {
        return normalizedHealthEndpoint;
    }
    if (!reachableHttpUrl) {
        return null;
    }
    return new URL('/health', reachableHttpUrl).toString().replace(/\/$/, '');
}
function resolveReachabilityMetadata(input) {
    const reachableHttpUrl = normalizeHttpUrl(input.reachableHttpUrl ??
        input.environment[ENVIRONMENT_KEY.AGENT_HTTP_URL] ??
        input.environment[ENVIRONMENT_KEY.REACHABLE_HTTP_URL]);
    const explicitReachabilityClass = normalizeReachabilityClass(input.reachabilityClass ?? input.environment[ENVIRONMENT_KEY.REACHABILITY_CLASS]);
    const reachabilityClass = inferReachabilityClass(reachableHttpUrl, explicitReachabilityClass);
    return {
        reachable_http_url: reachableHttpUrl,
        reachability_class: reachabilityClass,
        health_endpoint: resolveHealthEndpoint(input.healthEndpoint ?? input.environment[ENVIRONMENT_KEY.HEALTH_ENDPOINT] ?? null, reachableHttpUrl, reachabilityClass),
        tls_fingerprint: asNonEmptyString(input.tlsFingerprint ?? input.environment[ENVIRONMENT_KEY.TLS_FINGERPRINT]),
    };
}
function resolveControlPlaneConfig(workspaceRoot, environment) {
    const workspaceConfigPath = join(workspaceRoot, CONFIG_FILES.WORKSPACE_CONFIG);
    if (!existsSync(workspaceConfigPath)) {
        return null;
    }
    let parsedWorkspace;
    try {
        parsedWorkspace = parseYAML(readFileSync(workspaceConfigPath, { encoding: 'utf-8' }));
    }
    catch {
        return null;
    }
    const workspaceId = asNonEmptyString(parsedWorkspace?.id);
    const endpoint = asNonEmptyString(parsedWorkspace?.control_plane?.endpoint);
    const tokenEnv = asNonEmptyString(parsedWorkspace?.control_plane?.auth?.token_env);
    if (!workspaceId || !endpoint || !tokenEnv) {
        return null;
    }
    const token = asNonEmptyString(environment[tokenEnv]);
    if (!token) {
        return null;
    }
    try {
        void new URL(endpoint);
    }
    catch {
        return null;
    }
    return {
        workspaceId,
        endpoint: normalizeEndpoint(endpoint),
        token,
    };
}
function buildAgentHostEnrollmentUrl(endpoint, workspaceId) {
    return `${endpoint}${WORKSPACE_AGENT_HOST_ROUTE_PREFIX}/${encodeURIComponent(workspaceId)}/${WORKSPACE_AGENT_HOST_ROUTE_SUFFIX}`;
}
async function postAgentHostEnrollment(config, payload) {
    const response = await fetch(buildAgentHostEnrollmentUrl(config.endpoint, config.workspaceId), {
        method: 'POST',
        headers: {
            [HTTP_HEADER.AUTHORIZATION]: `Bearer ${config.token}`,
            [HTTP_HEADER.CONTENT_TYPE]: CONTENT_TYPE_JSON,
        },
        body: JSON.stringify(payload),
    });
    if (!response.ok) {
        throw createError(ErrorCodes.COMMAND_EXECUTION_FAILED, `Agent host enrollment failed for workspace ${config.workspaceId}: HTTP ${response.status}.`, {
            status: response.status,
            workspaceId: config.workspaceId,
        });
    }
}
const program = new Command()
    .name('agent:session')
    .description('Start an agent session')
    .requiredOption('--wu <id>', 'WU ID to work on (e.g., WU-1234)')
    .requiredOption('--tier <tier>', 'Context tier (1, 2, or 3)')
    .option('--agent-type <type>', 'Agent type (e.g. claude-code, codex-cli, gemini-cli)', 'claude-code')
    .option('--reachable-http-url <url>', 'Reachable HTTP URL for inbound tool dispatch (overrides env-based detection)')
    .option('--reachability-class <class>', 'Reachability class (reverse-tunnel, public, lan, unreachable)')
    .option('--health-endpoint <url>', 'Optional health endpoint for agent host discovery')
    .option('--tls-fingerprint <fingerprint>', 'Optional TLS fingerprint for the reachable host')
    // WU-2754 (ADR-014 extension 2): carry lifecycle_role + specialty_profile on every session
    // start so the session record links back to its parent delegation registry record.
    .option('--lifecycle-role <role>', 'ADR-014 lifecycle_role for this session (e.g. orchestrator, executor, reviewer, finisher)')
    .option('--specialty-profile <profile>', 'ADR-014 specialty_profile for this session (e.g. delivery, test, general)')
    .option('--delegation-id <id>', 'Parent delegation registry reference (dlg-XXXX) to cross-link this session with delegation:list output')
    .action(async (opts) => {
    try {
        const existing = await getCurrentSession();
        if (existing) {
            const message = `Session already active for ${existing.wu_id}`;
            console.error(chalk.red(message));
            console.error(`Run: pnpm agent:session:end to close it first.`);
            throw new ProcessExitError(message, EXIT_CODES.ERROR);
        }
        const tier = Number.parseInt(opts.tier, 10);
        if (![1, 2, 3].includes(tier)) {
            const message = 'Invalid tier. Must be 1, 2, or 3.';
            console.error(chalk.red(message));
            throw new ProcessExitError(message, EXIT_CODES.ERROR);
        }
        if (opts.reachabilityClass && normalizeReachabilityClass(opts.reachabilityClass) === null) {
            const message = 'Invalid reachability class. Use reverse-tunnel, public, lan, or unreachable.';
            console.error(chalk.red(message));
            throw new ProcessExitError(message, EXIT_CODES.ERROR);
        }
        const sessionId = await startSession(opts.wu, tier, opts.agentType, {
            lifecycleRole: opts.lifecycleRole,
            specialtyProfile: opts.specialtyProfile,
            delegationId: opts.delegationId,
        });
        const activeSession = await getCurrentSession();
        const conductorSink = await resolveConductorEventSink();
        const controlPlaneConfig = resolveControlPlaneConfig(process.cwd(), process.env);
        const activeLane = asNonEmptyString(activeSession?.lane) ?? 'Unknown';
        if (activeSession && controlPlaneConfig) {
            const reachabilityMetadata = resolveReachabilityMetadata({
                reachableHttpUrl: opts.reachableHttpUrl,
                reachabilityClass: opts.reachabilityClass,
                healthEndpoint: opts.healthEndpoint,
                tlsFingerprint: opts.tlsFingerprint,
                environment: process.env,
            });
            const now = new Date().toISOString();
            try {
                await postAgentHostEnrollment(controlPlaneConfig, {
                    workspace_id: controlPlaneConfig.workspaceId,
                    session_id: sessionId,
                    agent_id: activeSession.agent_type,
                    host_id: asNonEmptyString(activeSession.host_id),
                    client_type: activeSession.client_type ?? activeSession.agent_type,
                    agent_version: asNonEmptyString(activeSession.agent_version),
                    lane: asNonEmptyString(activeSession.lane),
                    wu_id: opts.wu,
                    enrolled_at: now,
                    last_seen_at: now,
                    ...reachabilityMetadata,
                });
                conductorSink(buildAgentSessionEnrolledEvent({
                    session_id: sessionId,
                    wu_id: opts.wu,
                    lane: activeLane,
                    client_type: activeSession.client_type ?? activeSession.agent_type,
                }));
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                console.warn(chalk.yellow(`Warning: agent session started locally, but host enrollment could not be sent: ${message}`));
                conductorSink(buildAgentSessionStalledEvent({
                    session_id: sessionId,
                    wu_id: opts.wu,
                    lane: activeLane,
                    stage: 'enrollment',
                    reason: message,
                }));
            }
        }
        // WU-2642 (INIT-057 Phase 4): Start sub-minute control-plane sync
        // sidecar so durable state changes during the session reach the cloud
        // within sync_interval. Best-effort — never blocks session start.
        try {
            const { spawnControlPlaneSidecar } = await import('./sidecar-manager.js');
            const sidecarResult = await spawnControlPlaneSidecar({
                workspaceRoot: process.cwd(),
                logger: {
                    warn: (message) => console.warn(chalk.yellow(`[agent:session] ${message}`)),
                },
            });
            if (sidecarResult.spawned) {
                console.log(chalk.dim(`  Sidecar: control-plane sync pid=${sidecarResult.pid}`));
            }
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            console.warn(chalk.yellow(`Warning: control-plane sync sidecar could not be started: ${message}`));
            conductorSink(buildAgentSessionStalledEvent({
                session_id: sessionId,
                wu_id: opts.wu,
                lane: activeLane,
                stage: 'sidecar_start',
                reason: message,
            }));
        }
        console.log(chalk.green(`✓ Session started`));
        console.log(`  Session ID: ${chalk.cyan(sessionId.slice(0, 8))}...`);
        console.log(`  WU: ${chalk.cyan(opts.wu)}`);
        console.log(`  Tier: ${chalk.cyan(tier)}`);
        console.log(`  Agent: ${chalk.cyan(opts.agentType)}`);
        // WU-2754 (ADR-014 extension 2): echo role axes when supplied so the
        // operator can confirm the session is correctly tagged.
        if (opts.lifecycleRole) {
            console.log(`  Lifecycle role: ${chalk.cyan(opts.lifecycleRole)}`);
        }
        if (opts.specialtyProfile) {
            console.log(`  Specialty profile: ${chalk.cyan(opts.specialtyProfile)}`);
        }
        if (opts.delegationId) {
            console.log(`  Delegation: ${chalk.cyan(opts.delegationId)}`);
        }
        if (opts.reachableHttpUrl) {
            console.log(`  Reachable HTTP URL: ${chalk.cyan(opts.reachableHttpUrl)}`);
        }
    }
    catch (err) {
        if (err instanceof ProcessExitError) {
            throw err;
        }
        const message = err instanceof Error ? err.message : String(err);
        console.error(chalk.red(`Error: ${message}`));
        throw new ProcessExitError(message, EXIT_CODES.ERROR);
    }
});
export async function main() {
    await program.parseAsync(process.argv);
}
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=agent-session.js.map