#!/usr/bin/env node
import { Command } from "commander";
import { DipclawConfig } from "./config/types.js";
import { Agent } from "./agent/agent.js";
import { CenterClient, DuplicateWorkerConnectionError, } from "./center/center-client.js";
import { CenterFrontend } from "./center/center-frontend.js";
import { syncOmniFlowSessionBlob, } from "./center/session-sync.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const pkg = require("../package.json");
const program = new Command();
program
    .name("dp-worker")
    .description("Simplified browser automation agent")
    .version(pkg.version)
    .option("-c, --config <path>", "Path to config file (JSON or YAML)")
    .option("--name <name>", "Agent name (also browser profile name)")
    .option("--workspace <path>", "Workspace directory path")
    .option("--llm-protocol <protocol>", "LLM protocol: openai or claude", process.env.DIPCLAW_LLM_PROTOCOL)
    .option("--llm-url <url>", "LLM API base URL", process.env.DIPCLAW_LLM_URL)
    .option("--llm-key <key>", "LLM API key", process.env.DIPCLAW_LLM_KEY)
    .option("--llm-model <model>", "LLM model name", process.env.DIPCLAW_LLM_MODEL)
    .option("--telegram-token <token>", "Telegram bot token", process.env.DIPCLAW_TELEGRAM_TOKEN)
    .option("--telegram-users <ids>", "Allowed Telegram user IDs (comma-separated)", process.env.DIPCLAW_TELEGRAM_USERS)
    .option("--max-iterations <n>", "Maximum tool-calling iterations per chat (default: 90)")
    .option("--debug", "Enable debug mode (log LLM requests/responses)", false)
    .option("--headless", "Run browser in headless mode", false)
    .option("--proxy <url>", "Browser proxy (e.g. socks5://127.0.0.1:1080)", process.env.DIPCLAW_PROXY)
    .option("--viewport <size>", "Browser viewport size (WxH, default: maximized)")
    .option("--timezone <tz>", "Browser timezone (e.g. America/New_York)")
    .option("--browser-connection <mode>", "Browser connection mode: extension-relay (default) or legacy remote-debug", process.env.DIPCLAW_BROWSER_CONNECTION)
    .option("--browser-backend <backend>", "Browser automation backend: chrome (default) or cloakbrowser", process.env.DIPCLAW_BROWSER_BACKEND)
    .option("--tui", "Enable interactive terminal UI (default: off)")
    .option("--center-url <url>", "dp-center base URL (enables center mode; requires --worker-key). Mutually exclusive with --config.", process.env.DP_CENTER_URL)
    .option("--worker-key <key>", "Bearer token issued by dp-center for this worker. Required with --center-url.", process.env.DP_WORKER_KEY)
    .option("--worker-id <id>", "Expected worker id when launched by dp-director; validated after bootstrap.");
const opts = program.opts();
function parseViewport(vp) {
    const [w, h] = vp.split("x").map(Number);
    if (!w || !h) {
        console.error(`Invalid viewport format: ${vp}. Expected WxH (e.g., 1280x720)`);
        process.exit(1);
    }
    return { width: w, height: h };
}
/** Parse a proxy URL string into structured config */
function parseProxyString(proxy) {
    try {
        const url = new URL(proxy);
        const protocol = url.protocol.replace(":", "");
        // URL strips the port when it matches the scheme default (http=80, https=443, ws=80, wss=443).
        const defaultPorts = { http: 80, https: 443, ws: 80, wss: 443 };
        const port = url.port ? parseInt(url.port, 10) : defaultPorts[protocol];
        if (!port || Number.isNaN(port)) {
            console.error(`Invalid proxy URL: ${proxy}. Missing port and no default for protocol '${protocol}'.`);
            process.exit(1);
        }
        return {
            protocol,
            host: url.hostname,
            port,
            ...(url.username ? { username: decodeURIComponent(url.username) } : {}),
            ...(url.password ? { password: decodeURIComponent(url.password) } : {}),
        };
    }
    catch {
        console.error(`Invalid proxy URL: ${proxy}. Expected format: protocol://[user:pass@]host:port`);
        process.exit(1);
    }
}
/**
 * Load config from a JSON file.
 * Supports ${ENV_VAR} and $ENV_VAR syntax for environment variable substitution.
 */
