@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/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/dist/index.js +815 -147
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
846
|
+
/** Function to spawn background tasks */
|
|
847
847
|
spawnTaskFn = void 0;
|
|
848
|
-
/**
|
|
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:
|
|
4430
|
+
const { writeFile: writeFile11, unlink: unlink6 } = await import("fs/promises");
|
|
4380
4431
|
const { execSync: execSync3 } = await import("child_process");
|
|
4381
|
-
const { join:
|
|
4432
|
+
const { join: join21 } = await import("path");
|
|
4382
4433
|
const { tmpdir } = await import("os");
|
|
4383
|
-
const tmpFile =
|
|
4384
|
-
await
|
|
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
|
|
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:
|
|
4453
|
-
const { join:
|
|
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 =
|
|
4456
|
-
await
|
|
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:
|
|
4480
|
-
const { existsSync:
|
|
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 (!
|
|
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
|
|
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: "
|
|
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'
|
|
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
|
|
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
|
-
|
|
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 '
|
|
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 (
|
|
4629
|
-
return { ok: false, error: "taskId is required for
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
4937
|
+
const { dirname: dirname6, join: join21 } = await import("path");
|
|
4816
4938
|
const __filename4 = fileURLToPath6(import.meta.url);
|
|
4817
|
-
const entryPoint =
|
|
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:
|
|
4844
|
-
const { exec } = await import("child_process");
|
|
4845
|
-
const openCmd =
|
|
4846
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
5423
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
6171
|
-
import { existsSync as
|
|
6172
|
-
import { join as
|
|
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 =
|
|
6206
|
-
if (
|
|
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(
|
|
6716
|
+
dirs.push(join14(userPluginDir, entry.name));
|
|
6212
6717
|
}
|
|
6213
6718
|
}
|
|
6214
6719
|
} catch {
|
|
6215
6720
|
}
|
|
6216
6721
|
}
|
|
6217
|
-
const nodeModulesDir =
|
|
6218
|
-
if (
|
|
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(
|
|
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 =
|
|
6234
|
-
if (!
|
|
6235
|
-
const raw = await
|
|
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 =
|
|
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 =
|
|
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
|
|
6320
|
-
import { join as
|
|
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(
|
|
6871
|
+
this.sessionStore = new SessionStore(join15(getConfigDir(), "conversations"));
|
|
6365
6872
|
this.toolRegistry = createFullRegistry();
|
|
6366
|
-
this.memoryManager = new MemoryManager(
|
|
6367
|
-
this.cronScheduler = new CronScheduler(
|
|
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:
|
|
6383
|
-
this.config.gateway.auth.token =
|
|
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 =
|
|
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.
|
|
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 =
|
|
6605
|
-
const html = await
|
|
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
|
|
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.
|
|
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
|
|
6960
|
-
const
|
|
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:
|
|
7587
|
+
systemPrompt: finalPrompt,
|
|
7003
7588
|
taskRegistry: this.taskRegistry,
|
|
7004
7589
|
spawnTask: spawnTaskFn,
|
|
7005
|
-
|
|
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
|
|
7114
|
-
import { existsSync as
|
|
7115
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
7178
|
-
const logFile =
|
|
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 (
|
|
7797
|
+
if (existsSync10(pidPath)) {
|
|
7209
7798
|
try {
|
|
7210
|
-
const pid = parseInt(await
|
|
7799
|
+
const pid = parseInt(await readFile13(pidPath, "utf-8"), 10);
|
|
7211
7800
|
process.kill(pid, "SIGTERM");
|
|
7212
|
-
await
|
|
7801
|
+
await unlink4(pidPath);
|
|
7213
7802
|
console.log(green2("Gateway stopped"));
|
|
7214
7803
|
return;
|
|
7215
7804
|
} catch {
|
|
7216
7805
|
try {
|
|
7217
|
-
await
|
|
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 (
|
|
7242
|
-
const pid = await
|
|
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
|
|
7271
|
-
import { join as
|
|
7272
|
-
import { homedir as homedir3, platform as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
7340
|
-
const plistPath =
|
|
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>${
|
|
7950
|
+
<string>${join17(homedir3(), ".clank", "logs", "gateway.log")}</string>
|
|
7362
7951
|
<key>StandardErrorPath</key>
|
|
7363
|
-
<string>${
|
|
7952
|
+
<string>${join17(homedir3(), ".clank", "logs", "gateway-error.log")}</string>
|
|
7364
7953
|
</dict>
|
|
7365
7954
|
</plist>`;
|
|
7366
|
-
await
|
|
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 =
|
|
7961
|
+
const plistPath = join17(homedir3(), "Library", "LaunchAgents", "com.clank.gateway.plist");
|
|
7373
7962
|
try {
|
|
7374
7963
|
execSync(`launchctl unload "${plistPath}"`);
|
|
7375
|
-
await
|
|
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 =
|
|
7411
|
-
const unitPath =
|
|
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
|
|
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 =
|
|
7438
|
-
await
|
|
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
|
|
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.
|
|
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 =
|
|
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 =
|
|
7624
|
-
const wsDir =
|
|
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
|
|
8401
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7786
8402
|
import { readdir as readdir7 } from "fs/promises";
|
|
7787
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
7886
|
-
if (!
|
|
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 =
|
|
7899
|
-
if (!
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
|
9237
|
+
var version = "1.7.0";
|
|
8622
9238
|
try {
|
|
8623
|
-
const pkg = JSON.parse(readFileSync(
|
|
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:
|
|
8714
|
-
const { exec } = await import("child_process");
|
|
8715
|
-
const cmd =
|
|
8716
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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
|