@tractorscorch/clank 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -843,10 +843,18 @@ var init_agent = __esm({
843
843
  alwaysApproved = /* @__PURE__ */ new Set();
844
844
  /** Background task registry (if available) */
845
845
  taskRegistry = null;
846
- /** Function to spawn background tasks (main agent only) */
846
+ /** Function to spawn background tasks */
847
847
  spawnTaskFn = void 0;
848
- /** Session key for this engine (used to consume completed tasks) */
848
+ /** Function to kill a running task */
849
+ killTaskFn = void 0;
850
+ /** Function to message a running child task */
851
+ messageTaskFn = void 0;
852
+ /** Session key for this engine */
849
853
  sessionKey = "";
854
+ /** Spawn depth: 0 = main, 1+ = sub-agent */
855
+ spawnDepth = 0;
856
+ /** Maximum allowed spawn depth */
857
+ maxSpawnDepth = 1;
850
858
  constructor(opts) {
851
859
  super();
852
860
  this.setMaxListeners(30);
@@ -858,7 +866,11 @@ var init_agent = __esm({
858
866
  if (opts.systemPrompt) this.systemPrompt = opts.systemPrompt;
859
867
  if (opts.taskRegistry) this.taskRegistry = opts.taskRegistry;
860
868
  if (opts.spawnTask) this.spawnTaskFn = opts.spawnTask;
869
+ if (opts.killTask) this.killTaskFn = opts.killTask;
870
+ if (opts.messageTask) this.messageTaskFn = opts.messageTask;
861
871
  if (opts.sessionKey) this.sessionKey = opts.sessionKey;
872
+ if (opts.spawnDepth !== void 0) this.spawnDepth = opts.spawnDepth;
873
+ if (opts.maxSpawnDepth !== void 0) this.maxSpawnDepth = opts.maxSpawnDepth;
862
874
  this.contextEngine = new ContextEngine({
863
875
  contextWindow: opts.provider.provider.contextWindow(),
864
876
  isLocal: opts.provider.isLocal
@@ -1076,7 +1088,12 @@ ${results}`
1076
1088
  agentId: this.identity.id,
1077
1089
  signal,
1078
1090
  taskRegistry: this.taskRegistry ?? void 0,
1079
- spawnTask: this.spawnTaskFn
1091
+ spawnTask: this.spawnTaskFn,
1092
+ killTask: this.killTaskFn,
1093
+ messageTask: this.messageTaskFn,
1094
+ spawnDepth: this.spawnDepth,
1095
+ maxSpawnDepth: this.maxSpawnDepth,
1096
+ sessionKey: this.sessionKey
1080
1097
  };
1081
1098
  const validation = tool.validate(tc.arguments, toolCtx);
1082
1099
  if (!validation.ok) {
@@ -2131,7 +2148,11 @@ function defaultConfig() {
2131
2148
  model: { primary: "ollama/qwen3.5" },
2132
2149
  workspace: process.cwd(),
2133
2150
  toolTier: "auto",
2134
- temperature: 0.7
2151
+ temperature: 0.7,
2152
+ subagents: {
2153
+ maxConcurrent: 8,
2154
+ maxSpawnDepth: 1
2155
+ }
2135
2156
  },
2136
2157
  list: []
2137
2158
  },
@@ -3488,6 +3509,34 @@ function createProvider(modelId, config, opts) {
3488
3509
  });
3489
3510
  return { provider: p, providerName: "openrouter", modelId, isLocal: false };
3490
3511
  }
3512
+ case "codex": {
3513
+ const codexConfig = config.codex ?? config["codex"];
3514
+ if (!codexConfig?.apiKey) {
3515
+ throw new Error(
3516
+ `Codex OAuth not configured. Run 'clank auth login' to sign in with your OpenAI account.`
3517
+ );
3518
+ }
3519
+ const p = new OpenAIProvider({
3520
+ apiKey: codexConfig.apiKey,
3521
+ baseUrl: "https://api.openai.com",
3522
+ model,
3523
+ maxResponseTokens: opts?.maxResponseTokens
3524
+ });
3525
+ return { provider: p, providerName: "codex", modelId, isLocal: false };
3526
+ }
3527
+ case "opencode": {
3528
+ const ocConfig = config.opencode ?? config["opencode"];
3529
+ if (!ocConfig?.apiKey) {
3530
+ throw new Error(`OpenCode API key required for model ${modelId}`);
3531
+ }
3532
+ const p = new OpenAIProvider({
3533
+ apiKey: ocConfig.apiKey,
3534
+ baseUrl: ocConfig.baseUrl || "https://opencode.ai/zen",
3535
+ model,
3536
+ maxResponseTokens: opts?.maxResponseTokens
3537
+ });
3538
+ return { provider: p, providerName: "opencode", modelId, isLocal: false };
3539
+ }
3491
3540
  case "lmstudio":
3492
3541
  case "llamacpp":
3493
3542
  case "vllm":
@@ -3627,7 +3676,7 @@ var init_model_tool = __esm({
3627
3676
  properties: {
3628
3677
  action: { type: "string", description: "'list', 'detect', 'set-default', or 'add-provider'" },
3629
3678
  model: { type: "string", description: "Model ID for set-default (e.g., 'ollama/qwen3.5')" },
3630
- provider: { type: "string", description: "Provider name for add-provider ('anthropic', 'openai', 'google', 'openrouter')" },
3679
+ provider: { type: "string", description: "Provider name for add-provider ('anthropic', 'openai', 'google', 'openrouter', 'opencode')" },
3631
3680
  apiKey: { type: "string", description: "API key for add-provider" }
3632
3681
  },
3633
3682
  required: ["action"]
@@ -3678,6 +3727,8 @@ var init_model_tool = __esm({
3678
3727
  const entry = { apiKey: args.apiKey };
3679
3728
  if (provider === "openrouter") {
3680
3729
  entry.baseUrl = "https://openrouter.ai/api/v1";
3730
+ } else if (provider === "opencode") {
3731
+ entry.baseUrl = "https://opencode.ai/zen";
3681
3732
  }
3682
3733
  config.models.providers[provider] = entry;
3683
3734
  await saveConfig(config);
@@ -4376,17 +4427,17 @@ var init_tts = __esm({
4376
4427
  /** Transcribe via local whisper.cpp */
4377
4428
  async transcribeLocal(audioBuffer, format) {
4378
4429
  try {
4379
- const { writeFile: writeFile10, unlink: unlink5 } = await import("fs/promises");
4430
+ const { writeFile: writeFile11, unlink: unlink6 } = await import("fs/promises");
4380
4431
  const { execSync: execSync3 } = await import("child_process");
4381
- const { join: join20 } = await import("path");
4432
+ const { join: join21 } = await import("path");
4382
4433
  const { tmpdir } = await import("os");
4383
- const tmpFile = join20(tmpdir(), `clank-stt-${Date.now()}.${format}`);
4384
- await writeFile10(tmpFile, audioBuffer);
4434
+ const tmpFile = join21(tmpdir(), `clank-stt-${Date.now()}.${format}`);
4435
+ await writeFile11(tmpFile, audioBuffer);
4385
4436
  const output = execSync3(`whisper "${tmpFile}" --model base.en --output-txt`, {
4386
4437
  encoding: "utf-8",
4387
4438
  timeout: 6e4
4388
4439
  });
4389
- await unlink5(tmpFile).catch(() => {
4440
+ await unlink6(tmpFile).catch(() => {
4390
4441
  });
4391
4442
  return output.trim() ? { text: output.trim() } : null;
4392
4443
  } catch {
@@ -4449,11 +4500,11 @@ var init_voice_tool = __esm({
4449
4500
  voiceId: args.voice_id
4450
4501
  });
4451
4502
  if (!result) return "Error: TTS synthesis failed";
4452
- const { writeFile: writeFile10 } = await import("fs/promises");
4453
- const { join: join20 } = await import("path");
4503
+ const { writeFile: writeFile11 } = await import("fs/promises");
4504
+ const { join: join21 } = await import("path");
4454
4505
  const { tmpdir } = await import("os");
4455
- const outPath = join20(tmpdir(), `clank-tts-${Date.now()}.${result.format}`);
4456
- await writeFile10(outPath, result.audioBuffer);
4506
+ const outPath = join21(tmpdir(), `clank-tts-${Date.now()}.${result.format}`);
4507
+ await writeFile11(outPath, result.audioBuffer);
4457
4508
  return `Audio generated: ${outPath} (${result.format}, ${Math.round(result.audioBuffer.length / 1024)}KB)`;
4458
4509
  }
4459
4510
  };
@@ -4476,19 +4527,19 @@ var init_voice_tool = __esm({
4476
4527
  return { ok: true };
4477
4528
  },
4478
4529
  async execute(args, ctx) {
4479
- const { readFile: readFile13 } = await import("fs/promises");
4480
- const { existsSync: existsSync12 } = await import("fs");
4530
+ const { readFile: readFile14 } = await import("fs/promises");
4531
+ const { existsSync: existsSync13 } = await import("fs");
4481
4532
  const { guardPath: guardPath2 } = await Promise.resolve().then(() => (init_path_guard(), path_guard_exports));
4482
4533
  const guard = guardPath2(args.file_path, ctx.projectRoot, { allowExternal: ctx.allowExternal });
4483
4534
  if (!guard.ok) return guard.error;
4484
4535
  const filePath = guard.path;
4485
- if (!existsSync12(filePath)) return `Error: File not found: ${filePath}`;
4536
+ if (!existsSync13(filePath)) return `Error: File not found: ${filePath}`;
4486
4537
  const config = await loadConfig();
4487
4538
  const engine = new STTEngine(config);
4488
4539
  if (!engine.isAvailable()) {
4489
4540
  return "Error: Speech-to-text not configured. Need OpenAI API key or local whisper.cpp installed.";
4490
4541
  }
4491
- const audioBuffer = await readFile13(filePath);
4542
+ const audioBuffer = await readFile14(filePath);
4492
4543
  const ext = filePath.split(".").pop() || "wav";
4493
4544
  const result = await engine.transcribe(audioBuffer, ext);
4494
4545
  if (!result) return "Error: Transcription failed";
@@ -4576,13 +4627,13 @@ var init_task_tool = __esm({
4576
4627
  taskTool = {
4577
4628
  definition: {
4578
4629
  name: "spawn_task",
4579
- description: "Spawn a background task on a sub-agent, check task status, or list tasks. Use 'spawn' to start a task that runs independently while you continue chatting. Use 'status' to check a specific task. Use 'list' to see all tasks.",
4630
+ description: "Manage background tasks on sub-agents. 'spawn' starts a task that runs independently while you continue chatting. 'kill' cancels a running task. 'steer' kills and re-spawns with new instructions. 'message' sends a message to a running child. 'status' checks one task. 'list' shows all.",
4580
4631
  parameters: {
4581
4632
  type: "object",
4582
4633
  properties: {
4583
4634
  action: {
4584
4635
  type: "string",
4585
- description: "Action: 'spawn' to start a task, 'status' to check one, 'list' to see all"
4636
+ description: "Action: 'spawn', 'status', 'list', 'kill', 'steer', or 'message'"
4586
4637
  },
4587
4638
  agentId: {
4588
4639
  type: "string",
@@ -4592,13 +4643,17 @@ var init_task_tool = __esm({
4592
4643
  type: "string",
4593
4644
  description: "The instruction for the sub-agent (required for spawn)"
4594
4645
  },
4646
+ message: {
4647
+ type: "string",
4648
+ description: "Message to send (required for steer and message actions)"
4649
+ },
4595
4650
  label: {
4596
4651
  type: "string",
4597
4652
  description: "Human-readable task name (optional for spawn)"
4598
4653
  },
4599
4654
  taskId: {
4600
4655
  type: "string",
4601
- description: "Task ID to check (required for status)"
4656
+ description: "Task ID (required for status, kill, steer, message)"
4602
4657
  },
4603
4658
  timeoutMs: {
4604
4659
  type: "number",
@@ -4609,13 +4664,14 @@ var init_task_tool = __esm({
4609
4664
  }
4610
4665
  },
4611
4666
  safetyLevel: ((args) => {
4612
- return args.action === "spawn" ? "medium" : "low";
4667
+ const action = args.action;
4668
+ return action === "spawn" || action === "kill" || action === "steer" ? "medium" : "low";
4613
4669
  }),
4614
4670
  readOnly: false,
4615
4671
  validate(args, _ctx) {
4616
4672
  const action = args.action;
4617
- if (!["spawn", "status", "list"].includes(action)) {
4618
- return { ok: false, error: "action must be 'spawn', 'status', or 'list'" };
4673
+ if (!["spawn", "status", "list", "kill", "steer", "message"].includes(action)) {
4674
+ return { ok: false, error: "action must be 'spawn', 'status', 'list', 'kill', 'steer', or 'message'" };
4619
4675
  }
4620
4676
  if (action === "spawn") {
4621
4677
  if (!args.agentId || typeof args.agentId !== "string") {
@@ -4625,8 +4681,14 @@ var init_task_tool = __esm({
4625
4681
  return { ok: false, error: "prompt is required for spawn" };
4626
4682
  }
4627
4683
  }
4628
- if (action === "status" && (!args.taskId || typeof args.taskId !== "string")) {
4629
- return { ok: false, error: "taskId is required for status" };
4684
+ if (["status", "kill", "message"].includes(action) && (!args.taskId || typeof args.taskId !== "string")) {
4685
+ return { ok: false, error: "taskId is required for " + action };
4686
+ }
4687
+ if (action === "steer" && (!args.taskId || !args.message)) {
4688
+ return { ok: false, error: "taskId and message are required for steer" };
4689
+ }
4690
+ if (action === "message" && !args.message) {
4691
+ return { ok: false, error: "message is required for message action" };
4630
4692
  }
4631
4693
  return { ok: true };
4632
4694
  },
@@ -4635,7 +4697,16 @@ var init_task_tool = __esm({
4635
4697
  switch (action) {
4636
4698
  case "spawn": {
4637
4699
  if (!ctx.spawnTask) {
4638
- return "Error: spawn_task is only available to the main agent. Sub-agents cannot spawn tasks.";
4700
+ if (ctx.spawnDepth !== void 0 && ctx.maxSpawnDepth !== void 0 && ctx.spawnDepth >= ctx.maxSpawnDepth) {
4701
+ return "Error: This agent is at maximum spawn depth and cannot create sub-agents.";
4702
+ }
4703
+ return "Error: spawn_task is not available in this context.";
4704
+ }
4705
+ if (ctx.taskRegistry && ctx.sessionKey) {
4706
+ const active = ctx.taskRegistry.countActiveByParent(ctx.sessionKey);
4707
+ if (active >= 8) {
4708
+ return `Error: Concurrent task limit reached (${active}/8 running). Kill a task first.`;
4709
+ }
4639
4710
  }
4640
4711
  const agentId = args.agentId;
4641
4712
  const prompt = args.prompt;
@@ -4655,6 +4726,50 @@ The task is running in the background. Results will be delivered when the task c
4655
4726
  return `Error spawning task: ${msg}`;
4656
4727
  }
4657
4728
  }
4729
+ case "kill": {
4730
+ if (!ctx.killTask) return "Error: kill is not available in this context.";
4731
+ try {
4732
+ const result = await ctx.killTask(args.taskId);
4733
+ if (result.status === "not_found") return `No running task found with ID: ${args.taskId}`;
4734
+ if (result.status === "not_owner") return `Cannot kill task ${args.taskId} \u2014 it was not spawned by this agent.`;
4735
+ return `Task ${args.taskId} killed.${result.cascadeKilled ? ` ${result.cascadeKilled} child task(s) also cancelled.` : ""}`;
4736
+ } catch (err) {
4737
+ return `Error killing task: ${err instanceof Error ? err.message : err}`;
4738
+ }
4739
+ }
4740
+ case "steer": {
4741
+ if (!ctx.killTask || !ctx.spawnTask) return "Error: steer is not available in this context.";
4742
+ const taskId = args.taskId;
4743
+ const message = args.message;
4744
+ const task = ctx.taskRegistry?.get(taskId);
4745
+ if (!task) return `No task found with ID: ${taskId}`;
4746
+ await ctx.killTask(taskId);
4747
+ try {
4748
+ const newId = await ctx.spawnTask({
4749
+ agentId: task.agentId,
4750
+ prompt: message,
4751
+ label: task.label + " (steered)",
4752
+ timeoutMs: task.timeoutMs
4753
+ });
4754
+ return `Task ${taskId} killed and re-spawned.
4755
+ New Task ID: ${newId}
4756
+ New instructions: ${message.slice(0, 100)}`;
4757
+ } catch (err) {
4758
+ return `Killed old task but failed to re-spawn: ${err instanceof Error ? err.message : err}`;
4759
+ }
4760
+ }
4761
+ case "message": {
4762
+ if (!ctx.messageTask) return "Error: message is not available in this context.";
4763
+ try {
4764
+ const result = await ctx.messageTask(args.taskId, args.message);
4765
+ if (result.status === "not_found") return `No running task found with ID: ${args.taskId}`;
4766
+ if (result.status === "not_owner") return `Cannot message task ${args.taskId} \u2014 it was not spawned by this agent.`;
4767
+ return `Message sent to task ${args.taskId}.
4768
+ Reply: ${result.replyText || "(no reply)"}`;
4769
+ } catch (err) {
4770
+ return `Error messaging task: ${err instanceof Error ? err.message : err}`;
4771
+ }
4772
+ }
4658
4773
  case "status": {
4659
4774
  if (!ctx.taskRegistry) return "Error: Task registry not available.";
4660
4775
  const task = ctx.taskRegistry.get(args.taskId);
@@ -4666,7 +4781,9 @@ The task is running in the background. Results will be delivered when the task c
4666
4781
  `Agent: ${task.agentId}`,
4667
4782
  `Model: ${task.model}`,
4668
4783
  `Status: ${task.status}`,
4669
- `Elapsed: ${elapsed}s`
4784
+ `Depth: ${task.spawnDepth}`,
4785
+ `Elapsed: ${elapsed}s`,
4786
+ `Children: ${task.children.length}`
4670
4787
  ];
4671
4788
  if (task.result) lines.push(`Result: ${task.result.slice(0, 500)}`);
4672
4789
  if (task.error) lines.push(`Error: ${task.error}`);
@@ -4678,7 +4795,8 @@ The task is running in the background. Results will be delivered when the task c
4678
4795
  if (tasks.length === 0) return "No background tasks.";
4679
4796
  return tasks.map((t) => {
4680
4797
  const elapsed = Math.round(((t.completedAt || Date.now()) - t.startedAt) / 1e3);
4681
- return `\u2022 [${t.status}] ${t.label} (agent: ${t.agentId}, ${elapsed}s)`;
4798
+ const depth = t.spawnDepth > 0 ? ` [depth ${t.spawnDepth}]` : "";
4799
+ return `\u2022 [${t.status}] ${t.label}${depth} (agent: ${t.agentId}, ${elapsed}s)`;
4682
4800
  }).join("\n");
4683
4801
  }
4684
4802
  default:
@@ -4686,7 +4804,11 @@ The task is running in the background. Results will be delivered when the task c
4686
4804
  }
4687
4805
  },
4688
4806
  formatConfirmation(args) {
4689
- return `Spawn background task on agent "${args.agentId}": ${args.prompt?.slice(0, 80)}`;
4807
+ const action = args.action;
4808
+ if (action === "spawn") return `Spawn background task on agent "${args.agentId}": ${args.prompt?.slice(0, 80)}`;
4809
+ if (action === "kill") return `Kill task ${args.taskId}`;
4810
+ if (action === "steer") return `Steer task ${args.taskId} with new instructions`;
4811
+ return `${action} task`;
4690
4812
  }
4691
4813
  };
4692
4814
  }
@@ -4812,9 +4934,9 @@ async function runChat(opts) {
4812
4934
  console.log(dim("Starting gateway..."));
4813
4935
  const { spawn } = await import("child_process");
4814
4936
  const { fileURLToPath: fileURLToPath6 } = await import("url");
4815
- const { dirname: dirname6, join: join20 } = await import("path");
4937
+ const { dirname: dirname6, join: join21 } = await import("path");
4816
4938
  const __filename4 = fileURLToPath6(import.meta.url);
4817
- const entryPoint = join20(dirname6(__filename4), "index.js");
4939
+ const entryPoint = join21(dirname6(__filename4), "index.js");
4818
4940
  const child = spawn(process.execPath, [entryPoint, "gateway", "start", "--foreground"], {
4819
4941
  detached: true,
4820
4942
  stdio: "ignore",
@@ -4840,10 +4962,10 @@ async function runChat(opts) {
4840
4962
  }
4841
4963
  const url = `http://127.0.0.1:${port}/#token=${token}`;
4842
4964
  console.log(dim(`Opening ${url}`));
4843
- const { platform: platform5 } = await import("os");
4844
- const { exec } = await import("child_process");
4845
- const openCmd = platform5() === "win32" ? `start "" "${url}"` : platform5() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
4846
- exec(openCmd);
4965
+ const { platform: platform6 } = await import("os");
4966
+ const { exec: exec2 } = await import("child_process");
4967
+ const openCmd = platform6() === "win32" ? `start "" "${url}"` : platform6() === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
4968
+ exec2(openCmd);
4847
4969
  console.log(green("Web UI opened in browser."));
4848
4970
  return;
4849
4971
  }
@@ -5397,32 +5519,58 @@ var init_registry2 = __esm({
5397
5519
  startedAt: Date.now(),
5398
5520
  timeoutMs: opts.timeoutMs,
5399
5521
  spawnedBy: opts.spawnedBy,
5400
- delivered: false
5522
+ delivered: false,
5523
+ spawnDepth: opts.spawnDepth ?? 0,
5524
+ parentSessionKey: opts.parentSessionKey,
5525
+ children: []
5401
5526
  };
5402
5527
  this.tasks.set(entry.id, entry);
5528
+ if (opts.parentSessionKey?.startsWith("task:")) {
5529
+ const parentTaskId = opts.parentSessionKey.slice(5);
5530
+ const parent = this.tasks.get(parentTaskId);
5531
+ if (parent) parent.children.push(entry.id);
5532
+ }
5403
5533
  return entry;
5404
5534
  }
5405
5535
  /** Update a task's fields */
5406
5536
  update(id, patch) {
5407
5537
  const task = this.tasks.get(id);
5408
- if (task) {
5409
- Object.assign(task, patch);
5410
- }
5538
+ if (task) Object.assign(task, patch);
5411
5539
  }
5412
5540
  /** Get a specific task */
5413
5541
  get(id) {
5414
5542
  return this.tasks.get(id);
5415
5543
  }
5416
- /** List all tasks, optionally filtered by status */
5544
+ /** Find a task by its session key (task:{id}) */
5545
+ getBySessionKey(sessionKey) {
5546
+ if (!sessionKey.startsWith("task:")) return void 0;
5547
+ return this.tasks.get(sessionKey.slice(5));
5548
+ }
5549
+ /** List all tasks, optionally filtered */
5417
5550
  list(filter) {
5418
5551
  let results = Array.from(this.tasks.values());
5419
- if (filter?.status) {
5420
- results = results.filter((t) => t.status === filter.status);
5552
+ if (filter?.status) results = results.filter((t) => t.status === filter.status);
5553
+ if (filter?.spawnedBy) results = results.filter((t) => t.spawnedBy === filter.spawnedBy);
5554
+ return results.sort((a, b) => b.startedAt - a.startedAt);
5555
+ }
5556
+ /** Count running tasks spawned by a specific session */
5557
+ countActiveByParent(spawnedBy) {
5558
+ let count = 0;
5559
+ for (const task of this.tasks.values()) {
5560
+ if (task.spawnedBy === spawnedBy && task.status === "running") count++;
5421
5561
  }
5422
- if (filter?.spawnedBy) {
5423
- results = results.filter((t) => t.spawnedBy === filter.spawnedBy);
5562
+ return count;
5563
+ }
5564
+ /** Recursively count all active descendants of a session */
5565
+ countActiveDescendants(sessionKey) {
5566
+ let count = 0;
5567
+ for (const task of this.tasks.values()) {
5568
+ if (task.spawnedBy === sessionKey && task.status === "running") {
5569
+ count++;
5570
+ count += this.countActiveDescendants(`task:${task.id}`);
5571
+ }
5424
5572
  }
5425
- return results.sort((a, b) => b.startedAt - a.startedAt);
5573
+ return count;
5426
5574
  }
5427
5575
  /**
5428
5576
  * Get completed tasks for a session that haven't been delivered yet.
@@ -5438,6 +5586,32 @@ var init_registry2 = __esm({
5438
5586
  }
5439
5587
  return ready;
5440
5588
  }
5589
+ /** Cancel a specific task */
5590
+ cancel(taskId) {
5591
+ const task = this.tasks.get(taskId);
5592
+ if (!task || task.status !== "running") return task;
5593
+ task.status = "timeout";
5594
+ task.completedAt = Date.now();
5595
+ task.error = "Cancelled by parent";
5596
+ return task;
5597
+ }
5598
+ /**
5599
+ * Cancel all tasks spawned by a session, and recursively cancel
5600
+ * their children. Returns total number of tasks cancelled.
5601
+ */
5602
+ cascadeCancel(sessionKey) {
5603
+ let count = 0;
5604
+ for (const task of this.tasks.values()) {
5605
+ if (task.spawnedBy === sessionKey && task.status === "running") {
5606
+ task.status = "timeout";
5607
+ task.completedAt = Date.now();
5608
+ task.error = "Parent cancelled";
5609
+ count++;
5610
+ count += this.cascadeCancel(`task:${task.id}`);
5611
+ }
5612
+ }
5613
+ return count;
5614
+ }
5441
5615
  /** Remove completed tasks older than maxAgeMs */
5442
5616
  cleanup(maxAgeMs) {
5443
5617
  const now = Date.now();
@@ -5470,6 +5644,335 @@ var init_tasks = __esm({
5470
5644
  }
5471
5645
  });
5472
5646
 
5647
+ // src/auth/oauth.ts
5648
+ var oauth_exports = {};
5649
+ __export(oauth_exports, {
5650
+ buildAuthorizationUrl: () => buildAuthorizationUrl,
5651
+ decodeJwt: () => decodeJwt,
5652
+ exchangeCodeForTokens: () => exchangeCodeForTokens,
5653
+ extractAccountInfo: () => extractAccountInfo,
5654
+ generatePKCE: () => generatePKCE,
5655
+ generateState: () => generateState,
5656
+ isRemoteEnvironment: () => isRemoteEnvironment,
5657
+ openBrowser: () => openBrowser,
5658
+ refreshAccessToken: () => refreshAccessToken,
5659
+ runOAuthFlow: () => runOAuthFlow,
5660
+ startCallbackServer: () => startCallbackServer
5661
+ });
5662
+ import { randomBytes, createHash } from "crypto";
5663
+ import { createServer } from "http";
5664
+ import { exec } from "child_process";
5665
+ import { platform as platform4 } from "os";
5666
+ function generatePKCE() {
5667
+ const verifier = randomBytes(32).toString("hex");
5668
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
5669
+ return { verifier, challenge };
5670
+ }
5671
+ function generateState() {
5672
+ return randomBytes(32).toString("hex");
5673
+ }
5674
+ function buildAuthorizationUrl(challenge, state) {
5675
+ const url = new URL(AUTHORIZE_URL);
5676
+ url.searchParams.set("response_type", "code");
5677
+ url.searchParams.set("client_id", CLIENT_ID);
5678
+ url.searchParams.set("redirect_uri", REDIRECT_URI);
5679
+ url.searchParams.set("scope", SCOPES);
5680
+ url.searchParams.set("code_challenge", challenge);
5681
+ url.searchParams.set("code_challenge_method", "S256");
5682
+ url.searchParams.set("state", state);
5683
+ url.searchParams.set("id_token_add_organizations", "true");
5684
+ url.searchParams.set("originator", "pi");
5685
+ return url;
5686
+ }
5687
+ function startCallbackServer(expectedState, timeoutMs = 6e4) {
5688
+ return new Promise((resolve4, reject) => {
5689
+ const server = createServer((req, res) => {
5690
+ const url = new URL(req.url || "/", `http://localhost:${CALLBACK_PORT}`);
5691
+ if (url.pathname === "/auth/callback") {
5692
+ const code = url.searchParams.get("code");
5693
+ const returnedState = url.searchParams.get("state");
5694
+ if (returnedState !== expectedState) {
5695
+ res.writeHead(400, { "Content-Type": "text/html" });
5696
+ res.end("<html><body><h2>State mismatch \u2014 possible CSRF attack.</h2></body></html>");
5697
+ cleanup();
5698
+ reject(new Error("OAuth state mismatch \u2014 possible CSRF attack"));
5699
+ return;
5700
+ }
5701
+ if (!code) {
5702
+ res.writeHead(400, { "Content-Type": "text/html" });
5703
+ res.end("<html><body><h2>No authorization code received.</h2></body></html>");
5704
+ cleanup();
5705
+ reject(new Error("No authorization code in callback"));
5706
+ return;
5707
+ }
5708
+ res.writeHead(200, { "Content-Type": "text/html" });
5709
+ res.end("<html><body><h2>Authenticated! You can close this tab.</h2></body></html>");
5710
+ cleanup();
5711
+ resolve4(code);
5712
+ }
5713
+ });
5714
+ const timeout = setTimeout(() => {
5715
+ cleanup();
5716
+ reject(new Error("OAuth callback timed out \u2014 no response within 60 seconds"));
5717
+ }, timeoutMs);
5718
+ function cleanup() {
5719
+ clearTimeout(timeout);
5720
+ server.close();
5721
+ }
5722
+ server.on("error", (err) => {
5723
+ clearTimeout(timeout);
5724
+ if (err.code === "EADDRINUSE") {
5725
+ reject(new Error(`Port ${CALLBACK_PORT} is in use. Close whatever is using it and try again.`));
5726
+ } else {
5727
+ reject(err);
5728
+ }
5729
+ });
5730
+ server.listen(CALLBACK_PORT, "127.0.0.1");
5731
+ });
5732
+ }
5733
+ async function exchangeCodeForTokens(code, verifier) {
5734
+ const res = await fetch(TOKEN_URL, {
5735
+ method: "POST",
5736
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5737
+ body: new URLSearchParams({
5738
+ grant_type: "authorization_code",
5739
+ client_id: CLIENT_ID,
5740
+ code,
5741
+ code_verifier: verifier,
5742
+ redirect_uri: REDIRECT_URI
5743
+ })
5744
+ });
5745
+ if (!res.ok) {
5746
+ const text = await res.text().catch(() => "Unknown error");
5747
+ throw new Error(`Token exchange failed (${res.status}): ${text}`);
5748
+ }
5749
+ return await res.json();
5750
+ }
5751
+ async function refreshAccessToken(refreshToken) {
5752
+ const res = await fetch(TOKEN_URL, {
5753
+ method: "POST",
5754
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5755
+ body: new URLSearchParams({
5756
+ grant_type: "refresh_token",
5757
+ refresh_token: refreshToken,
5758
+ client_id: CLIENT_ID
5759
+ })
5760
+ });
5761
+ if (!res.ok) {
5762
+ const text = await res.text().catch(() => "Unknown error");
5763
+ throw new Error(`Token refresh failed (${res.status}): ${text}`);
5764
+ }
5765
+ return await res.json();
5766
+ }
5767
+ function decodeJwt(token) {
5768
+ const parts = token.split(".");
5769
+ if (parts.length !== 3) throw new Error("Invalid JWT format");
5770
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString());
5771
+ }
5772
+ function extractAccountInfo(accessToken) {
5773
+ const claims = decodeJwt(accessToken);
5774
+ const authClaims = claims["https://api.openai.com/auth"];
5775
+ return {
5776
+ accountId: authClaims?.chatgpt_account_id || "",
5777
+ email: claims.email || ""
5778
+ };
5779
+ }
5780
+ function isRemoteEnvironment() {
5781
+ return !!(process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.REMOTE_CONTAINERS || process.env.CODESPACES || platform4() === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY);
5782
+ }
5783
+ function openBrowser(url) {
5784
+ const cmd = platform4() === "darwin" ? "open" : platform4() === "win32" ? "start" : "xdg-open";
5785
+ exec(`${cmd} "${url}"`, () => {
5786
+ });
5787
+ }
5788
+ async function runOAuthFlow(opts) {
5789
+ const { verifier, challenge } = generatePKCE();
5790
+ const state = generateState();
5791
+ const authUrl = buildAuthorizationUrl(challenge, state);
5792
+ opts?.onProgress?.("Starting OAuth flow...");
5793
+ const codePromise = startCallbackServer(state);
5794
+ if (isRemoteEnvironment()) {
5795
+ opts?.onProgress?.("Remote environment detected \u2014 paste this URL in your browser:");
5796
+ opts?.onUrl?.(authUrl.toString());
5797
+ } else {
5798
+ opts?.onProgress?.("Opening browser for OpenAI login...");
5799
+ openBrowser(authUrl.toString());
5800
+ opts?.onUrl?.(authUrl.toString());
5801
+ }
5802
+ const code = await codePromise;
5803
+ opts?.onProgress?.("Authorization code received, exchanging for tokens...");
5804
+ const tokens = await exchangeCodeForTokens(code, verifier);
5805
+ opts?.onProgress?.("Tokens received, extracting account info...");
5806
+ const { accountId, email } = extractAccountInfo(tokens.access_token);
5807
+ return {
5808
+ type: "oauth",
5809
+ provider: "openai-codex",
5810
+ access: tokens.access_token,
5811
+ refresh: tokens.refresh_token,
5812
+ expires: Date.now() + tokens.expires_in * 1e3,
5813
+ accountId,
5814
+ email,
5815
+ clientId: CLIENT_ID
5816
+ };
5817
+ }
5818
+ var AUTHORIZE_URL, TOKEN_URL, REDIRECT_URI, CLIENT_ID, SCOPES, CALLBACK_PORT;
5819
+ var init_oauth = __esm({
5820
+ "src/auth/oauth.ts"() {
5821
+ "use strict";
5822
+ init_esm_shims();
5823
+ AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
5824
+ TOKEN_URL = "https://auth.openai.com/oauth/token";
5825
+ REDIRECT_URI = "http://localhost:1455/auth/callback";
5826
+ CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
5827
+ SCOPES = "openid profile email offline_access";
5828
+ CALLBACK_PORT = 1455;
5829
+ }
5830
+ });
5831
+
5832
+ // src/auth/credentials.ts
5833
+ var credentials_exports = {};
5834
+ __export(credentials_exports, {
5835
+ AuthProfileStore: () => AuthProfileStore,
5836
+ needsRefresh: () => needsRefresh
5837
+ });
5838
+ import { readFile as readFile10, writeFile as writeFile8, unlink as unlink3, stat as stat6 } from "fs/promises";
5839
+ import { existsSync as existsSync8, writeFileSync } from "fs";
5840
+ import { join as join13 } from "path";
5841
+ function needsRefresh(credential) {
5842
+ return Date.now() >= credential.expires - 5 * 60 * 1e3;
5843
+ }
5844
+ var AuthProfileStore;
5845
+ var init_credentials = __esm({
5846
+ "src/auth/credentials.ts"() {
5847
+ "use strict";
5848
+ init_esm_shims();
5849
+ init_config2();
5850
+ init_oauth();
5851
+ AuthProfileStore = class {
5852
+ filePath;
5853
+ lockPath;
5854
+ constructor() {
5855
+ const configDir = getConfigDir();
5856
+ this.filePath = join13(configDir, "auth-profiles.json");
5857
+ this.lockPath = join13(configDir, ".auth-lock");
5858
+ }
5859
+ /** Load profiles from disk */
5860
+ async load() {
5861
+ try {
5862
+ const data = await readFile10(this.filePath, "utf-8");
5863
+ return JSON.parse(data);
5864
+ } catch {
5865
+ return { profiles: {} };
5866
+ }
5867
+ }
5868
+ /** Save profiles to disk with restricted permissions */
5869
+ async save(profiles) {
5870
+ await writeFile8(this.filePath, JSON.stringify(profiles, null, 2), { mode: 384 });
5871
+ }
5872
+ /** Get a specific credential by profile ID */
5873
+ async getCredential(profileId) {
5874
+ const profiles = await this.load();
5875
+ return profiles.profiles[profileId];
5876
+ }
5877
+ /** Store a credential */
5878
+ async setCredential(profileId, credential) {
5879
+ const profiles = await this.load();
5880
+ profiles.profiles[profileId] = credential;
5881
+ await this.save(profiles);
5882
+ }
5883
+ /** Remove a credential */
5884
+ async removeCredential(profileId) {
5885
+ const profiles = await this.load();
5886
+ delete profiles.profiles[profileId];
5887
+ await this.save(profiles);
5888
+ }
5889
+ /** List all profile IDs */
5890
+ async listProfiles() {
5891
+ const profiles = await this.load();
5892
+ return Object.entries(profiles.profiles).map(([id, cred]) => ({
5893
+ id,
5894
+ provider: cred.provider,
5895
+ type: cred.type,
5896
+ email: cred.type === "oauth" ? cred.email : void 0
5897
+ }));
5898
+ }
5899
+ /**
5900
+ * Resolve an API key or access token for a profile.
5901
+ * For OAuth: checks expiry, refreshes if needed, returns access token.
5902
+ * For API key: returns the key directly.
5903
+ */
5904
+ async resolveApiKey(profileId) {
5905
+ const credential = await this.getCredential(profileId);
5906
+ if (!credential) {
5907
+ throw new Error(`No credential found for profile "${profileId}". Run 'clank auth login' first.`);
5908
+ }
5909
+ if (credential.type === "api_key") {
5910
+ return credential.key;
5911
+ }
5912
+ if (needsRefresh(credential)) {
5913
+ const refreshed = await this.refreshWithLock(credential, profileId);
5914
+ return refreshed.access;
5915
+ }
5916
+ return credential.access;
5917
+ }
5918
+ /**
5919
+ * Refresh an OAuth token with file-based locking.
5920
+ * Prevents multiple processes from refreshing simultaneously.
5921
+ */
5922
+ async refreshWithLock(credential, profileId) {
5923
+ if (existsSync8(this.lockPath)) {
5924
+ try {
5925
+ const lockStat = await stat6(this.lockPath);
5926
+ if (Date.now() - lockStat.mtimeMs < 3e4) {
5927
+ await new Promise((r) => setTimeout(r, 2e3));
5928
+ const fresh = await this.getCredential(profileId);
5929
+ if (fresh && fresh.type === "oauth" && !needsRefresh(fresh)) {
5930
+ return fresh;
5931
+ }
5932
+ }
5933
+ } catch {
5934
+ }
5935
+ }
5936
+ try {
5937
+ writeFileSync(this.lockPath, String(process.pid), { flag: "wx" });
5938
+ } catch {
5939
+ await new Promise((r) => setTimeout(r, 2e3));
5940
+ const fresh = await this.getCredential(profileId);
5941
+ if (fresh && fresh.type === "oauth" && !needsRefresh(fresh)) {
5942
+ return fresh;
5943
+ }
5944
+ }
5945
+ try {
5946
+ const tokens = await refreshAccessToken(credential.refresh);
5947
+ const refreshed = {
5948
+ ...credential,
5949
+ access: tokens.access_token,
5950
+ refresh: tokens.refresh_token,
5951
+ expires: Date.now() + tokens.expires_in * 1e3
5952
+ };
5953
+ await this.setCredential(profileId, refreshed);
5954
+ return refreshed;
5955
+ } finally {
5956
+ try {
5957
+ await unlink3(this.lockPath);
5958
+ } catch {
5959
+ }
5960
+ }
5961
+ }
5962
+ };
5963
+ }
5964
+ });
5965
+
5966
+ // src/auth/index.ts
5967
+ var init_auth = __esm({
5968
+ "src/auth/index.ts"() {
5969
+ "use strict";
5970
+ init_esm_shims();
5971
+ init_oauth();
5972
+ init_credentials();
5973
+ }
5974
+ });
5975
+
5473
5976
  // src/routing/resolve-route.ts
5474
5977
  function resolveRoute(context, bindings, agents2, defaultAgentId) {
5475
5978
  const scored = bindings.map((b) => ({
@@ -5885,10 +6388,10 @@ Describe or analyze the image if you can, or acknowledge it.`
5885
6388
  return;
5886
6389
  }
5887
6390
  const { writeFile: wf } = await import("fs/promises");
5888
- const { join: join20 } = await import("path");
6391
+ const { join: join21 } = await import("path");
5889
6392
  const { tmpdir } = await import("os");
5890
6393
  const safeName = (doc.file_name || "file").replace(/[^a-zA-Z0-9._-]/g, "_");
5891
- const savePath = join20(tmpdir(), `clank-upload-${Date.now()}-${safeName}`);
6394
+ const savePath = join21(tmpdir(), `clank-upload-${Date.now()}-${safeName}`);
5892
6395
  await wf(savePath, Buffer.from(await res.arrayBuffer()));
5893
6396
  const caption = msg.caption || "";
5894
6397
  const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
@@ -5997,7 +6500,9 @@ You can read this file with the read_file tool.`
5997
6500
  if (tasks.length === 0) return "No background tasks.";
5998
6501
  return "*Background Tasks:*\n" + tasks.map((t) => {
5999
6502
  const elapsed = Math.round(((t.completedAt || Date.now()) - t.startedAt) / 1e3);
6000
- return `\u2022 *${t.label.slice(0, 40)}* (${t.agentId}) \u2014 ${t.status} (${elapsed}s)`;
6503
+ const depth = t.spawnDepth > 0 ? ` [depth ${t.spawnDepth}]` : "";
6504
+ const kids = t.children.length > 0 ? ` (${t.children.length} children)` : "";
6505
+ return `\u2022 *${t.label.slice(0, 40)}* (${t.agentId})${depth}${kids} \u2014 ${t.status} (${elapsed}s)`;
6001
6506
  }).join("\n");
6002
6507
  }
6003
6508
  case "think":
@@ -6167,9 +6672,9 @@ var init_web = __esm({
6167
6672
  });
6168
6673
 
6169
6674
  // src/plugins/loader.ts
6170
- import { readdir as readdir6, readFile as readFile10 } from "fs/promises";
6171
- import { existsSync as existsSync8 } from "fs";
6172
- import { join as join13 } from "path";
6675
+ import { readdir as readdir6, readFile as readFile11 } from "fs/promises";
6676
+ import { existsSync as existsSync9 } from "fs";
6677
+ import { join as join14 } from "path";
6173
6678
  import { homedir as homedir2 } from "os";
6174
6679
  var PluginLoader;
6175
6680
  var init_loader = __esm({
@@ -6202,25 +6707,25 @@ var init_loader = __esm({
6202
6707
  /** Discover plugin directories */
6203
6708
  async discoverPlugins() {
6204
6709
  const dirs = [];
6205
- const userPluginDir = join13(homedir2(), ".clank", "plugins");
6206
- if (existsSync8(userPluginDir)) {
6710
+ const userPluginDir = join14(homedir2(), ".clank", "plugins");
6711
+ if (existsSync9(userPluginDir)) {
6207
6712
  try {
6208
6713
  const entries = await readdir6(userPluginDir, { withFileTypes: true });
6209
6714
  for (const entry of entries) {
6210
6715
  if (entry.isDirectory()) {
6211
- dirs.push(join13(userPluginDir, entry.name));
6716
+ dirs.push(join14(userPluginDir, entry.name));
6212
6717
  }
6213
6718
  }
6214
6719
  } catch {
6215
6720
  }
6216
6721
  }
6217
- const nodeModulesDir = join13(process.cwd(), "node_modules");
6218
- if (existsSync8(nodeModulesDir)) {
6722
+ const nodeModulesDir = join14(process.cwd(), "node_modules");
6723
+ if (existsSync9(nodeModulesDir)) {
6219
6724
  try {
6220
6725
  const entries = await readdir6(nodeModulesDir);
6221
6726
  for (const entry of entries) {
6222
6727
  if (entry.startsWith("clank-plugin-")) {
6223
- dirs.push(join13(nodeModulesDir, entry));
6728
+ dirs.push(join14(nodeModulesDir, entry));
6224
6729
  }
6225
6730
  }
6226
6731
  } catch {
@@ -6230,9 +6735,9 @@ var init_loader = __esm({
6230
6735
  }
6231
6736
  /** Load a single plugin from a directory */
6232
6737
  async loadPlugin(dir) {
6233
- const manifestPath = join13(dir, "clank-plugin.json");
6234
- if (!existsSync8(manifestPath)) return null;
6235
- const raw = await readFile10(manifestPath, "utf-8");
6738
+ const manifestPath = join14(dir, "clank-plugin.json");
6739
+ if (!existsSync9(manifestPath)) return null;
6740
+ const raw = await readFile11(manifestPath, "utf-8");
6236
6741
  const manifest = JSON.parse(raw);
6237
6742
  if (!manifest.name) return null;
6238
6743
  const plugin = {
@@ -6244,7 +6749,7 @@ var init_loader = __esm({
6244
6749
  if (manifest.tools) {
6245
6750
  for (const toolEntry of manifest.tools) {
6246
6751
  try {
6247
- const entrypoint = join13(dir, toolEntry.entrypoint);
6752
+ const entrypoint = join14(dir, toolEntry.entrypoint);
6248
6753
  const mod = await import(entrypoint);
6249
6754
  const tool = mod.default || mod.tool;
6250
6755
  if (tool) {
@@ -6258,7 +6763,7 @@ var init_loader = __esm({
6258
6763
  if (manifest.hooks) {
6259
6764
  for (const hookEntry of manifest.hooks) {
6260
6765
  try {
6261
- const handlerPath = join13(dir, hookEntry.handler);
6766
+ const handlerPath = join14(dir, hookEntry.handler);
6262
6767
  const mod = await import(handlerPath);
6263
6768
  const handler = mod.default || mod.handler;
6264
6769
  if (handler) {
@@ -6314,10 +6819,10 @@ var init_plugins = __esm({
6314
6819
  });
6315
6820
 
6316
6821
  // src/gateway/server.ts
6317
- import { createServer } from "http";
6822
+ import { createServer as createServer2 } from "http";
6318
6823
  import { WebSocketServer, WebSocket } from "ws";
6319
- import { readFile as readFile11 } from "fs/promises";
6320
- import { join as join14, dirname as dirname2 } from "path";
6824
+ import { readFile as readFile12 } from "fs/promises";
6825
+ import { join as join15, dirname as dirname2 } from "path";
6321
6826
  import { fileURLToPath as fileURLToPath2 } from "url";
6322
6827
  var GatewayServer;
6323
6828
  var init_server = __esm({
@@ -6332,6 +6837,7 @@ var init_server = __esm({
6332
6837
  init_config2();
6333
6838
  init_cron();
6334
6839
  init_tasks();
6840
+ init_auth();
6335
6841
  init_routing();
6336
6842
  init_telegram();
6337
6843
  init_discord();
@@ -6351,6 +6857,7 @@ var init_server = __esm({
6351
6857
  configWatcher;
6352
6858
  pluginLoader;
6353
6859
  taskRegistry;
6860
+ authProfileStore;
6354
6861
  adapters = [];
6355
6862
  running = false;
6356
6863
  /** Rate limiting: track message timestamps per session */
@@ -6361,13 +6868,14 @@ var init_server = __esm({
6361
6868
  // max 20 messages per minute per session
6362
6869
  constructor(config) {
6363
6870
  this.config = config;
6364
- this.sessionStore = new SessionStore(join14(getConfigDir(), "conversations"));
6871
+ this.sessionStore = new SessionStore(join15(getConfigDir(), "conversations"));
6365
6872
  this.toolRegistry = createFullRegistry();
6366
- this.memoryManager = new MemoryManager(join14(getConfigDir(), "memory"));
6367
- this.cronScheduler = new CronScheduler(join14(getConfigDir(), "cron"));
6873
+ this.memoryManager = new MemoryManager(join15(getConfigDir(), "memory"));
6874
+ this.cronScheduler = new CronScheduler(join15(getConfigDir(), "cron"));
6368
6875
  this.configWatcher = new ConfigWatcher();
6369
6876
  this.pluginLoader = new PluginLoader();
6370
6877
  this.taskRegistry = new TaskRegistry();
6878
+ this.authProfileStore = new AuthProfileStore();
6371
6879
  }
6372
6880
  /** Get the task registry (for adapters to list tasks) */
6373
6881
  getTaskRegistry() {
@@ -6379,8 +6887,8 @@ var init_server = __esm({
6379
6887
  console.error(` Unhandled rejection: ${err instanceof Error ? err.message : err}`);
6380
6888
  });
6381
6889
  if (this.config.gateway.auth.mode === "token" && !this.config.gateway.auth.token) {
6382
- const { randomBytes: randomBytes2 } = await import("crypto");
6383
- this.config.gateway.auth.token = randomBytes2(16).toString("hex");
6890
+ const { randomBytes: randomBytes3 } = await import("crypto");
6891
+ this.config.gateway.auth.token = randomBytes3(16).toString("hex");
6384
6892
  console.log(` Generated auth token: ${this.config.gateway.auth.token.slice(0, 8)}...`);
6385
6893
  }
6386
6894
  await this.sessionStore.init();
@@ -6408,7 +6916,7 @@ var init_server = __esm({
6408
6916
  await this.startAdapters();
6409
6917
  const port = this.config.gateway.port || DEFAULT_PORT;
6410
6918
  const bind = this.config.gateway.bind === "loopback" ? "127.0.0.1" : "0.0.0.0";
6411
- this.httpServer = createServer((req, res) => this.handleHttp(req, res));
6919
+ this.httpServer = createServer2((req, res) => this.handleHttp(req, res));
6412
6920
  this.wss = new WebSocketServer({ server: this.httpServer });
6413
6921
  this.wss.on("connection", (ws) => this.handleConnection(ws));
6414
6922
  return new Promise((resolve4, reject) => {
@@ -6571,7 +7079,7 @@ var init_server = __esm({
6571
7079
  res.writeHead(200, { "Content-Type": "application/json" });
6572
7080
  res.end(JSON.stringify({
6573
7081
  status: "ok",
6574
- version: "1.6.0",
7082
+ version: "1.7.0",
6575
7083
  uptime: process.uptime(),
6576
7084
  clients: this.clients.size,
6577
7085
  agents: this.engines.size
@@ -6601,14 +7109,14 @@ var init_server = __esm({
6601
7109
  if (url === "/chat" || url === "/") {
6602
7110
  try {
6603
7111
  const __dirname4 = dirname2(fileURLToPath2(import.meta.url));
6604
- const htmlPath = join14(__dirname4, "..", "web", "index.html");
6605
- const html = await readFile11(htmlPath, "utf-8");
7112
+ const htmlPath = join15(__dirname4, "..", "web", "index.html");
7113
+ const html = await readFile12(htmlPath, "utf-8");
6606
7114
  res.writeHead(200, { "Content-Type": "text/html" });
6607
7115
  res.end(html);
6608
7116
  return;
6609
7117
  } catch {
6610
7118
  try {
6611
- const html = await readFile11(join14(process.cwd(), "src", "web", "index.html"), "utf-8");
7119
+ const html = await readFile12(join15(process.cwd(), "src", "web", "index.html"), "utf-8");
6612
7120
  res.writeHead(200, { "Content-Type": "text/html" });
6613
7121
  res.end(html);
6614
7122
  return;
@@ -6683,7 +7191,7 @@ var init_server = __esm({
6683
7191
  const hello = {
6684
7192
  type: "hello",
6685
7193
  protocol: PROTOCOL_VERSION,
6686
- version: "1.6.0",
7194
+ version: "1.7.0",
6687
7195
  agents: this.config.agents.list.map((a) => ({
6688
7196
  id: a.id,
6689
7197
  name: a.name || a.id,
@@ -6784,10 +7292,30 @@ var init_server = __esm({
6784
7292
  result: task.result,
6785
7293
  error: task.error,
6786
7294
  startedAt: task.startedAt,
6787
- completedAt: task.completedAt
7295
+ completedAt: task.completedAt,
7296
+ spawnDepth: task.spawnDepth,
7297
+ children: task.children.length
6788
7298
  } : null);
6789
7299
  break;
6790
7300
  }
7301
+ case "task.kill": {
7302
+ const killId = frame.params?.taskId;
7303
+ const killTask = this.taskRegistry.get(killId);
7304
+ if (!killTask) {
7305
+ this.sendResponse(client, frame.id, false, void 0, "Task not found");
7306
+ break;
7307
+ }
7308
+ const subEng = this.engines.get(`task:${killId}`);
7309
+ if (subEng) {
7310
+ subEng.cancel();
7311
+ subEng.destroy();
7312
+ this.engines.delete(`task:${killId}`);
7313
+ }
7314
+ this.taskRegistry.cancel(killId);
7315
+ const cascaded = this.taskRegistry.cascadeCancel(`task:${killId}`);
7316
+ this.sendResponse(client, frame.id, true, { killed: killId, cascadeKilled: cascaded });
7317
+ break;
7318
+ }
6791
7319
  // === Config ===
6792
7320
  case "config.get": {
6793
7321
  const { redactConfig: redactConfig2 } = await Promise.resolve().then(() => (init_redact(), redact_exports));
@@ -6930,6 +7458,15 @@ var init_server = __esm({
6930
7458
  if (engine) return engine;
6931
7459
  const agentConfig = this.config.agents.list.find((a) => a.id === agentId);
6932
7460
  const modelConfig = agentConfig?.model || this.config.agents.defaults.model;
7461
+ const allModels = [modelConfig.primary, ...modelConfig.fallbacks || []];
7462
+ if (allModels.some((m) => m.startsWith("codex/"))) {
7463
+ try {
7464
+ const token = await this.authProfileStore.resolveApiKey("openai-codex:default");
7465
+ this.config.models.providers.codex = { apiKey: token };
7466
+ } catch (err) {
7467
+ console.error(` Codex OAuth: ${err instanceof Error ? err.message : err}`);
7468
+ }
7469
+ }
6933
7470
  const resolved = await resolveWithFallback(
6934
7471
  modelConfig.primary,
6935
7472
  modelConfig.fallbacks || [],
@@ -6956,8 +7493,15 @@ var init_server = __esm({
6956
7493
  const memoryBudget = resolved.isLocal ? 1500 : 4e3;
6957
7494
  const memoryBlock = await this.memoryManager.buildMemoryBlock("", identity.workspace, memoryBudget);
6958
7495
  const fullPrompt = memoryBlock ? systemPrompt + "\n\n---\n\n" + memoryBlock : systemPrompt;
6959
- const isMainAgent = !sessionKey.startsWith("task:") && (agentId === (this.config.agents.list[0]?.id || "default") || agentId === "default");
6960
- const spawnTaskFn = isMainAgent ? async (opts) => {
7496
+ const currentDepth = sessionKey.startsWith("task:") ? (this.taskRegistry.getBySessionKey(sessionKey)?.spawnDepth ?? 0) + 1 : 0;
7497
+ const maxSpawnDepth = this.config.agents.defaults.subagents?.maxSpawnDepth ?? 1;
7498
+ const maxConcurrent = this.config.agents.defaults.subagents?.maxConcurrent ?? 8;
7499
+ const canSpawn = currentDepth < maxSpawnDepth;
7500
+ const spawnTaskFn = canSpawn ? async (opts) => {
7501
+ const active = this.taskRegistry.countActiveByParent(sessionKey);
7502
+ if (active >= maxConcurrent) {
7503
+ throw new Error(`Concurrent task limit reached (${active}/${maxConcurrent}). Kill a task first.`);
7504
+ }
6961
7505
  const subAgentConfig = this.config.agents.list.find((a) => a.id === opts.agentId);
6962
7506
  const subModel = subAgentConfig?.model?.primary || this.config.agents.defaults.model.primary;
6963
7507
  const task = this.taskRegistry.create({
@@ -6966,7 +7510,9 @@ var init_server = __esm({
6966
7510
  prompt: opts.prompt,
6967
7511
  label: opts.label,
6968
7512
  timeoutMs: opts.timeoutMs,
6969
- spawnedBy: sessionKey
7513
+ spawnedBy: sessionKey,
7514
+ spawnDepth: currentDepth,
7515
+ parentSessionKey: sessionKey
6970
7516
  });
6971
7517
  const subSessionKey = `task:${task.id}`;
6972
7518
  const subEngine = await this.getOrCreateEngine(subSessionKey, opts.agentId, "task");
@@ -6993,16 +7539,59 @@ var init_server = __esm({
6993
7539
  });
6994
7540
  return task.id;
6995
7541
  } : void 0;
7542
+ const killTaskFn = async (taskId) => {
7543
+ const task = this.taskRegistry.get(taskId);
7544
+ if (!task) return { status: "not_found" };
7545
+ if (task.spawnedBy !== sessionKey) return { status: "not_owner" };
7546
+ if (task.status !== "running") return { status: "already_done" };
7547
+ const subEngine = this.engines.get(`task:${taskId}`);
7548
+ if (subEngine) {
7549
+ subEngine.cancel();
7550
+ subEngine.destroy();
7551
+ this.engines.delete(`task:${taskId}`);
7552
+ }
7553
+ this.taskRegistry.cancel(taskId);
7554
+ const cascadeKilled = this.taskRegistry.cascadeCancel(`task:${taskId}`);
7555
+ return { status: "ok", cascadeKilled };
7556
+ };
7557
+ const messageTaskFn = async (taskId, message) => {
7558
+ const task = this.taskRegistry.get(taskId);
7559
+ if (!task) return { status: "not_found" };
7560
+ if (task.spawnedBy !== sessionKey) return { status: "not_owner" };
7561
+ if (task.status !== "running") return { status: "not_running" };
7562
+ const subEngine = this.engines.get(`task:${taskId}`);
7563
+ if (!subEngine) return { status: "not_found" };
7564
+ try {
7565
+ const reply = await subEngine.sendMessage(message);
7566
+ return { status: "ok", replyText: reply };
7567
+ } catch (err) {
7568
+ return { status: "error", replyText: err instanceof Error ? err.message : String(err) };
7569
+ }
7570
+ };
7571
+ let finalPrompt = fullPrompt;
7572
+ if (currentDepth > 0) {
7573
+ const depthNote = currentDepth < maxSpawnDepth ? "You can spawn further sub-agents if needed." : "You cannot spawn further sub-agents (depth limit reached).";
7574
+ finalPrompt = `[Sub-Agent] You were spawned by a parent agent to complete a specific task.
7575
+ ${depthNote}
7576
+
7577
+ ---
7578
+
7579
+ ${fullPrompt}`;
7580
+ }
6996
7581
  engine = new AgentEngine({
6997
7582
  identity,
6998
7583
  toolRegistry: this.toolRegistry,
6999
7584
  sessionStore: this.sessionStore,
7000
7585
  provider: resolved,
7001
7586
  autoApprove: this.config.tools.autoApprove,
7002
- systemPrompt: fullPrompt,
7587
+ systemPrompt: finalPrompt,
7003
7588
  taskRegistry: this.taskRegistry,
7004
7589
  spawnTask: spawnTaskFn,
7005
- sessionKey
7590
+ killTask: killTaskFn,
7591
+ messageTask: messageTaskFn,
7592
+ sessionKey,
7593
+ spawnDepth: currentDepth,
7594
+ maxSpawnDepth
7006
7595
  });
7007
7596
  await engine.loadSession(sessionKey, channel);
7008
7597
  this.engines.set(sessionKey, engine);
@@ -7110,9 +7699,9 @@ __export(gateway_cmd_exports, {
7110
7699
  gatewayStop: () => gatewayStop,
7111
7700
  isGatewayRunning: () => isGatewayRunning
7112
7701
  });
7113
- import { writeFile as writeFile8, readFile as readFile12, unlink as unlink3 } from "fs/promises";
7114
- import { existsSync as existsSync9 } from "fs";
7115
- import { join as join15, dirname as dirname3 } from "path";
7702
+ import { writeFile as writeFile9, readFile as readFile13, unlink as unlink4 } from "fs/promises";
7703
+ import { existsSync as existsSync10 } from "fs";
7704
+ import { join as join16, dirname as dirname3 } from "path";
7116
7705
  import { fileURLToPath as fileURLToPath3 } from "url";
7117
7706
  async function isGatewayRunning(port) {
7118
7707
  const config = await loadConfig();
@@ -7125,7 +7714,7 @@ async function isGatewayRunning(port) {
7125
7714
  }
7126
7715
  }
7127
7716
  function pidFilePath() {
7128
- return join15(getConfigDir(), "gateway.pid");
7717
+ return join16(getConfigDir(), "gateway.pid");
7129
7718
  }
7130
7719
  async function gatewayStartForeground(opts) {
7131
7720
  await ensureConfigDir();
@@ -7137,12 +7726,12 @@ async function gatewayStartForeground(opts) {
7137
7726
  console.log(green2(` Gateway already running on port ${config.gateway.port}`));
7138
7727
  return;
7139
7728
  }
7140
- await writeFile8(pidFilePath(), String(process.pid), "utf-8");
7729
+ await writeFile9(pidFilePath(), String(process.pid), "utf-8");
7141
7730
  const server = new GatewayServer(config);
7142
7731
  const shutdown = async () => {
7143
7732
  console.log(dim2("\nShutting down..."));
7144
7733
  try {
7145
- await unlink3(pidFilePath());
7734
+ await unlink4(pidFilePath());
7146
7735
  } catch {
7147
7736
  }
7148
7737
  await server.stop();
@@ -7156,7 +7745,7 @@ async function gatewayStartForeground(opts) {
7156
7745
  console.log(dim2("Press Ctrl+C to stop"));
7157
7746
  } catch (err) {
7158
7747
  try {
7159
- await unlink3(pidFilePath());
7748
+ await unlink4(pidFilePath());
7160
7749
  } catch {
7161
7750
  }
7162
7751
  console.error(red2(`Failed to start gateway: ${err instanceof Error ? err.message : err}`));
@@ -7170,12 +7759,12 @@ async function gatewayStartBackground() {
7170
7759
  return true;
7171
7760
  }
7172
7761
  console.log(dim2(" Starting gateway in background..."));
7173
- const entryPoint = join15(dirname3(__filename2), "index.js");
7762
+ const entryPoint = join16(dirname3(__filename2), "index.js");
7174
7763
  const { mkdir: mkdir7 } = await import("fs/promises");
7175
7764
  const { spawn } = await import("child_process");
7176
7765
  const { openSync } = await import("fs");
7177
- await mkdir7(join15(getConfigDir(), "logs"), { recursive: true });
7178
- const logFile = join15(getConfigDir(), "logs", "gateway.log");
7766
+ await mkdir7(join16(getConfigDir(), "logs"), { recursive: true });
7767
+ const logFile = join16(getConfigDir(), "logs", "gateway.log");
7179
7768
  const logFd = openSync(logFile, "a");
7180
7769
  const child = spawn(process.execPath, [entryPoint, "gateway", "start", "--foreground"], {
7181
7770
  detached: true,
@@ -7205,16 +7794,16 @@ async function gatewayStart(opts) {
7205
7794
  }
7206
7795
  async function gatewayStop() {
7207
7796
  const pidPath = pidFilePath();
7208
- if (existsSync9(pidPath)) {
7797
+ if (existsSync10(pidPath)) {
7209
7798
  try {
7210
- const pid = parseInt(await readFile12(pidPath, "utf-8"), 10);
7799
+ const pid = parseInt(await readFile13(pidPath, "utf-8"), 10);
7211
7800
  process.kill(pid, "SIGTERM");
7212
- await unlink3(pidPath);
7801
+ await unlink4(pidPath);
7213
7802
  console.log(green2("Gateway stopped"));
7214
7803
  return;
7215
7804
  } catch {
7216
7805
  try {
7217
- await unlink3(pidPath);
7806
+ await unlink4(pidPath);
7218
7807
  } catch {
7219
7808
  }
7220
7809
  }
@@ -7238,8 +7827,8 @@ async function gatewayStatus() {
7238
7827
  console.log(dim2(` Clients: ${data.clients?.length || 0}`));
7239
7828
  console.log(dim2(` Sessions: ${data.sessions?.length || 0}`));
7240
7829
  const pidPath = pidFilePath();
7241
- if (existsSync9(pidPath)) {
7242
- const pid = await readFile12(pidPath, "utf-8");
7830
+ if (existsSync10(pidPath)) {
7831
+ const pid = await readFile13(pidPath, "utf-8");
7243
7832
  console.log(dim2(` PID: ${pid.trim()}`));
7244
7833
  }
7245
7834
  } else {
@@ -7267,12 +7856,12 @@ var init_gateway_cmd = __esm({
7267
7856
  });
7268
7857
 
7269
7858
  // src/daemon/install.ts
7270
- import { writeFile as writeFile9, mkdir as mkdir6, unlink as unlink4 } from "fs/promises";
7271
- import { join as join16 } from "path";
7272
- import { homedir as homedir3, platform as platform4 } from "os";
7859
+ import { writeFile as writeFile10, mkdir as mkdir6, unlink as unlink5 } from "fs/promises";
7860
+ import { join as join17 } from "path";
7861
+ import { homedir as homedir3, platform as platform5 } from "os";
7273
7862
  import { execSync } from "child_process";
7274
7863
  async function installDaemon() {
7275
- const os = platform4();
7864
+ const os = platform5();
7276
7865
  switch (os) {
7277
7866
  case "darwin":
7278
7867
  await installLaunchd();
@@ -7288,7 +7877,7 @@ async function installDaemon() {
7288
7877
  }
7289
7878
  }
7290
7879
  async function uninstallDaemon() {
7291
- const os = platform4();
7880
+ const os = platform5();
7292
7881
  switch (os) {
7293
7882
  case "darwin":
7294
7883
  await uninstallLaunchd();
@@ -7304,7 +7893,7 @@ async function uninstallDaemon() {
7304
7893
  }
7305
7894
  }
7306
7895
  async function daemonStatus() {
7307
- const os = platform4();
7896
+ const os = platform5();
7308
7897
  switch (os) {
7309
7898
  case "darwin":
7310
7899
  try {
@@ -7336,8 +7925,8 @@ async function daemonStatus() {
7336
7925
  }
7337
7926
  }
7338
7927
  async function installLaunchd() {
7339
- const plistDir = join16(homedir3(), "Library", "LaunchAgents");
7340
- const plistPath = join16(plistDir, "com.clank.gateway.plist");
7928
+ const plistDir = join17(homedir3(), "Library", "LaunchAgents");
7929
+ const plistPath = join17(plistDir, "com.clank.gateway.plist");
7341
7930
  const clankPath = execSync("which clank || echo clank", { encoding: "utf-8" }).trim();
7342
7931
  await mkdir6(plistDir, { recursive: true });
7343
7932
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
@@ -7358,21 +7947,21 @@ async function installLaunchd() {
7358
7947
  <key>KeepAlive</key>
7359
7948
  <true/>
7360
7949
  <key>StandardOutPath</key>
7361
- <string>${join16(homedir3(), ".clank", "logs", "gateway.log")}</string>
7950
+ <string>${join17(homedir3(), ".clank", "logs", "gateway.log")}</string>
7362
7951
  <key>StandardErrorPath</key>
7363
- <string>${join16(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
7952
+ <string>${join17(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
7364
7953
  </dict>
7365
7954
  </plist>`;
7366
- await writeFile9(plistPath, plist, "utf-8");
7955
+ await writeFile10(plistPath, plist, "utf-8");
7367
7956
  execSync(`launchctl load "${plistPath}"`);
7368
7957
  console.log(green3("Daemon installed (launchd)"));
7369
7958
  console.log(dim3(` Plist: ${plistPath}`));
7370
7959
  }
7371
7960
  async function uninstallLaunchd() {
7372
- const plistPath = join16(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
7961
+ const plistPath = join17(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
7373
7962
  try {
7374
7963
  execSync(`launchctl unload "${plistPath}"`);
7375
- await unlink4(plistPath);
7964
+ await unlink5(plistPath);
7376
7965
  console.log(green3("Daemon uninstalled"));
7377
7966
  } catch {
7378
7967
  console.log(dim3("Daemon was not installed"));
@@ -7407,8 +7996,8 @@ async function uninstallTaskScheduler() {
7407
7996
  }
7408
7997
  }
7409
7998
  async function installSystemd() {
7410
- const unitDir = join16(homedir3(), ".config", "systemd", "user");
7411
- const unitPath = join16(unitDir, "clank-gateway.service");
7999
+ const unitDir = join17(homedir3(), ".config", "systemd", "user");
8000
+ const unitPath = join17(unitDir, "clank-gateway.service");
7412
8001
  const clankPath = execSync("which clank || echo clank", { encoding: "utf-8" }).trim();
7413
8002
  await mkdir6(unitDir, { recursive: true });
7414
8003
  const unit = `[Unit]
@@ -7423,7 +8012,7 @@ RestartSec=5
7423
8012
  [Install]
7424
8013
  WantedBy=default.target
7425
8014
  `;
7426
- await writeFile9(unitPath, unit, "utf-8");
8015
+ await writeFile10(unitPath, unit, "utf-8");
7427
8016
  execSync("systemctl --user daemon-reload");
7428
8017
  execSync("systemctl --user enable clank-gateway");
7429
8018
  execSync("systemctl --user start clank-gateway");
@@ -7434,8 +8023,8 @@ async function uninstallSystemd() {
7434
8023
  try {
7435
8024
  execSync("systemctl --user stop clank-gateway");
7436
8025
  execSync("systemctl --user disable clank-gateway");
7437
- const unitPath = join16(homedir3(), ".config", "systemd", "user", "clank-gateway.service");
7438
- await unlink4(unitPath);
8026
+ const unitPath = join17(homedir3(), ".config", "systemd", "user", "clank-gateway.service");
8027
+ await unlink5(unitPath);
7439
8028
  execSync("systemctl --user daemon-reload");
7440
8029
  console.log(green3("Daemon uninstalled"));
7441
8030
  } catch {
@@ -7474,8 +8063,8 @@ __export(setup_exports, {
7474
8063
  runSetup: () => runSetup
7475
8064
  });
7476
8065
  import { createInterface as createInterface2 } from "readline";
7477
- import { randomBytes } from "crypto";
7478
- import { dirname as dirname4, join as join17 } from "path";
8066
+ import { randomBytes as randomBytes2 } from "crypto";
8067
+ import { dirname as dirname4, join as join18 } from "path";
7479
8068
  import { fileURLToPath as fileURLToPath4 } from "url";
7480
8069
  function ask(rl, question) {
7481
8070
  return new Promise((resolve4) => rl.question(question, resolve4));
@@ -7553,7 +8142,9 @@ async function runSetup(opts) {
7553
8142
  console.log(" 2. OpenAI (GPT-4o, Codex)");
7554
8143
  console.log(" 3. Google (Gemini)");
7555
8144
  console.log(" 4. OpenRouter (many models via one key)");
7556
- console.log(" 5. Done");
8145
+ console.log(" 5. OpenCode (subscription-based, many models)");
8146
+ console.log(" 6. OpenAI Codex (ChatGPT Plus/Pro login)");
8147
+ console.log(" 7. Done");
7557
8148
  const choice = await ask(rl, cyan2(" Which provider? "));
7558
8149
  const providerSetup = async (name, defaultModel, keyName) => {
7559
8150
  const existing = providers[name]?.apiKey;
@@ -7569,6 +8160,7 @@ async function runSetup(opts) {
7569
8160
  if (key.trim()) {
7570
8161
  const entry = { apiKey: key.trim() };
7571
8162
  if (name === "openrouter") entry.baseUrl = "https://openrouter.ai/api/v1";
8163
+ else if (name === "opencode") entry.baseUrl = "https://opencode.ai/zen";
7572
8164
  config.models.providers[name] = entry;
7573
8165
  if (!fallbacks.includes(defaultModel)) fallbacks.push(defaultModel);
7574
8166
  console.log(green4(` ${name} configured`));
@@ -7597,7 +8189,31 @@ async function runSetup(opts) {
7597
8189
  }
7598
8190
  break;
7599
8191
  }
7600
- case "5":
8192
+ case "5": {
8193
+ await providerSetup("opencode", "opencode/claude-sonnet-4-6", "OpenCode");
8194
+ break;
8195
+ }
8196
+ case "6": {
8197
+ try {
8198
+ const { runOAuthFlow: runOAuthFlow2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
8199
+ const { AuthProfileStore: AuthProfileStore2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
8200
+ console.log(dim4(" Launching browser for OpenAI login..."));
8201
+ const credential = await runOAuthFlow2({
8202
+ onUrl: (url) => console.log(dim4(` If browser didn't open: ${url}`)),
8203
+ onProgress: (msg) => console.log(dim4(` ${msg}`))
8204
+ });
8205
+ const store = new AuthProfileStore2();
8206
+ await store.setCredential("openai-codex:default", credential);
8207
+ if (!fallbacks.includes("codex/codex-mini-latest")) {
8208
+ fallbacks.push("codex/codex-mini-latest");
8209
+ }
8210
+ console.log(green4(` Codex configured (${credential.email})`));
8211
+ } catch (err) {
8212
+ console.log(yellow2(` Codex OAuth failed: ${err instanceof Error ? err.message : err}`));
8213
+ }
8214
+ break;
8215
+ }
8216
+ case "7":
7601
8217
  case "":
7602
8218
  picking = false;
7603
8219
  break;
@@ -7614,14 +8230,14 @@ async function runSetup(opts) {
7614
8230
  const port = await ask(rl, cyan2(` Port [${config.gateway.port}]: `));
7615
8231
  if (port.trim()) config.gateway.port = parseInt(port, 10);
7616
8232
  }
7617
- config.gateway.auth.token = randomBytes(16).toString("hex");
8233
+ config.gateway.auth.token = randomBytes2(16).toString("hex");
7618
8234
  console.log(dim4(` Port: ${config.gateway.port}`));
7619
8235
  console.log(dim4(` Token: ${config.gateway.auth.token.slice(0, 8)}...`));
7620
8236
  console.log("");
7621
8237
  console.log(dim4(" Creating workspace..."));
7622
8238
  const { ensureWorkspaceFiles: ensureWorkspaceFiles2 } = await Promise.resolve().then(() => (init_system_prompt(), system_prompt_exports));
7623
- const templateDir = join17(__dirname2, "..", "workspace", "templates");
7624
- const wsDir = join17(getConfigDir(), "workspace");
8239
+ const templateDir = join18(__dirname2, "..", "workspace", "templates");
8240
+ const wsDir = join18(getConfigDir(), "workspace");
7625
8241
  try {
7626
8242
  await ensureWorkspaceFiles2(wsDir, templateDir);
7627
8243
  } catch {
@@ -7782,9 +8398,9 @@ var fix_exports = {};
7782
8398
  __export(fix_exports, {
7783
8399
  runFix: () => runFix
7784
8400
  });
7785
- import { existsSync as existsSync10 } from "fs";
8401
+ import { existsSync as existsSync11 } from "fs";
7786
8402
  import { readdir as readdir7 } from "fs/promises";
7787
- import { join as join18 } from "path";
8403
+ import { join as join19 } from "path";
7788
8404
  async function runFix(opts) {
7789
8405
  console.log("");
7790
8406
  console.log(" Clank Diagnostics");
@@ -7814,7 +8430,7 @@ async function runFix(opts) {
7814
8430
  }
7815
8431
  async function checkConfig() {
7816
8432
  const configPath = getConfigPath();
7817
- if (!existsSync10(configPath)) {
8433
+ if (!existsSync11(configPath)) {
7818
8434
  return {
7819
8435
  name: "Config",
7820
8436
  status: "warn",
@@ -7882,8 +8498,8 @@ async function checkModels() {
7882
8498
  return { name: "Model (primary)", status: "ok", message: modelId };
7883
8499
  }
7884
8500
  async function checkSessions() {
7885
- const sessDir = join18(getConfigDir(), "conversations");
7886
- if (!existsSync10(sessDir)) {
8501
+ const sessDir = join19(getConfigDir(), "conversations");
8502
+ if (!existsSync11(sessDir)) {
7887
8503
  return { name: "Sessions", status: "ok", message: "no sessions yet" };
7888
8504
  }
7889
8505
  try {
@@ -7895,8 +8511,8 @@ async function checkSessions() {
7895
8511
  }
7896
8512
  }
7897
8513
  async function checkWorkspace() {
7898
- const wsDir = join18(getConfigDir(), "workspace");
7899
- if (!existsSync10(wsDir)) {
8514
+ const wsDir = join19(getConfigDir(), "workspace");
8515
+ if (!existsSync11(wsDir)) {
7900
8516
  return {
7901
8517
  name: "Workspace",
7902
8518
  status: "warn",
@@ -8189,7 +8805,7 @@ async function runTui(opts) {
8189
8805
  ws.on("open", () => {
8190
8806
  ws.send(JSON.stringify({
8191
8807
  type: "connect",
8192
- params: { auth: { token }, mode: "tui", version: "1.6.0" }
8808
+ params: { auth: { token }, mode: "tui", version: "1.7.0" }
8193
8809
  }));
8194
8810
  });
8195
8811
  ws.on("message", (data) => {
@@ -8538,7 +9154,7 @@ __export(uninstall_exports, {
8538
9154
  });
8539
9155
  import { createInterface as createInterface4 } from "readline";
8540
9156
  import { rm } from "fs/promises";
8541
- import { existsSync as existsSync11 } from "fs";
9157
+ import { existsSync as existsSync12 } from "fs";
8542
9158
  async function runUninstall(opts) {
8543
9159
  const configDir = getConfigDir();
8544
9160
  console.log("");
@@ -8577,7 +9193,7 @@ async function runUninstall(opts) {
8577
9193
  } catch {
8578
9194
  }
8579
9195
  console.log(dim10(" Deleting data..."));
8580
- if (existsSync11(configDir)) {
9196
+ if (existsSync12(configDir)) {
8581
9197
  await rm(configDir, { recursive: true, force: true });
8582
9198
  console.log(green10(` Removed ${configDir}`));
8583
9199
  } else {
@@ -8615,12 +9231,12 @@ init_esm_shims();
8615
9231
  import { Command } from "commander";
8616
9232
  import { readFileSync } from "fs";
8617
9233
  import { fileURLToPath as fileURLToPath5 } from "url";
8618
- import { dirname as dirname5, join as join19 } from "path";
9234
+ import { dirname as dirname5, join as join20 } from "path";
8619
9235
  var __filename3 = fileURLToPath5(import.meta.url);
8620
9236
  var __dirname3 = dirname5(__filename3);
8621
- var version = "1.6.0";
9237
+ var version = "1.7.0";
8622
9238
  try {
8623
- const pkg = JSON.parse(readFileSync(join19(__dirname3, "..", "package.json"), "utf-8"));
9239
+ const pkg = JSON.parse(readFileSync(join20(__dirname3, "..", "package.json"), "utf-8"));
8624
9240
  version = pkg.version;
8625
9241
  } catch {
8626
9242
  }
@@ -8710,10 +9326,10 @@ program.command("dashboard").description("Open the Web UI in your browser").opti
8710
9326
  Web UI: ${url}
8711
9327
  `);
8712
9328
  if (opts.open !== false) {
8713
- const { platform: platform5 } = await import("os");
8714
- const { exec } = await import("child_process");
8715
- const cmd = platform5() === "win32" ? `start ${url}` : platform5() === "darwin" ? `open ${url}` : `xdg-open ${url}`;
8716
- exec(cmd);
9329
+ const { platform: platform6 } = await import("os");
9330
+ const { exec: exec2 } = await import("child_process");
9331
+ const cmd = platform6() === "win32" ? `start ${url}` : platform6() === "darwin" ? `open ${url}` : `xdg-open ${url}`;
9332
+ exec2(cmd);
8717
9333
  }
8718
9334
  });
8719
9335
  var pipeline = program.command("pipeline").description("Manage agent pipelines");
@@ -8729,10 +9345,10 @@ pipeline.command("status <id>").description("Check pipeline execution status").a
8729
9345
  });
8730
9346
  var cron = program.command("cron").description("Manage scheduled jobs");
8731
9347
  cron.command("list").description("List cron jobs").action(async () => {
8732
- const { join: join20 } = await import("path");
9348
+ const { join: join21 } = await import("path");
8733
9349
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8734
9350
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
8735
- const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
9351
+ const scheduler = new CronScheduler2(join21(getConfigDir3(), "cron"));
8736
9352
  await scheduler.init();
8737
9353
  const jobs = scheduler.listJobs();
8738
9354
  if (jobs.length === 0) {
@@ -8744,10 +9360,10 @@ cron.command("list").description("List cron jobs").action(async () => {
8744
9360
  }
8745
9361
  });
8746
9362
  cron.command("add").description("Add a cron job").requiredOption("--schedule <expr>", "Schedule (e.g., '1h', '30m', 'daily')").requiredOption("--prompt <text>", "What the agent should do").option("--name <name>", "Job name").option("--agent <id>", "Agent ID", "default").action(async (opts) => {
8747
- const { join: join20 } = await import("path");
9363
+ const { join: join21 } = await import("path");
8748
9364
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8749
9365
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
8750
- const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
9366
+ const scheduler = new CronScheduler2(join21(getConfigDir3(), "cron"));
8751
9367
  await scheduler.init();
8752
9368
  const job = await scheduler.addJob({
8753
9369
  name: opts.name || "CLI Job",
@@ -8758,10 +9374,10 @@ cron.command("add").description("Add a cron job").requiredOption("--schedule <ex
8758
9374
  console.log(` Job created: ${job.id.slice(0, 8)} \u2014 "${job.name}" every ${job.schedule}`);
8759
9375
  });
8760
9376
  cron.command("remove <id>").description("Remove a cron job").action(async (id) => {
8761
- const { join: join20 } = await import("path");
9377
+ const { join: join21 } = await import("path");
8762
9378
  const { getConfigDir: getConfigDir3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8763
9379
  const { CronScheduler: CronScheduler2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
8764
- const scheduler = new CronScheduler2(join20(getConfigDir3(), "cron"));
9380
+ const scheduler = new CronScheduler2(join21(getConfigDir3(), "cron"));
8765
9381
  await scheduler.init();
8766
9382
  const removed = await scheduler.removeJob(id);
8767
9383
  console.log(removed ? ` Job ${id.slice(0, 8)} removed` : ` Job not found`);
@@ -8793,5 +9409,57 @@ program.action(async () => {
8793
9409
  const { runTui: runTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
8794
9410
  await runTui2({});
8795
9411
  });
9412
+ var auth = program.command("auth").description("Manage OAuth authentication (Codex login)");
9413
+ auth.command("login").description("Sign in with your OpenAI account (ChatGPT Plus/Pro)").action(async () => {
9414
+ const { runOAuthFlow: runOAuthFlow2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
9415
+ const { AuthProfileStore: AuthProfileStore2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
9416
+ try {
9417
+ const credential = await runOAuthFlow2({
9418
+ onUrl: (url) => console.log(`
9419
+ If browser didn't open:
9420
+ ${url}
9421
+ `),
9422
+ onProgress: (msg) => console.log(` ${msg}`)
9423
+ });
9424
+ const store = new AuthProfileStore2();
9425
+ await store.setCredential("openai-codex:default", credential);
9426
+ console.log(`
9427
+ Authenticated as ${credential.email}`);
9428
+ console.log(` Account: ${credential.accountId}`);
9429
+ console.log(` Token expires: ${new Date(credential.expires).toLocaleString()}`);
9430
+ console.log(`
9431
+ Use model "codex/codex-mini-latest" in your agent config.`);
9432
+ } catch (err) {
9433
+ console.error(` OAuth failed: ${err instanceof Error ? err.message : err}`);
9434
+ process.exit(1);
9435
+ }
9436
+ });
9437
+ auth.command("status").description("Show stored authentication credentials").action(async () => {
9438
+ const { AuthProfileStore: AuthProfileStore2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
9439
+ const store = new AuthProfileStore2();
9440
+ const profiles = await store.listProfiles();
9441
+ if (profiles.length === 0) {
9442
+ console.log(" No stored credentials. Run 'clank auth login' to sign in.");
9443
+ return;
9444
+ }
9445
+ console.log(" Stored credentials:\n");
9446
+ for (const p of profiles) {
9447
+ const cred = await store.getCredential(p.id);
9448
+ const expiry = cred?.type === "oauth" ? new Date(cred.expires).toLocaleString() : "n/a";
9449
+ const expired = cred?.type === "oauth" && Date.now() >= cred.expires;
9450
+ console.log(` ${p.id}`);
9451
+ console.log(` Provider: ${p.provider}`);
9452
+ console.log(` Type: ${p.type}`);
9453
+ if (p.email) console.log(` Email: ${p.email}`);
9454
+ console.log(` Expires: ${expiry}${expired ? " (EXPIRED \u2014 will auto-refresh)" : ""}`);
9455
+ console.log("");
9456
+ }
9457
+ });
9458
+ auth.command("logout").description("Remove stored OAuth credentials").action(async () => {
9459
+ const { AuthProfileStore: AuthProfileStore2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
9460
+ const store = new AuthProfileStore2();
9461
+ await store.removeCredential("openai-codex:default");
9462
+ console.log(" Credentials removed.");
9463
+ });
8796
9464
  program.parse();
8797
9465
  //# sourceMappingURL=index.js.map