function loadConfigFile(filePath) {
    const absPath = path.resolve(filePath);
    if (!fs.existsSync(absPath)) {
        console.error(`Config file not found: ${absPath}`);
        process.exit(1);
    }
    let content = fs.readFileSync(absPath, "utf-8");
    // Substitute environment variables: ${VAR} or $VAR
    content = content.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, bare) => {
        const varName = braced || bare;
        return process.env[varName] ?? "";
    });
    try {
        return JSON.parse(content);
    }
    catch (err) {
        console.error(`Failed to parse config file: ${err instanceof Error ? err.message : err}`);
        process.exit(1);
    }
}
/** Build rawConfig from CLI flags + env vars (original behavior) */
function buildConfigFromCli() {
    if (!opts.name || !opts.workspace) {
        console.error("Error: --name and --workspace are required when not using --config");
        process.exit(1);
    }
    return {
        name: opts.name,
        workspace: path.resolve(opts.workspace),
        llm: {
            protocol: opts.llmProtocol,
            baseUrl: opts.llmUrl,
            apiKey: opts.llmKey,
            model: opts.llmModel,
        },
        debug: opts.debug,
        tui: opts.tui || false,
        telegram: opts.telegramToken
            ? {
                botToken: opts.telegramToken,
                allowedUsers: opts.telegramUsers
                    ? opts.telegramUsers.split(",").map(Number)
                    : [],
            }
            : undefined,
        browser: {
            connectionMode: opts.browserConnection,
            headless: opts.headless,
            ...(opts.browserBackend ? { backend: opts.browserBackend } : {}),
            proxy: opts.proxy ? parseProxyString(opts.proxy) : undefined,
            viewport: opts.viewport ? parseViewport(opts.viewport) : undefined,
        },
    };
}
/** Merge file config with CLI overrides. CLI flags take precedence. */
function buildConfig() {
    let base = {};
    if (opts.config) {
        base = loadConfigFile(opts.config);
        // Resolve workspace relative to config file directory
        if (typeof base.workspace === "string" && !path.isAbsolute(base.workspace)) {
            base.workspace = path.resolve(path.dirname(path.resolve(opts.config)), base.workspace);
        }
    }
    // CLI overrides on top of file config
    if (opts.name)
        base.name = opts.name;
    if (opts.workspace)
        base.workspace = path.resolve(opts.workspace);
    const hasLlmCli = opts.llmProtocol || opts.llmUrl || opts.llmKey || opts.llmModel;
    if (hasLlmCli) {
        const fileLlm = base.llm || {};
        base.llm = {
            ...fileLlm,
            ...(opts.llmProtocol ? { protocol: opts.llmProtocol } : {}),
            ...(opts.llmUrl ? { baseUrl: opts.llmUrl } : {}),
            ...(opts.llmKey ? { apiKey: opts.llmKey } : {}),
            ...(opts.llmModel ? { model: opts.llmModel } : {}),
        };
    }
    if (opts.telegramToken) {
        base.telegram = {
            botToken: opts.telegramToken,
            allowedUsers: opts.telegramUsers
                ? opts.telegramUsers.split(",").map(Number)
                : [],
        };
    }
    const hasBrowserCli = opts.headless ||
        opts.proxy ||
        opts.viewport ||
        opts.timezone ||
        opts.browserConnection ||
        opts.browserBackend;
    if (hasBrowserCli) {
        const fileBrowser = base.browser || {};
        base.browser = {
            ...fileBrowser,
            ...(opts.browserConnection ? { connectionMode: opts.browserConnection } : {}),
            ...(opts.headless ? { headless: true } : {}),
            ...(opts.browserBackend ? { backend: opts.browserBackend } : {}),
            ...(opts.proxy ? { proxy: parseProxyString(opts.proxy) } : {}),
            ...(opts.viewport ? { viewport: parseViewport(opts.viewport) } : {}),
            ...(opts.timezone ? { timezone: opts.timezone } : {}),
        };
    }
    if (opts.maxIterations)
        base.maxIterations = parseInt(opts.maxIterations, 10);
    if (opts.debug)
        base.debug = true;
    if (opts.tui)
        base.tui = true;
    if (!base.name || !base.workspace) {
        console.error("Error: 'name' and 'workspace' are required (via --config file or CLI flags)");
        process.exit(1);
    }
    return base;
}
/**
 * Idempotent workspace layout setup. Same dirs/files the local-mode path
 * has always created; center mode also calls this once it knows the workspace.
 */
