@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/CHANGELOG.md +16 -0
- package/README.md +195 -195
- package/dist/index.js +456 -47
- package/dist/index.js.map +1 -1
- package/package.json +58 -58
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:
|
|
2303
|
-
const config = await
|
|
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
|
-
|
|
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:
|
|
5502
|
-
const config = await
|
|
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.
|
|
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.
|
|
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:
|
|
6478
|
-
const cfg = await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
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
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
8296
|
-
const config = await
|
|
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:
|
|
8362
|
-
const config = await
|
|
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)) {
|