@tractorscorch/clank 1.5.10 → 1.6.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
@@ -841,6 +841,12 @@ var init_agent = __esm({
841
841
  autoApprove = { low: true, medium: false, high: false };
842
842
  /** Tools the user has approved "always" for this session */
843
843
  alwaysApproved = /* @__PURE__ */ new Set();
844
+ /** Background task registry (if available) */
845
+ taskRegistry = null;
846
+ /** Function to spawn background tasks (main agent only) */
847
+ spawnTaskFn = void 0;
848
+ /** Session key for this engine (used to consume completed tasks) */
849
+ sessionKey = "";
844
850
  constructor(opts) {
845
851
  super();
846
852
  this.setMaxListeners(30);
@@ -850,6 +856,9 @@ var init_agent = __esm({
850
856
  this.resolvedProvider = opts.provider;
851
857
  if (opts.autoApprove) this.autoApprove = opts.autoApprove;
852
858
  if (opts.systemPrompt) this.systemPrompt = opts.systemPrompt;
859
+ if (opts.taskRegistry) this.taskRegistry = opts.taskRegistry;
860
+ if (opts.spawnTask) this.spawnTaskFn = opts.spawnTask;
861
+ if (opts.sessionKey) this.sessionKey = opts.sessionKey;
853
862
  this.contextEngine = new ContextEngine({
854
863
  contextWindow: opts.provider.provider.contextWindow(),
855
864
  isLocal: opts.provider.isLocal
@@ -892,6 +901,24 @@ var init_agent = __esm({
892
901
  }
893
902
  this.abortController = new AbortController();
894
903
  const signal = this.abortController.signal;
904
+ if (this.taskRegistry && this.sessionKey) {
905
+ const completed = this.taskRegistry.consumeCompleted(this.sessionKey);
906
+ if (completed.length > 0) {
907
+ const results = completed.map((t) => {
908
+ if (t.status === "completed") {
909
+ return `[Task Complete] "${t.label}" (agent: ${t.agentId}):
910
+ ${t.result}`;
911
+ }
912
+ return `[Task ${t.status === "failed" ? "Failed" : "Timed Out"}] "${t.label}" (agent: ${t.agentId}): ${t.error || "unknown error"}`;
913
+ }).join("\n\n");
914
+ this.contextEngine.ingest({
915
+ role: "user",
916
+ content: `[System] Background tasks completed:
917
+
918
+ ${results}`
919
+ });
920
+ }
921
+ }
895
922
  this.contextEngine.ingest({ role: "user", content: text });
896
923
  if (shouldPersist(text)) {
897
924
  const memory = extractMemory(text);
@@ -1047,7 +1074,9 @@ var init_agent = __esm({
1047
1074
  allowExternal: true,
1048
1075
  autoApprove: this.autoApprove,
1049
1076
  agentId: this.identity.id,
1050
- signal
1077
+ signal,
1078
+ taskRegistry: this.taskRegistry ?? void 0,
1079
+ spawnTask: this.spawnTaskFn
1051
1080
  };
1052
1081
  const validation = tool.validate(tc.arguments, toolCtx);
1053
1082
  if (!validation.ok) {
@@ -2299,8 +2328,8 @@ var init_web_search = __esm({
2299
2328
  async execute(args, ctx) {
2300
2329
  const query = args.query;
2301
2330
  const count = Math.min(Number(args.count) || 5, 20);
2302
- const { loadConfig: loadConfig3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
2303
- const config = await loadConfig3();
2331
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
2332
+ const config = await loadConfig2();
2304
2333
  const apiKey = config.tools.webSearch?.apiKey;
2305
2334
  if (!apiKey) {
2306
2335
  return "Error: Brave Search API key not configured. Run `clank setup --section search` or tell me to set it up.";
@@ -3446,6 +3475,19 @@ function createProvider(modelId, config, opts) {
3446
3475
  const p = new GoogleProvider({ apiKey: config.google.apiKey, model });
3447
3476
  return { provider: p, providerName: "google", modelId, isLocal: false };
3448
3477
  }
3478
+ case "openrouter": {
3479
+ const orConfig = config.openrouter ?? config["openrouter"];
3480
+ if (!orConfig?.apiKey) {
3481
+ throw new Error(`OpenRouter API key required for model ${modelId}`);
3482
+ }
3483
+ const p = new OpenAIProvider({
3484
+ apiKey: orConfig.apiKey,
3485
+ baseUrl: orConfig.baseUrl || "https://openrouter.ai/api/v1",
3486
+ model,
3487
+ maxResponseTokens: opts?.maxResponseTokens
3488
+ });
3489
+ return { provider: p, providerName: "openrouter", modelId, isLocal: false };
3490
+ }
3449
3491
  case "lmstudio":
3450
3492
  case "llamacpp":
3451
3493
  case "vllm":
@@ -3585,7 +3627,7 @@ var init_model_tool = __esm({
3585
3627
  properties: {
3586
3628
  action: { type: "string", description: "'list', 'detect', 'set-default', or 'add-provider'" },
3587
3629
  model: { type: "string", description: "Model ID for set-default (e.g., 'ollama/qwen3.5')" },
3588
- provider: { type: "string", description: "Provider name for add-provider ('anthropic', 'openai', 'google')" },
3630
+ provider: { type: "string", description: "Provider name for add-provider ('anthropic', 'openai', 'google', 'openrouter')" },
3589
3631
  apiKey: { type: "string", description: "API key for add-provider" }
3590
3632
  },
3591
3633
  required: ["action"]
@@ -3633,7 +3675,11 @@ var init_model_tool = __esm({
3633
3675
  if (action === "add-provider") {
3634
3676
  if (!args.provider || !args.apiKey) return "Error: provider and apiKey are required";
3635
3677
  const provider = args.provider;
3636
- config.models.providers[provider] = { apiKey: args.apiKey };
3678
+ const entry = { apiKey: args.apiKey };
3679
+ if (provider === "openrouter") {
3680
+ entry.baseUrl = "https://openrouter.ai/api/v1";
3681
+ }
3682
+ config.models.providers[provider] = entry;
3637
3683
  await saveConfig(config);
3638
3684
  return `Provider ${provider} added with API key`;
3639
3685
  }
@@ -4521,6 +4567,131 @@ var init_file_share_tool = __esm({
4521
4567
  }
4522
4568
  });
4523
4569
 
4570
+ // src/tools/self-config/task-tool.ts
4571
+ var taskTool;
4572
+ var init_task_tool = __esm({
4573
+ "src/tools/self-config/task-tool.ts"() {
4574
+ "use strict";
4575
+ init_esm_shims();
4576
+ taskTool = {
4577
+ definition: {
4578
+ 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.",
4580
+ parameters: {
4581
+ type: "object",
4582
+ properties: {
4583
+ action: {
4584
+ type: "string",
4585
+ description: "Action: 'spawn' to start a task, 'status' to check one, 'list' to see all"
4586
+ },
4587
+ agentId: {
4588
+ type: "string",
4589
+ description: "Agent ID to run the task (required for spawn). Use 'default' for the default agent."
4590
+ },
4591
+ prompt: {
4592
+ type: "string",
4593
+ description: "The instruction for the sub-agent (required for spawn)"
4594
+ },
4595
+ label: {
4596
+ type: "string",
4597
+ description: "Human-readable task name (optional for spawn)"
4598
+ },
4599
+ taskId: {
4600
+ type: "string",
4601
+ description: "Task ID to check (required for status)"
4602
+ },
4603
+ timeoutMs: {
4604
+ type: "number",
4605
+ description: "Timeout in ms (optional, default 300000 = 5 minutes)"
4606
+ }
4607
+ },
4608
+ required: ["action"]
4609
+ }
4610
+ },
4611
+ safetyLevel: ((args) => {
4612
+ return args.action === "spawn" ? "medium" : "low";
4613
+ }),
4614
+ readOnly: false,
4615
+ validate(args, _ctx) {
4616
+ const action = args.action;
4617
+ if (!["spawn", "status", "list"].includes(action)) {
4618
+ return { ok: false, error: "action must be 'spawn', 'status', or 'list'" };
4619
+ }
4620
+ if (action === "spawn") {
4621
+ if (!args.agentId || typeof args.agentId !== "string") {
4622
+ return { ok: false, error: "agentId is required for spawn" };
4623
+ }
4624
+ if (!args.prompt || typeof args.prompt !== "string") {
4625
+ return { ok: false, error: "prompt is required for spawn" };
4626
+ }
4627
+ }
4628
+ if (action === "status" && (!args.taskId || typeof args.taskId !== "string")) {
4629
+ return { ok: false, error: "taskId is required for status" };
4630
+ }
4631
+ return { ok: true };
4632
+ },
4633
+ async execute(args, ctx) {
4634
+ const action = args.action;
4635
+ switch (action) {
4636
+ case "spawn": {
4637
+ if (!ctx.spawnTask) {
4638
+ return "Error: spawn_task is only available to the main agent. Sub-agents cannot spawn tasks.";
4639
+ }
4640
+ const agentId = args.agentId;
4641
+ const prompt = args.prompt;
4642
+ const label = args.label || prompt.slice(0, 60);
4643
+ const timeoutMs = args.timeoutMs || 3e5;
4644
+ try {
4645
+ const taskId = await ctx.spawnTask({ agentId, prompt, label, timeoutMs });
4646
+ return `Task spawned successfully.
4647
+ Task ID: ${taskId}
4648
+ Agent: ${agentId}
4649
+ Label: ${label}
4650
+ Timeout: ${Math.round(timeoutMs / 1e3)}s
4651
+
4652
+ The task is running in the background. Results will be delivered when the task completes.`;
4653
+ } catch (err) {
4654
+ const msg = err instanceof Error ? err.message : String(err);
4655
+ return `Error spawning task: ${msg}`;
4656
+ }
4657
+ }
4658
+ case "status": {
4659
+ if (!ctx.taskRegistry) return "Error: Task registry not available.";
4660
+ const task = ctx.taskRegistry.get(args.taskId);
4661
+ if (!task) return `No task found with ID: ${args.taskId}`;
4662
+ const elapsed = Math.round((Date.now() - task.startedAt) / 1e3);
4663
+ const lines = [
4664
+ `Task: ${task.label}`,
4665
+ `ID: ${task.id}`,
4666
+ `Agent: ${task.agentId}`,
4667
+ `Model: ${task.model}`,
4668
+ `Status: ${task.status}`,
4669
+ `Elapsed: ${elapsed}s`
4670
+ ];
4671
+ if (task.result) lines.push(`Result: ${task.result.slice(0, 500)}`);
4672
+ if (task.error) lines.push(`Error: ${task.error}`);
4673
+ return lines.join("\n");
4674
+ }
4675
+ case "list": {
4676
+ if (!ctx.taskRegistry) return "Error: Task registry not available.";
4677
+ const tasks = ctx.taskRegistry.list();
4678
+ if (tasks.length === 0) return "No background tasks.";
4679
+ return tasks.map((t) => {
4680
+ const elapsed = Math.round(((t.completedAt || Date.now()) - t.startedAt) / 1e3);
4681
+ return `\u2022 [${t.status}] ${t.label} (agent: ${t.agentId}, ${elapsed}s)`;
4682
+ }).join("\n");
4683
+ }
4684
+ default:
4685
+ return `Unknown action: ${action}`;
4686
+ }
4687
+ },
4688
+ formatConfirmation(args) {
4689
+ return `Spawn background task on agent "${args.agentId}": ${args.prompt?.slice(0, 80)}`;
4690
+ }
4691
+ };
4692
+ }
4693
+ });
4694
+
4524
4695
  // src/tools/self-config/index.ts
4525
4696
  function registerSelfConfigTools(registry) {
4526
4697
  registry.register(configTool);
@@ -4535,6 +4706,7 @@ function registerSelfConfigTools(registry) {
4535
4706
  registry.register(sttTool);
4536
4707
  registry.register(voiceListTool);
4537
4708
  registry.register(fileShareTool);
4709
+ registry.register(taskTool);
4538
4710
  }
4539
4711
  var init_self_config = __esm({
4540
4712
  "src/tools/self-config/index.ts"() {
@@ -4550,6 +4722,7 @@ var init_self_config = __esm({
4550
4722
  init_message_tool();
4551
4723
  init_voice_tool();
4552
4724
  init_file_share_tool();
4725
+ init_task_tool();
4553
4726
  init_config_tool();
4554
4727
  init_channel_tool();
4555
4728
  init_agent_tool();
@@ -4560,6 +4733,7 @@ var init_self_config = __esm({
4560
4733
  init_message_tool();
4561
4734
  init_voice_tool();
4562
4735
  init_file_share_tool();
4736
+ init_task_tool();
4563
4737
  }
4564
4738
  });
4565
4739
 
@@ -5190,6 +5364,112 @@ var init_memory2 = __esm({
5190
5364
  }
5191
5365
  });
5192
5366
 
5367
+ // src/tasks/registry.ts
5368
+ import { randomUUID as randomUUID4 } from "crypto";
5369
+ var TaskRegistry;
5370
+ var init_registry2 = __esm({
5371
+ "src/tasks/registry.ts"() {
5372
+ "use strict";
5373
+ init_esm_shims();
5374
+ TaskRegistry = class {
5375
+ tasks = /* @__PURE__ */ new Map();
5376
+ cleanupTimer = null;
5377
+ /** Start the cleanup interval */
5378
+ start() {
5379
+ this.cleanupTimer = setInterval(() => this.cleanup(30 * 6e4), 10 * 6e4);
5380
+ }
5381
+ /** Stop the cleanup interval */
5382
+ stop() {
5383
+ if (this.cleanupTimer) {
5384
+ clearInterval(this.cleanupTimer);
5385
+ this.cleanupTimer = null;
5386
+ }
5387
+ }
5388
+ /** Create a new running task */
5389
+ create(opts) {
5390
+ const entry = {
5391
+ id: randomUUID4(),
5392
+ label: opts.label,
5393
+ agentId: opts.agentId,
5394
+ model: opts.model,
5395
+ status: "running",
5396
+ prompt: opts.prompt,
5397
+ startedAt: Date.now(),
5398
+ timeoutMs: opts.timeoutMs,
5399
+ spawnedBy: opts.spawnedBy,
5400
+ delivered: false
5401
+ };
5402
+ this.tasks.set(entry.id, entry);
5403
+ return entry;
5404
+ }
5405
+ /** Update a task's fields */
5406
+ update(id, patch) {
5407
+ const task = this.tasks.get(id);
5408
+ if (task) {
5409
+ Object.assign(task, patch);
5410
+ }
5411
+ }
5412
+ /** Get a specific task */
5413
+ get(id) {
5414
+ return this.tasks.get(id);
5415
+ }
5416
+ /** List all tasks, optionally filtered by status */
5417
+ list(filter) {
5418
+ let results = Array.from(this.tasks.values());
5419
+ if (filter?.status) {
5420
+ results = results.filter((t) => t.status === filter.status);
5421
+ }
5422
+ if (filter?.spawnedBy) {
5423
+ results = results.filter((t) => t.spawnedBy === filter.spawnedBy);
5424
+ }
5425
+ return results.sort((a, b) => b.startedAt - a.startedAt);
5426
+ }
5427
+ /**
5428
+ * Get completed tasks for a session that haven't been delivered yet.
5429
+ * Marks them as delivered so they aren't injected twice.
5430
+ */
5431
+ consumeCompleted(spawnedBy) {
5432
+ const ready = [];
5433
+ for (const task of this.tasks.values()) {
5434
+ if (task.spawnedBy === spawnedBy && task.status !== "running" && !task.delivered) {
5435
+ task.delivered = true;
5436
+ ready.push(task);
5437
+ }
5438
+ }
5439
+ return ready;
5440
+ }
5441
+ /** Remove completed tasks older than maxAgeMs */
5442
+ cleanup(maxAgeMs) {
5443
+ const now = Date.now();
5444
+ for (const [id, task] of this.tasks) {
5445
+ if (task.status !== "running" && task.completedAt && now - task.completedAt > maxAgeMs) {
5446
+ this.tasks.delete(id);
5447
+ }
5448
+ }
5449
+ }
5450
+ /** Cancel all running tasks */
5451
+ cancelAll() {
5452
+ for (const task of this.tasks.values()) {
5453
+ if (task.status === "running") {
5454
+ task.status = "timeout";
5455
+ task.completedAt = Date.now();
5456
+ task.error = "Gateway shutting down";
5457
+ }
5458
+ }
5459
+ }
5460
+ };
5461
+ }
5462
+ });
5463
+
5464
+ // src/tasks/index.ts
5465
+ var init_tasks = __esm({
5466
+ "src/tasks/index.ts"() {
5467
+ "use strict";
5468
+ init_esm_shims();
5469
+ init_registry2();
5470
+ }
5471
+ });
5472
+
5193
5473
  // src/routing/resolve-route.ts
5194
5474
  function resolveRoute(context, bindings, agents2, defaultAgentId) {
5195
5475
  const scored = bindings.map((b) => ({
@@ -5498,8 +5778,8 @@ var init_telegram = __esm({
5498
5778
  }
5499
5779
  const audioBuffer = Buffer.from(await res.arrayBuffer());
5500
5780
  const { STTEngine: STTEngine2 } = await Promise.resolve().then(() => (init_voice(), voice_exports));
5501
- const { loadConfig: loadConfig3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
5502
- const config = await loadConfig3();
5781
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
5782
+ const config = await loadConfig2();
5503
5783
  const stt = new STTEngine2(config);
5504
5784
  if (!stt.isAvailable()) {
5505
5785
  await ctx.api.sendMessage(chatId, "Voice messages require speech-to-text. Set up Whisper: /help");
@@ -5668,6 +5948,7 @@ You can read this file with the read_file tool.`
5668
5948
  "/new \u2014 Start a new session",
5669
5949
  "/reset \u2014 Clear current session",
5670
5950
  "/model \u2014 Show current model",
5951
+ "/tasks \u2014 Show background tasks",
5671
5952
  "/think \u2014 Toggle thinking display"
5672
5953
  ].join("\n");
5673
5954
  case "status": {
@@ -5711,6 +5992,14 @@ You can read this file with the read_file tool.`
5711
5992
  const model = this.config?.agents?.defaults?.model?.primary || "unknown";
5712
5993
  return `Current model: \`${model}\``;
5713
5994
  }
5995
+ case "tasks": {
5996
+ const tasks = this.gateway?.getTaskRegistry()?.list() || [];
5997
+ if (tasks.length === 0) return "No background tasks.";
5998
+ return "*Background Tasks:*\n" + tasks.map((t) => {
5999
+ 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)`;
6001
+ }).join("\n");
6002
+ }
5714
6003
  case "think":
5715
6004
  return "Thinking display toggled. (Note: thinking visibility is per-client in the TUI/Web UI)";
5716
6005
  default:
@@ -6042,6 +6331,7 @@ var init_server = __esm({
6042
6331
  init_memory2();
6043
6332
  init_config2();
6044
6333
  init_cron();
6334
+ init_tasks();
6045
6335
  init_routing();
6046
6336
  init_telegram();
6047
6337
  init_discord();
@@ -6060,6 +6350,7 @@ var init_server = __esm({
6060
6350
  cronScheduler;
6061
6351
  configWatcher;
6062
6352
  pluginLoader;
6353
+ taskRegistry;
6063
6354
  adapters = [];
6064
6355
  running = false;
6065
6356
  /** Rate limiting: track message timestamps per session */
@@ -6076,6 +6367,11 @@ var init_server = __esm({
6076
6367
  this.cronScheduler = new CronScheduler(join14(getConfigDir(), "cron"));
6077
6368
  this.configWatcher = new ConfigWatcher();
6078
6369
  this.pluginLoader = new PluginLoader();
6370
+ this.taskRegistry = new TaskRegistry();
6371
+ }
6372
+ /** Get the task registry (for adapters to list tasks) */
6373
+ getTaskRegistry() {
6374
+ return this.taskRegistry;
6079
6375
  }
6080
6376
  /** Start the gateway server */
6081
6377
  async start() {
@@ -6090,6 +6386,7 @@ var init_server = __esm({
6090
6386
  await this.sessionStore.init();
6091
6387
  await this.memoryManager.init();
6092
6388
  await this.cronScheduler.init();
6389
+ this.taskRegistry.start();
6093
6390
  const plugins = await this.pluginLoader.loadAll();
6094
6391
  for (const tool of this.pluginLoader.getTools()) {
6095
6392
  this.toolRegistry.register(tool);
@@ -6243,6 +6540,8 @@ var init_server = __esm({
6243
6540
  async stop() {
6244
6541
  this.running = false;
6245
6542
  this.cronScheduler.stop();
6543
+ this.taskRegistry.cancelAll();
6544
+ this.taskRegistry.stop();
6246
6545
  this.configWatcher.stop();
6247
6546
  for (const adapter of this.adapters) {
6248
6547
  try {
@@ -6272,7 +6571,7 @@ var init_server = __esm({
6272
6571
  res.writeHead(200, { "Content-Type": "application/json" });
6273
6572
  res.end(JSON.stringify({
6274
6573
  status: "ok",
6275
- version: "1.5.10",
6574
+ version: "1.6.0",
6276
6575
  uptime: process.uptime(),
6277
6576
  clients: this.clients.size,
6278
6577
  agents: this.engines.size
@@ -6384,7 +6683,7 @@ var init_server = __esm({
6384
6683
  const hello = {
6385
6684
  type: "hello",
6386
6685
  protocol: PROTOCOL_VERSION,
6387
- version: "1.5.10",
6686
+ version: "1.6.0",
6388
6687
  agents: this.config.agents.list.map((a) => ({
6389
6688
  id: a.id,
6390
6689
  name: a.name || a.id,
@@ -6461,6 +6760,34 @@ var init_server = __esm({
6461
6760
  } : null);
6462
6761
  break;
6463
6762
  }
6763
+ // === Tasks ===
6764
+ case "task.list":
6765
+ this.sendResponse(client, frame.id, true, this.taskRegistry.list().map((t) => ({
6766
+ id: t.id,
6767
+ label: t.label,
6768
+ agentId: t.agentId,
6769
+ model: t.model,
6770
+ status: t.status,
6771
+ startedAt: t.startedAt,
6772
+ completedAt: t.completedAt
6773
+ })));
6774
+ break;
6775
+ case "task.status": {
6776
+ const task = this.taskRegistry.get(frame.params?.taskId);
6777
+ this.sendResponse(client, frame.id, true, task ? {
6778
+ id: task.id,
6779
+ label: task.label,
6780
+ agentId: task.agentId,
6781
+ model: task.model,
6782
+ status: task.status,
6783
+ prompt: task.prompt,
6784
+ result: task.result,
6785
+ error: task.error,
6786
+ startedAt: task.startedAt,
6787
+ completedAt: task.completedAt
6788
+ } : null);
6789
+ break;
6790
+ }
6464
6791
  // === Config ===
6465
6792
  case "config.get": {
6466
6793
  const { redactConfig: redactConfig2 } = await Promise.resolve().then(() => (init_redact(), redact_exports));
@@ -6474,8 +6801,8 @@ var init_server = __esm({
6474
6801
  break;
6475
6802
  }
6476
6803
  case "config.set": {
6477
- const { loadConfig: loadConfig3, saveConfig: saveConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
6478
- const cfg = await loadConfig3();
6804
+ const { loadConfig: loadConfig2, saveConfig: saveConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
6805
+ const cfg = await loadConfig2();
6479
6806
  const key = frame.params?.key;
6480
6807
  const value = frame.params?.value;
6481
6808
  const BLOCKED_KEYS = ["__proto__", "constructor", "prototype"];
@@ -6629,13 +6956,53 @@ var init_server = __esm({
6629
6956
  const memoryBudget = resolved.isLocal ? 1500 : 4e3;
6630
6957
  const memoryBlock = await this.memoryManager.buildMemoryBlock("", identity.workspace, memoryBudget);
6631
6958
  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) => {
6961
+ const subAgentConfig = this.config.agents.list.find((a) => a.id === opts.agentId);
6962
+ const subModel = subAgentConfig?.model?.primary || this.config.agents.defaults.model.primary;
6963
+ const task = this.taskRegistry.create({
6964
+ agentId: opts.agentId,
6965
+ model: subModel,
6966
+ prompt: opts.prompt,
6967
+ label: opts.label,
6968
+ timeoutMs: opts.timeoutMs,
6969
+ spawnedBy: sessionKey
6970
+ });
6971
+ const subSessionKey = `task:${task.id}`;
6972
+ const subEngine = await this.getOrCreateEngine(subSessionKey, opts.agentId, "task");
6973
+ const timeout = setTimeout(() => {
6974
+ subEngine.cancel();
6975
+ this.taskRegistry.update(task.id, { status: "timeout", completedAt: Date.now(), error: "Task timed out" });
6976
+ subEngine.destroy();
6977
+ this.engines.delete(subSessionKey);
6978
+ }, opts.timeoutMs);
6979
+ subEngine.sendMessage(opts.prompt).then((result) => {
6980
+ clearTimeout(timeout);
6981
+ this.taskRegistry.update(task.id, { status: "completed", result, completedAt: Date.now() });
6982
+ subEngine.destroy();
6983
+ this.engines.delete(subSessionKey);
6984
+ }).catch((err) => {
6985
+ clearTimeout(timeout);
6986
+ this.taskRegistry.update(task.id, {
6987
+ status: "failed",
6988
+ error: err instanceof Error ? err.message : String(err),
6989
+ completedAt: Date.now()
6990
+ });
6991
+ subEngine.destroy();
6992
+ this.engines.delete(subSessionKey);
6993
+ });
6994
+ return task.id;
6995
+ } : void 0;
6632
6996
  engine = new AgentEngine({
6633
6997
  identity,
6634
6998
  toolRegistry: this.toolRegistry,
6635
6999
  sessionStore: this.sessionStore,
6636
7000
  provider: resolved,
6637
7001
  autoApprove: this.config.tools.autoApprove,
6638
- systemPrompt: fullPrompt
7002
+ systemPrompt: fullPrompt,
7003
+ taskRegistry: this.taskRegistry,
7004
+ spawnTask: spawnTaskFn,
7005
+ sessionKey
6639
7006
  });
6640
7007
  await engine.loadSession(sessionKey, channel);
6641
7008
  this.engines.set(sessionKey, engine);
@@ -7115,7 +7482,12 @@ function ask(rl, question) {
7115
7482
  }
7116
7483
  async function runSetup(opts) {
7117
7484
  await ensureConfigDir();
7118
- const config = defaultConfig();
7485
+ let config;
7486
+ try {
7487
+ config = await loadConfig();
7488
+ } catch {
7489
+ config = defaultConfig();
7490
+ }
7119
7491
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
7120
7492
  try {
7121
7493
  console.log("");
@@ -7163,41 +7535,78 @@ async function runSetup(opts) {
7163
7535
  console.log(dim4(" Install Ollama (recommended) or configure a cloud provider."));
7164
7536
  }
7165
7537
  console.log("");
7166
- const addCloud = await ask(rl, cyan2(" Add a cloud provider as fallback? [y/N] "));
7538
+ const existingProviders = [];
7539
+ const providers = config.models.providers;
7540
+ for (const name of ["anthropic", "openai", "google", "openrouter"]) {
7541
+ if (providers[name]?.apiKey) existingProviders.push(name);
7542
+ }
7543
+ if (existingProviders.length > 0) {
7544
+ console.log(dim4(` Existing cloud providers: ${existingProviders.join(", ")}`));
7545
+ }
7546
+ const addCloud = await ask(rl, cyan2(" Add cloud providers? [y/N] "));
7167
7547
  if (addCloud.toLowerCase() === "y") {
7168
- console.log(dim4(" 1. Anthropic (Claude)"));
7169
- console.log(dim4(" 2. OpenAI (GPT)"));
7170
- console.log(dim4(" 3. Google (Gemini)"));
7171
- const provider = await ask(rl, cyan2(" Which provider? [1]: "));
7172
- switch (provider || "1") {
7173
- case "1": {
7174
- const key = await ask(rl, cyan2(" Anthropic API key: "));
7175
- if (key.trim()) {
7176
- config.models.providers.anthropic = { apiKey: key.trim() };
7177
- config.agents.defaults.model.fallbacks = ["anthropic/claude-sonnet-4-6"];
7178
- console.log(green4(" Anthropic configured as fallback"));
7548
+ const fallbacks = config.agents.defaults.model.fallbacks || [];
7549
+ let picking = true;
7550
+ while (picking) {
7551
+ console.log("");
7552
+ console.log(" 1. Anthropic (Claude)");
7553
+ console.log(" 2. OpenAI (GPT-4o, Codex)");
7554
+ console.log(" 3. Google (Gemini)");
7555
+ console.log(" 4. OpenRouter (many models via one key)");
7556
+ console.log(" 5. Done");
7557
+ const choice = await ask(rl, cyan2(" Which provider? "));
7558
+ const providerSetup = async (name, defaultModel, keyName) => {
7559
+ const existing = providers[name]?.apiKey;
7560
+ if (existing) {
7561
+ const keep = await ask(rl, cyan2(` ${keyName} already configured. Keep existing? [Y/n] `));
7562
+ if (keep.toLowerCase() !== "n") {
7563
+ console.log(green4(` Kept existing ${name} config`));
7564
+ if (!fallbacks.includes(defaultModel)) fallbacks.push(defaultModel);
7565
+ return;
7566
+ }
7179
7567
  }
7180
- break;
7181
- }
7182
- case "2": {
7183
- const key = await ask(rl, cyan2(" OpenAI API key: "));
7568
+ const key = await ask(rl, cyan2(` ${keyName} API key: `));
7184
7569
  if (key.trim()) {
7185
- config.models.providers.openai = { apiKey: key.trim() };
7186
- config.agents.defaults.model.fallbacks = ["openai/gpt-4o"];
7187
- console.log(green4(" OpenAI configured as fallback"));
7570
+ const entry = { apiKey: key.trim() };
7571
+ if (name === "openrouter") entry.baseUrl = "https://openrouter.ai/api/v1";
7572
+ config.models.providers[name] = entry;
7573
+ if (!fallbacks.includes(defaultModel)) fallbacks.push(defaultModel);
7574
+ console.log(green4(` ${name} configured`));
7188
7575
  }
7189
- break;
7190
- }
7191
- case "3": {
7192
- const key = await ask(rl, cyan2(" Google AI API key: "));
7193
- if (key.trim()) {
7194
- config.models.providers.google = { apiKey: key.trim() };
7195
- config.agents.defaults.model.fallbacks = ["google/gemini-2.0-flash"];
7196
- console.log(green4(" Google configured as fallback"));
7576
+ };
7577
+ switch (choice) {
7578
+ case "1":
7579
+ await providerSetup("anthropic", "anthropic/claude-sonnet-4-6", "Anthropic");
7580
+ break;
7581
+ case "2":
7582
+ await providerSetup("openai", "openai/gpt-4o", "OpenAI");
7583
+ break;
7584
+ case "3":
7585
+ await providerSetup("google", "google/gemini-2.0-flash", "Google");
7586
+ break;
7587
+ case "4": {
7588
+ await providerSetup("openrouter", "openrouter/anthropic/claude-sonnet-4-6", "OpenRouter");
7589
+ if (providers.openrouter?.apiKey) {
7590
+ const orModel = await ask(rl, cyan2(" Default OpenRouter model (e.g., meta-llama/llama-3.1-70b): "));
7591
+ if (orModel.trim()) {
7592
+ const fullModel = `openrouter/${orModel.trim()}`;
7593
+ const idx = fallbacks.indexOf("openrouter/anthropic/claude-sonnet-4-6");
7594
+ if (idx >= 0) fallbacks[idx] = fullModel;
7595
+ else fallbacks.push(fullModel);
7596
+ }
7597
+ }
7598
+ break;
7197
7599
  }
7198
- break;
7600
+ case "5":
7601
+ case "":
7602
+ picking = false;
7603
+ break;
7604
+ default:
7605
+ console.log(dim4(" Invalid choice"));
7606
+ break;
7199
7607
  }
7200
7608
  }
7609
+ config.agents.defaults.model.fallbacks = fallbacks;
7201
7610
  }
7202
7611
  console.log("");
7203
7612
  console.log(dim4(" Gateway settings:"));
@@ -7780,7 +8189,7 @@ async function runTui(opts) {
7780
8189
  ws.on("open", () => {
7781
8190
  ws.send(JSON.stringify({
7782
8191
  type: "connect",
7783
- params: { auth: { token }, mode: "tui", version: "1.5.10" }
8192
+ params: { auth: { token }, mode: "tui", version: "1.6.0" }
7784
8193
  }));
7785
8194
  });
7786
8195
  ws.on("message", (data) => {
@@ -8209,7 +8618,7 @@ import { fileURLToPath as fileURLToPath5 } from "url";
8209
8618
  import { dirname as dirname5, join as join19 } from "path";
8210
8619
  var __filename3 = fileURLToPath5(import.meta.url);
8211
8620
  var __dirname3 = dirname5(__filename3);
8212
- var version = "1.5.10";
8621
+ var version = "1.6.0";
8213
8622
  try {
8214
8623
  const pkg = JSON.parse(readFileSync(join19(__dirname3, "..", "package.json"), "utf-8"));
8215
8624
  version = pkg.version;
@@ -8292,8 +8701,8 @@ program.command("tui").description("Launch the terminal UI (connects to gateway)
8292
8701
  await runTui2(opts);
8293
8702
  });
8294
8703
  program.command("dashboard").description("Open the Web UI in your browser").option("--no-open", "Don't auto-open browser").action(async (opts) => {
8295
- const { loadConfig: loadConfig3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8296
- const config = await loadConfig3();
8704
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8705
+ const config = await loadConfig2();
8297
8706
  const port = config.gateway.port || 18789;
8298
8707
  const token = config.gateway.auth.token || "";
8299
8708
  const url = `http://127.0.0.1:${port}/#token=${token}`;
@@ -8358,8 +8767,8 @@ cron.command("remove <id>").description("Remove a cron job").action(async (id) =
8358
8767
  console.log(removed ? ` Job ${id.slice(0, 8)} removed` : ` Job not found`);
8359
8768
  });
8360
8769
  program.command("channels").description("Show channel adapter status").action(async () => {
8361
- const { loadConfig: loadConfig3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8362
- const config = await loadConfig3();
8770
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8771
+ const config = await loadConfig2();
8363
8772
  const channels = config.channels || {};
8364
8773
  console.log(" Channels:");
8365
8774
  for (const [name, cfg] of Object.entries(channels)) {