function ensureWorkspaceLayout(workspace) {
    const dirs = [
        workspace,
        path.join(workspace, "browser-data"),
        path.join(workspace, "skills"),
        path.join(workspace, "logs"),
        path.join(workspace, "tasks"),
        path.join(workspace, "memory"),
        path.join(workspace, "sessions"),
        path.join(workspace, "attachments"),
    ];
    for (const dir of dirs)
        fs.mkdirSync(dir, { recursive: true });
    const tasksFile = path.join(workspace, "tasks", "tasks.json");
    if (!fs.existsSync(tasksFile))
        fs.writeFileSync(tasksFile, "[]", "utf-8");
    const memoryIndex = path.join(workspace, "memory", "MEMORY.md");
    if (!fs.existsSync(memoryIndex))
        fs.writeFileSync(memoryIndex, "# Agent Memory\n\n", "utf-8");
}
/**
 * Wire up signal handlers + crash handlers around an `agent.stop()` /
 * `extraShutdown()` shutdown sequence. Returns nothing; the handlers
 * eventually call process.exit.
 */
function installShutdownHandlers(agent, extraShutdown) {
    let stopping = false;
    let pendingExitCode = 0;
    const shutdown = async (signal, exitCode = 0) => {
        if (stopping) {
            console.log(`\n⚠️  Received second ${signal}, force exiting.`);
            process.exit(pendingExitCode || (signal === "SIGINT" ? 130 : 143));
        }
        stopping = true;
        pendingExitCode = exitCode;
        console.log(`\n🛑 Received ${signal}, shutting down...`);
        try {
            await extraShutdown();
        }
        catch (err) {
            console.error("Shutdown hook error:", err);
        }
        await agent.stop();
        process.exit(exitCode);
    };
    process.on("SIGINT", () => shutdown("SIGINT"));
    process.on("SIGTERM", () => shutdown("SIGTERM"));
    process.on("uncaughtException", (err) => {
        console.error("💥 Uncaught exception:", err);
        shutdown("uncaughtException", 1).catch(() => process.exit(1));
    });
    process.on("unhandledRejection", (reason) => {
        console.error("💥 Unhandled rejection:", reason);
        shutdown("unhandledRejection", 1).catch(() => process.exit(1));
    });
}
/** Standalone (legacy) boot path: build config from --config/CLI, start Agent. */
async function localMain() {
    const rawConfig = buildConfig();
    const parseResult = DipclawConfig.safeParse(rawConfig);
    if (!parseResult.success) {
        console.error("Invalid configuration:");
        console.error(parseResult.error.issues
            .map((i) => `  - ${i.path.join(".")}: ${i.message}`)
            .join("\n"));
        process.exit(1);
    }
    const config = parseResult.data;
    ensureWorkspaceLayout(config.workspace);
    console.log(`🤖 Starting dipclaw agent: ${config.name}`);
    console.log(`📁 Workspace: ${config.workspace}`);
    console.log(`🧠 LLM: ${config.llm.protocol} @ ${config.llm.baseUrl} (${config.llm.model})`);
    const agent = new Agent(config);
    installShutdownHandlers(agent, async () => { });
    await agent.start();
}
/** center-mode boot path: bootstrap config from dp-center, wire center frontend. */
async function centerMain() {
    const centerUrl = opts.centerUrl;
    const workerKey = opts.workerKey;
    // Default workspace lives under ~/.dipclaw/<short-key-hash>/ when not
    // explicitly set, so the director can spawn multiple workers on one host without
    // them colliding. The short hash is non-secret; it's just a stable
    // directory name derived from the key the host already holds.
    const workspace = opts.workspace
        ? path.resolve(opts.workspace)
        : path.join(os.homedir(), ".dipclaw", await shortKeyHash(workerKey));
    ensureWorkspaceLayout(workspace);
    console.log(`🤖 Starting dipclaw agent in center mode`);
    console.log(`📁 Workspace: ${workspace}`);
    console.log(`☁️  dp-center: ${centerUrl}`);
    const client = new CenterClient({
        centerUrl,
        workerKey,
        workspace,
        headless: opts.headless,
    });
    client.on("error", (err) => {
        console.warn(`⚠️ CenterClient warning (connection error): ${err.message}`);
    });
    const parsed = await client.bootstrap();
    const config = parsed.config;
    const extras = parsed.extras;
    if (opts.workerId && opts.workerId !== extras.workerId) {
        console.error(`Error: --worker-id ${opts.workerId} does not match center bootstrap worker_id ${extras.workerId}.`);
        process.exit(1);
    }
    console.log(`🆔 worker ${extras.workerId} (${config.name}) — config_etag=${extras.configEtag}`);
    if (extras.sessionBlob) {
        const result = await syncOmniFlowSessionBlob(config.workspace, extras.sessionBlob);
        console.log(result.status === "skipped"
            ? `📦 OmniFlow session already current (hash: ${extras.sessionBlob.content_hash.slice(0, 12)}…)`
            : `📦 OmniFlow session synced (hash: ${extras.sessionBlob.content_hash.slice(0, 12)}…)`);
    }
    else {
        console.log("📦 No OmniFlow session configured for this identity.");
    }
    const agent = new Agent(config);
    const frontend = new CenterFrontend({
        agent,
        client,
        workerId: extras.workerId,
        version: pkg.version,
    });
    installShutdownHandlers(agent, async () => {
        await frontend.stop();
        await client.close();
    });
    await frontend.start();
    try {
        await client.connectAndWait();
    }
    catch (err) {
        await frontend.stop();
        await client.close();
        if (err instanceof DuplicateWorkerConnectionError) {
            console.error(`Error: worker ${extras.workerId} is already connected to center; exiting.`);
            process.exit(42);
        }
        throw err;
    }
    await agent.start({ skipScheduler: true });
}
async function shortKeyHash(key) {
    // Small one-way digest for a deterministic per-key dir. crypto.subtle is
    // available in Node ≥18; falls back to nothing if it ever isn't.
    const bytes = new TextEncoder().encode(key);
    const digest = await crypto.subtle.digest("SHA-256", bytes);
    const hex = Array.from(new Uint8Array(digest))
        .map((b) => b.toString(16).padStart(2, "0"))
        .join("");
    return hex.slice(0, 12);
}
async function main() {
    const centerMode = Boolean(opts.centerUrl && opts.workerKey);
    if (opts.centerUrl && !opts.workerKey) {
        console.error("Error: --center-url requires --worker-key.");
        process.exit(1);
    }
    if (opts.workerKey && !opts.centerUrl) {
        console.error("Error: --worker-key requires --center-url.");
        process.exit(1);
    }
    if (centerMode && opts.config) {
        console.error("Error: --config is mutually exclusive with --center-url/--worker-key (center pushes the config).");
        process.exit(1);
    }
    if (centerMode) {
        await centerMain();
    }
    else {
        await localMain();
    }
}
program.parse(process.argv);
main().catch((err) => {
    console.error("Fatal error:", err);
    process.exit(1);
});
//# sourceMappingURL=index.js.map