@shipers-dev/multi 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +108 -39
- package/package.json +1 -1
- package/src/acp-runner.ts +15 -3
- package/src/index.ts +89 -35
package/dist/index.js
CHANGED
|
@@ -5198,6 +5198,7 @@ async function runAcp(opts) {
|
|
|
5198
5198
|
const stream2 = ndJsonStream(output, input);
|
|
5199
5199
|
let activeSessionId = opts.sessionId || null;
|
|
5200
5200
|
let recording = false;
|
|
5201
|
+
const alwaysAllow = new Set;
|
|
5201
5202
|
const client = {
|
|
5202
5203
|
async sessionUpdate(params) {
|
|
5203
5204
|
if (!recording)
|
|
@@ -5205,7 +5206,7 @@ async function runAcp(opts) {
|
|
|
5205
5206
|
await handleSessionUpdate(params, opts);
|
|
5206
5207
|
},
|
|
5207
5208
|
async requestPermission(params) {
|
|
5208
|
-
return await handleRequestPermission(params, opts);
|
|
5209
|
+
return await handleRequestPermission(params, opts, alwaysAllow);
|
|
5209
5210
|
},
|
|
5210
5211
|
async readTextFile(params) {
|
|
5211
5212
|
try {
|
|
@@ -5325,8 +5326,14 @@ ${entries}` } });
|
|
|
5325
5326
|
break;
|
|
5326
5327
|
}
|
|
5327
5328
|
}
|
|
5328
|
-
async function handleRequestPermission(params, o) {
|
|
5329
|
+
async function handleRequestPermission(params, o, allowCache) {
|
|
5329
5330
|
const tc = params.toolCall || {};
|
|
5331
|
+
const toolKey = `${tc.toolName || tc.title || ""}|${tc.kind || ""}`.toLowerCase();
|
|
5332
|
+
if (toolKey && allowCache.has(toolKey)) {
|
|
5333
|
+
const allowOpt = params.options.find((op) => /allow/i.test(op.kind || "") || /allow/i.test(op.name || ""));
|
|
5334
|
+
if (allowOpt)
|
|
5335
|
+
return { outcome: { outcome: "selected", optionId: allowOpt.optionId } };
|
|
5336
|
+
}
|
|
5330
5337
|
const created = await apiClient.post(`${o.apiUrl}/api/permissions`, {
|
|
5331
5338
|
issue_id: o.issueId,
|
|
5332
5339
|
device_id: o.deviceId,
|
|
@@ -5346,6 +5353,10 @@ ${entries}` } });
|
|
|
5346
5353
|
if (!row)
|
|
5347
5354
|
break;
|
|
5348
5355
|
if (row.status === "resolved" && row.chosen) {
|
|
5356
|
+
const chosenOpt = params.options.find((op) => op.optionId === row.chosen);
|
|
5357
|
+
const isAlways = chosenOpt && (/always/i.test(chosenOpt.name || "") || /allow_always/i.test(chosenOpt.kind || ""));
|
|
5358
|
+
if (isAlways && toolKey)
|
|
5359
|
+
allowCache.add(toolKey);
|
|
5349
5360
|
return { outcome: { outcome: "selected", optionId: row.chosen } };
|
|
5350
5361
|
}
|
|
5351
5362
|
if (row.status === "cancelled") {
|
|
@@ -5381,7 +5392,7 @@ var LOG_PATH = join(MULTI_DIR, "logs", "agent.log");
|
|
|
5381
5392
|
var SKILLS_DIR = join(MULTI_DIR, "skills");
|
|
5382
5393
|
var STOP_PATH = join(MULTI_DIR, "stop.flag");
|
|
5383
5394
|
var TASKS_DB_PATH = join(MULTI_DIR, "tasks.db");
|
|
5384
|
-
var VERSION = "0.
|
|
5395
|
+
var VERSION = "0.5.1";
|
|
5385
5396
|
var COMMANDS = {
|
|
5386
5397
|
setup: "Register this device with a workspace",
|
|
5387
5398
|
connect: "Connect device to realtime hub and execute assigned tasks",
|
|
@@ -5414,6 +5425,7 @@ async function main() {
|
|
|
5414
5425
|
options: {
|
|
5415
5426
|
help: { type: "boolean", default: false, short: "h" },
|
|
5416
5427
|
version: { type: "boolean", default: false },
|
|
5428
|
+
detach: { type: "boolean", default: false, short: "d" },
|
|
5417
5429
|
name: { type: "string" },
|
|
5418
5430
|
workspace: { type: "string" },
|
|
5419
5431
|
agent: { type: "string" },
|
|
@@ -5437,7 +5449,11 @@ async function main() {
|
|
|
5437
5449
|
break;
|
|
5438
5450
|
case "connect":
|
|
5439
5451
|
case "start":
|
|
5440
|
-
|
|
5452
|
+
if (args.values.detach) {
|
|
5453
|
+
await cmdConnectDetached(apiUrl);
|
|
5454
|
+
} else {
|
|
5455
|
+
await cmdConnect(apiUrl, config);
|
|
5456
|
+
}
|
|
5441
5457
|
break;
|
|
5442
5458
|
case "link":
|
|
5443
5459
|
await cmdLink(apiUrl, config, args.values.agent);
|
|
@@ -5473,10 +5489,11 @@ Commands:
|
|
|
5473
5489
|
|
|
5474
5490
|
Options:
|
|
5475
5491
|
--name <name> Device name
|
|
5476
|
-
--workspace <id> Workspace ID
|
|
5477
5492
|
--agent <id> Agent ID (for link)
|
|
5478
|
-
--api <url> API URL
|
|
5479
|
-
--
|
|
5493
|
+
--api <url> API URL
|
|
5494
|
+
-d, --detach Run connect in background (daemon)
|
|
5495
|
+
-v, --version Print version
|
|
5496
|
+
-h, --help Show this help
|
|
5480
5497
|
|
|
5481
5498
|
Examples:
|
|
5482
5499
|
multi-agent setup --name "My Mac" --workspace ws_xxx
|
|
@@ -5719,6 +5736,28 @@ async function cmdConnect(apiUrl, config) {
|
|
|
5719
5736
|
}
|
|
5720
5737
|
}
|
|
5721
5738
|
}
|
|
5739
|
+
async function cmdConnectDetached(apiUrl) {
|
|
5740
|
+
if (existsSync2(PID_PATH)) {
|
|
5741
|
+
const pid = Number(readFileSync2(PID_PATH, "utf8").trim());
|
|
5742
|
+
if (pid && isRunning(pid)) {
|
|
5743
|
+
console.log(`\u274C Daemon already running (pid ${pid}).`);
|
|
5744
|
+
process.exit(1);
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
ensureDirs();
|
|
5748
|
+
const logFd = Bun.file(LOG_PATH);
|
|
5749
|
+
const args = Bun.argv.slice(1).filter((a) => a !== "-d" && a !== "--detach");
|
|
5750
|
+
const proc = Bun.spawn([process.execPath, ...args, "--api", apiUrl], {
|
|
5751
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
5752
|
+
env: { ...process.env, MULTI_DETACHED: "1" }
|
|
5753
|
+
});
|
|
5754
|
+
proc.unref?.();
|
|
5755
|
+
await sleep(500);
|
|
5756
|
+
console.log(`\u2705 Daemon started in background (pid ${proc.pid}).`);
|
|
5757
|
+
console.log(` Tail logs: multi-agent logs`);
|
|
5758
|
+
console.log(` Stop: multi-agent stop`);
|
|
5759
|
+
process.exit(0);
|
|
5760
|
+
}
|
|
5722
5761
|
async function pickFreePort() {
|
|
5723
5762
|
for (let i = 0;i < 10; i++) {
|
|
5724
5763
|
const p = 40000 + Math.floor(Math.random() * 20000);
|
|
@@ -5805,29 +5844,71 @@ async function handleRunTask(apiUrl, deviceId, task, detected) {
|
|
|
5805
5844
|
} catch {}
|
|
5806
5845
|
};
|
|
5807
5846
|
const turn = {
|
|
5808
|
-
|
|
5809
|
-
toolOrder: [],
|
|
5847
|
+
blocks: [],
|
|
5810
5848
|
tools: new Map,
|
|
5811
5849
|
plans: [],
|
|
5812
5850
|
progress: [],
|
|
5813
5851
|
result: null,
|
|
5814
5852
|
error: null
|
|
5815
5853
|
};
|
|
5854
|
+
const appendText = (text) => {
|
|
5855
|
+
const last = turn.blocks[turn.blocks.length - 1];
|
|
5856
|
+
if (last && last.kind === "text")
|
|
5857
|
+
last.text += text;
|
|
5858
|
+
else
|
|
5859
|
+
turn.blocks.push({ kind: "text", text });
|
|
5860
|
+
};
|
|
5861
|
+
const upsertTool = (id, patch) => {
|
|
5862
|
+
const existing = turn.tools.get(id);
|
|
5863
|
+
if (existing) {
|
|
5864
|
+
if (patch.tool && patch.tool !== "tool")
|
|
5865
|
+
existing.tool = patch.tool;
|
|
5866
|
+
if (patch.kind)
|
|
5867
|
+
existing.kind = patch.kind;
|
|
5868
|
+
if (patch.status)
|
|
5869
|
+
existing.status = patch.status;
|
|
5870
|
+
if (patch.input !== undefined)
|
|
5871
|
+
existing.input = patch.input;
|
|
5872
|
+
} else {
|
|
5873
|
+
turn.tools.set(id, { id, tool: patch.tool || "tool", kind: patch.kind, status: patch.status, input: patch.input, results: [] });
|
|
5874
|
+
turn.blocks.push({ kind: "tool", id });
|
|
5875
|
+
}
|
|
5876
|
+
};
|
|
5877
|
+
const toolLabel = (t) => {
|
|
5878
|
+
const clean = stripMd(t.tool);
|
|
5879
|
+
if (clean && clean !== "tool")
|
|
5880
|
+
return clean;
|
|
5881
|
+
if (t.input && typeof t.input === "object") {
|
|
5882
|
+
if (t.input.command)
|
|
5883
|
+
return `Bash: ${String(t.input.command).split(`
|
|
5884
|
+
`)[0].slice(0, 60)}`;
|
|
5885
|
+
if (t.input.file_path)
|
|
5886
|
+
return `${t.kind === "edit" ? "Edit" : "Read"}: ${t.input.file_path}`;
|
|
5887
|
+
if (t.input.pattern)
|
|
5888
|
+
return `Grep: ${t.input.pattern}`;
|
|
5889
|
+
if (t.input.url)
|
|
5890
|
+
return `Fetch: ${t.input.url}`;
|
|
5891
|
+
}
|
|
5892
|
+
return t.kind ? `${t.kind}` : "tool";
|
|
5893
|
+
};
|
|
5816
5894
|
const render = () => {
|
|
5817
5895
|
const parts = [];
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5896
|
+
for (const b of turn.blocks) {
|
|
5897
|
+
if (b.kind === "text") {
|
|
5898
|
+
if (b.text)
|
|
5899
|
+
parts.push(b.text);
|
|
5900
|
+
continue;
|
|
5901
|
+
}
|
|
5902
|
+
const t = turn.tools.get(b.id);
|
|
5822
5903
|
if (!t)
|
|
5823
5904
|
continue;
|
|
5824
5905
|
const icon = statusIcon(t.status);
|
|
5825
|
-
const
|
|
5826
|
-
const head = `${icon} ${
|
|
5906
|
+
const label = toolLabel(t);
|
|
5907
|
+
const head = `${icon} ${label}${t.status && t.status !== "completed" ? ` \xB7 ${t.status}` : ""}`;
|
|
5827
5908
|
const body = [];
|
|
5828
5909
|
if (t.input !== undefined && t.input !== null) {
|
|
5829
5910
|
const inputStr = typeof t.input === "object" ? JSON.stringify(t.input, null, 2) : String(t.input);
|
|
5830
|
-
body.push("```json\n" + inputStr + "\n```");
|
|
5911
|
+
body.push("```json\n" + inputStr.slice(0, 2000) + "\n```");
|
|
5831
5912
|
}
|
|
5832
5913
|
if (t.results.length) {
|
|
5833
5914
|
const joined = t.results.join(`
|
|
@@ -5889,7 +5970,7 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5889
5970
|
}
|
|
5890
5971
|
case "assistant_text": {
|
|
5891
5972
|
await ensureLiveComment();
|
|
5892
|
-
|
|
5973
|
+
appendText(p.text);
|
|
5893
5974
|
hasAssistantText = true;
|
|
5894
5975
|
schedulePatch();
|
|
5895
5976
|
break;
|
|
@@ -5897,8 +5978,8 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5897
5978
|
case "stdout": {
|
|
5898
5979
|
if (p.line) {
|
|
5899
5980
|
await ensureLiveComment();
|
|
5900
|
-
|
|
5901
|
-
` : "") + p.line;
|
|
5981
|
+
appendText((turn.blocks.length ? `
|
|
5982
|
+
` : "") + p.line);
|
|
5902
5983
|
hasAssistantText = true;
|
|
5903
5984
|
schedulePatch();
|
|
5904
5985
|
}
|
|
@@ -5906,36 +5987,24 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5906
5987
|
}
|
|
5907
5988
|
case "tool_call": {
|
|
5908
5989
|
await ensureLiveComment();
|
|
5909
|
-
const id = p.id || `anon-${turn.
|
|
5910
|
-
|
|
5911
|
-
if (existing) {
|
|
5912
|
-
if (p.tool)
|
|
5913
|
-
existing.tool = p.tool;
|
|
5914
|
-
if (p.kind)
|
|
5915
|
-
existing.kind = p.kind;
|
|
5916
|
-
if (p.status)
|
|
5917
|
-
existing.status = p.status;
|
|
5918
|
-
if (p.input !== undefined)
|
|
5919
|
-
existing.input = p.input;
|
|
5920
|
-
} else {
|
|
5921
|
-
turn.toolOrder.push(id);
|
|
5922
|
-
turn.tools.set(id, { id, tool: p.tool || "tool", kind: p.kind, status: p.status, input: p.input, results: [] });
|
|
5923
|
-
}
|
|
5990
|
+
const id = p.id || `anon-${turn.tools.size}`;
|
|
5991
|
+
upsertTool(id, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
|
|
5924
5992
|
schedulePatch();
|
|
5925
5993
|
break;
|
|
5926
5994
|
}
|
|
5927
5995
|
case "tool_result": {
|
|
5928
5996
|
await ensureLiveComment();
|
|
5929
|
-
const
|
|
5997
|
+
const lastToolId = [...turn.blocks].reverse().find((b) => b.kind === "tool")?.id;
|
|
5998
|
+
const id = p.tool_use_id || lastToolId;
|
|
5930
5999
|
const entry = id ? turn.tools.get(id) : undefined;
|
|
5931
6000
|
const content = String(p.content ?? "").trim();
|
|
5932
6001
|
if (entry && content) {
|
|
5933
6002
|
entry.results.push(content);
|
|
5934
6003
|
schedulePatch();
|
|
5935
6004
|
} else if (content) {
|
|
5936
|
-
const pid = `result-${turn.
|
|
5937
|
-
|
|
5938
|
-
turn.tools.
|
|
6005
|
+
const pid = `result-${turn.tools.size}`;
|
|
6006
|
+
upsertTool(pid, { tool: "tool result" });
|
|
6007
|
+
turn.tools.get(pid).results.push(content);
|
|
5939
6008
|
schedulePatch();
|
|
5940
6009
|
}
|
|
5941
6010
|
break;
|
|
@@ -5945,7 +6014,7 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
5945
6014
|
if (p.is_error)
|
|
5946
6015
|
hadError = true;
|
|
5947
6016
|
if (!hasAssistantText && p.result) {
|
|
5948
|
-
|
|
6017
|
+
appendText(p.result);
|
|
5949
6018
|
hasAssistantText = true;
|
|
5950
6019
|
}
|
|
5951
6020
|
turn.result = { duration_ms: p.duration_ms, total_cost_usd: p.total_cost_usd, stopReason: p.stopReason, is_error: p.is_error };
|
package/package.json
CHANGED
package/src/acp-runner.ts
CHANGED
|
@@ -54,6 +54,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
54
54
|
|
|
55
55
|
let activeSessionId: string | null = opts.sessionId || null;
|
|
56
56
|
let recording = false; // only forward events after prompt() starts
|
|
57
|
+
const alwaysAllow = new Set<string>(); // keys: toolName|kind that user said "always allow"
|
|
57
58
|
|
|
58
59
|
const client: Client = {
|
|
59
60
|
async sessionUpdate(params: SessionNotification): Promise<void> {
|
|
@@ -61,7 +62,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
61
62
|
await handleSessionUpdate(params, opts);
|
|
62
63
|
},
|
|
63
64
|
async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
|
64
|
-
return await handleRequestPermission(params, opts);
|
|
65
|
+
return await handleRequestPermission(params, opts, alwaysAllow);
|
|
65
66
|
},
|
|
66
67
|
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
|
67
68
|
try {
|
|
@@ -170,8 +171,16 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
async function handleRequestPermission(params: RequestPermissionRequest, o: AcpRunOpts): Promise<RequestPermissionResponse> {
|
|
174
|
+
async function handleRequestPermission(params: RequestPermissionRequest, o: AcpRunOpts, allowCache: Set<string>): Promise<RequestPermissionResponse> {
|
|
174
175
|
const tc: any = params.toolCall || {};
|
|
176
|
+
const toolKey = `${tc.toolName || tc.title || ''}|${tc.kind || ''}`.toLowerCase();
|
|
177
|
+
|
|
178
|
+
// Auto-approve if user previously chose "always allow" for same tool/kind
|
|
179
|
+
if (toolKey && allowCache.has(toolKey)) {
|
|
180
|
+
const allowOpt = (params.options as any[]).find(op => /allow/i.test(op.kind || '') || /allow/i.test(op.name || ''));
|
|
181
|
+
if (allowOpt) return { outcome: { outcome: 'selected', optionId: allowOpt.optionId } as any };
|
|
182
|
+
}
|
|
183
|
+
|
|
175
184
|
const created = await apiClient.post<any>(`${o.apiUrl}/api/permissions`, {
|
|
176
185
|
issue_id: o.issueId,
|
|
177
186
|
device_id: o.deviceId,
|
|
@@ -183,7 +192,6 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
183
192
|
const permId = created.data?.id;
|
|
184
193
|
if (!permId) return { outcome: { outcome: 'cancelled' } as any };
|
|
185
194
|
|
|
186
|
-
// Poll every 500ms up to 5 min
|
|
187
195
|
const deadline = Date.now() + 5 * 60 * 1000;
|
|
188
196
|
while (Date.now() < deadline) {
|
|
189
197
|
await new Promise(r => setTimeout(r, 500));
|
|
@@ -191,6 +199,10 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
191
199
|
const row = got.data;
|
|
192
200
|
if (!row) break;
|
|
193
201
|
if (row.status === 'resolved' && row.chosen) {
|
|
202
|
+
// If the chosen option is an "always" variant, cache it
|
|
203
|
+
const chosenOpt = (params.options as any[]).find(op => op.optionId === row.chosen);
|
|
204
|
+
const isAlways = chosenOpt && (/always/i.test(chosenOpt.name || '') || /allow_always/i.test(chosenOpt.kind || ''));
|
|
205
|
+
if (isAlways && toolKey) allowCache.add(toolKey);
|
|
194
206
|
return { outcome: { outcome: 'selected', optionId: row.chosen } as any };
|
|
195
207
|
}
|
|
196
208
|
if (row.status === 'cancelled') {
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ const LOG_PATH = join(MULTI_DIR, 'logs', 'agent.log');
|
|
|
16
16
|
const SKILLS_DIR = join(MULTI_DIR, 'skills');
|
|
17
17
|
const STOP_PATH = join(MULTI_DIR, 'stop.flag');
|
|
18
18
|
const TASKS_DB_PATH = join(MULTI_DIR, 'tasks.db');
|
|
19
|
-
const VERSION = '0.
|
|
19
|
+
const VERSION = '0.5.1';
|
|
20
20
|
|
|
21
21
|
const COMMANDS = {
|
|
22
22
|
setup: 'Register this device with a workspace',
|
|
@@ -54,6 +54,7 @@ async function main() {
|
|
|
54
54
|
options: {
|
|
55
55
|
help: { type: 'boolean', default: false, short: 'h' },
|
|
56
56
|
version: { type: 'boolean', default: false },
|
|
57
|
+
detach: { type: 'boolean', default: false, short: 'd' },
|
|
57
58
|
name: { type: 'string' },
|
|
58
59
|
workspace: { type: 'string' },
|
|
59
60
|
agent: { type: 'string' },
|
|
@@ -80,7 +81,11 @@ async function main() {
|
|
|
80
81
|
break;
|
|
81
82
|
case 'connect':
|
|
82
83
|
case 'start':
|
|
83
|
-
|
|
84
|
+
if (args.values.detach) {
|
|
85
|
+
await cmdConnectDetached(apiUrl);
|
|
86
|
+
} else {
|
|
87
|
+
await cmdConnect(apiUrl, config);
|
|
88
|
+
}
|
|
84
89
|
break;
|
|
85
90
|
case 'link':
|
|
86
91
|
await cmdLink(apiUrl, config, args.values.agent);
|
|
@@ -117,10 +122,11 @@ Commands:
|
|
|
117
122
|
|
|
118
123
|
Options:
|
|
119
124
|
--name <name> Device name
|
|
120
|
-
--workspace <id> Workspace ID
|
|
121
125
|
--agent <id> Agent ID (for link)
|
|
122
|
-
--api <url> API URL
|
|
123
|
-
--
|
|
126
|
+
--api <url> API URL
|
|
127
|
+
-d, --detach Run connect in background (daemon)
|
|
128
|
+
-v, --version Print version
|
|
129
|
+
-h, --help Show this help
|
|
124
130
|
|
|
125
131
|
Examples:
|
|
126
132
|
multi-agent setup --name "My Mac" --workspace ws_xxx
|
|
@@ -342,6 +348,31 @@ async function cmdConnect(apiUrl: string, config: Config) {
|
|
|
342
348
|
}
|
|
343
349
|
}
|
|
344
350
|
|
|
351
|
+
async function cmdConnectDetached(apiUrl: string) {
|
|
352
|
+
if (existsSync(PID_PATH)) {
|
|
353
|
+
const pid = Number(readFileSync(PID_PATH, 'utf8').trim());
|
|
354
|
+
if (pid && isRunning(pid)) {
|
|
355
|
+
console.log(`❌ Daemon already running (pid ${pid}).`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
ensureDirs();
|
|
360
|
+
const logFd = Bun.file(LOG_PATH);
|
|
361
|
+
// Re-exec ourselves with same args minus --detach
|
|
362
|
+
const args = Bun.argv.slice(1).filter(a => a !== '-d' && a !== '--detach');
|
|
363
|
+
const proc = Bun.spawn([process.execPath, ...args, '--api', apiUrl], {
|
|
364
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
365
|
+
env: { ...process.env, MULTI_DETACHED: '1' },
|
|
366
|
+
});
|
|
367
|
+
(proc as any).unref?.();
|
|
368
|
+
// Give daemon a moment to write pidfile
|
|
369
|
+
await sleep(500);
|
|
370
|
+
console.log(`✅ Daemon started in background (pid ${proc.pid}).`);
|
|
371
|
+
console.log(` Tail logs: multi-agent logs`);
|
|
372
|
+
console.log(` Stop: multi-agent stop`);
|
|
373
|
+
process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
|
|
345
376
|
async function pickFreePort(): Promise<number> {
|
|
346
377
|
// Bind to 0, read assigned port, close immediately.
|
|
347
378
|
for (let i = 0; i < 10; i++) {
|
|
@@ -417,9 +448,9 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
417
448
|
};
|
|
418
449
|
|
|
419
450
|
type ToolEntry = { id: string; tool: string; kind?: string; status?: string; input?: any; results: string[] };
|
|
451
|
+
type Block = { kind: 'text'; text: string } | { kind: 'tool'; id: string };
|
|
420
452
|
const turn = {
|
|
421
|
-
|
|
422
|
-
toolOrder: [] as string[],
|
|
453
|
+
blocks: [] as Block[],
|
|
423
454
|
tools: new Map<string, ToolEntry>(),
|
|
424
455
|
plans: [] as string[],
|
|
425
456
|
progress: [] as string[],
|
|
@@ -427,20 +458,54 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
427
458
|
error: null as null | string,
|
|
428
459
|
};
|
|
429
460
|
|
|
461
|
+
const appendText = (text: string) => {
|
|
462
|
+
const last = turn.blocks[turn.blocks.length - 1];
|
|
463
|
+
if (last && last.kind === 'text') last.text += text;
|
|
464
|
+
else turn.blocks.push({ kind: 'text', text });
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const upsertTool = (id: string, patch: Partial<ToolEntry>) => {
|
|
468
|
+
const existing = turn.tools.get(id);
|
|
469
|
+
if (existing) {
|
|
470
|
+
if (patch.tool && patch.tool !== 'tool') existing.tool = patch.tool;
|
|
471
|
+
if (patch.kind) existing.kind = patch.kind;
|
|
472
|
+
if (patch.status) existing.status = patch.status;
|
|
473
|
+
if (patch.input !== undefined) existing.input = patch.input;
|
|
474
|
+
} else {
|
|
475
|
+
turn.tools.set(id, { id, tool: patch.tool || 'tool', kind: patch.kind, status: patch.status, input: patch.input, results: [] });
|
|
476
|
+
turn.blocks.push({ kind: 'tool', id });
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const toolLabel = (t: ToolEntry): string => {
|
|
481
|
+
const clean = stripMd(t.tool);
|
|
482
|
+
if (clean && clean !== 'tool') return clean;
|
|
483
|
+
// Fallback from input shape
|
|
484
|
+
if (t.input && typeof t.input === 'object') {
|
|
485
|
+
if (t.input.command) return `Bash: ${String(t.input.command).split('\n')[0].slice(0, 60)}`;
|
|
486
|
+
if (t.input.file_path) return `${t.kind === 'edit' ? 'Edit' : 'Read'}: ${t.input.file_path}`;
|
|
487
|
+
if (t.input.pattern) return `Grep: ${t.input.pattern}`;
|
|
488
|
+
if (t.input.url) return `Fetch: ${t.input.url}`;
|
|
489
|
+
}
|
|
490
|
+
return (t.kind ? `${t.kind}` : 'tool');
|
|
491
|
+
};
|
|
492
|
+
|
|
430
493
|
const render = (): string => {
|
|
431
494
|
const parts: string[] = [];
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
495
|
+
for (const b of turn.blocks) {
|
|
496
|
+
if (b.kind === 'text') {
|
|
497
|
+
if (b.text) parts.push(b.text);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const t = turn.tools.get(b.id);
|
|
436
501
|
if (!t) continue;
|
|
437
502
|
const icon = statusIcon(t.status);
|
|
438
|
-
const
|
|
439
|
-
const head = `${icon} ${
|
|
503
|
+
const label = toolLabel(t);
|
|
504
|
+
const head = `${icon} ${label}${t.status && t.status !== 'completed' ? ` · ${t.status}` : ''}`;
|
|
440
505
|
const body: string[] = [];
|
|
441
506
|
if (t.input !== undefined && t.input !== null) {
|
|
442
507
|
const inputStr = typeof t.input === 'object' ? JSON.stringify(t.input, null, 2) : String(t.input);
|
|
443
|
-
body.push('```json\n' + inputStr + '\n```');
|
|
508
|
+
body.push('```json\n' + inputStr.slice(0, 2000) + '\n```');
|
|
444
509
|
}
|
|
445
510
|
if (t.results.length) {
|
|
446
511
|
const joined = t.results.join('\n').slice(-2000);
|
|
@@ -450,7 +515,6 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
450
515
|
}
|
|
451
516
|
|
|
452
517
|
if (turn.plans.length) parts.push('**Plan**\n\n' + turn.plans[turn.plans.length - 1]);
|
|
453
|
-
|
|
454
518
|
if (turn.error) parts.push(`> ❌ ${turn.error}`);
|
|
455
519
|
|
|
456
520
|
if (turn.result) {
|
|
@@ -486,16 +550,15 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
486
550
|
}
|
|
487
551
|
case 'assistant_text': {
|
|
488
552
|
await ensureLiveComment();
|
|
489
|
-
|
|
553
|
+
appendText(p.text);
|
|
490
554
|
hasAssistantText = true;
|
|
491
555
|
schedulePatch();
|
|
492
556
|
break;
|
|
493
557
|
}
|
|
494
558
|
case 'stdout': {
|
|
495
|
-
// Legacy (non-ACP) runners emit stdout — route to live comment too.
|
|
496
559
|
if (p.line) {
|
|
497
560
|
await ensureLiveComment();
|
|
498
|
-
|
|
561
|
+
appendText((turn.blocks.length ? '\n' : '') + p.line);
|
|
499
562
|
hasAssistantText = true;
|
|
500
563
|
schedulePatch();
|
|
501
564
|
}
|
|
@@ -503,33 +566,24 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
503
566
|
}
|
|
504
567
|
case 'tool_call': {
|
|
505
568
|
await ensureLiveComment();
|
|
506
|
-
const id = p.id || `anon-${turn.
|
|
507
|
-
|
|
508
|
-
if (existing) {
|
|
509
|
-
if (p.tool) existing.tool = p.tool;
|
|
510
|
-
if (p.kind) existing.kind = p.kind;
|
|
511
|
-
if (p.status) existing.status = p.status;
|
|
512
|
-
if (p.input !== undefined) existing.input = p.input;
|
|
513
|
-
} else {
|
|
514
|
-
turn.toolOrder.push(id);
|
|
515
|
-
turn.tools.set(id, { id, tool: p.tool || 'tool', kind: p.kind, status: p.status, input: p.input, results: [] });
|
|
516
|
-
}
|
|
569
|
+
const id = p.id || `anon-${turn.tools.size}`;
|
|
570
|
+
upsertTool(id, { tool: p.tool, kind: p.kind, status: p.status, input: p.input });
|
|
517
571
|
schedulePatch();
|
|
518
572
|
break;
|
|
519
573
|
}
|
|
520
574
|
case 'tool_result': {
|
|
521
575
|
await ensureLiveComment();
|
|
522
|
-
const
|
|
576
|
+
const lastToolId = [...turn.blocks].reverse().find(b => b.kind === 'tool')?.id;
|
|
577
|
+
const id = p.tool_use_id || lastToolId;
|
|
523
578
|
const entry = id ? turn.tools.get(id) : undefined;
|
|
524
579
|
const content = String(p.content ?? '').trim();
|
|
525
580
|
if (entry && content) {
|
|
526
581
|
entry.results.push(content);
|
|
527
582
|
schedulePatch();
|
|
528
583
|
} else if (content) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
turn.
|
|
532
|
-
turn.tools.set(pid, { id: pid, tool: 'tool result', results: [content] });
|
|
584
|
+
const pid = `result-${turn.tools.size}`;
|
|
585
|
+
upsertTool(pid, { tool: 'tool result' });
|
|
586
|
+
turn.tools.get(pid)!.results.push(content);
|
|
533
587
|
schedulePatch();
|
|
534
588
|
}
|
|
535
589
|
break;
|
|
@@ -538,7 +592,7 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
538
592
|
await ensureLiveComment();
|
|
539
593
|
if (p.is_error) hadError = true;
|
|
540
594
|
if (!hasAssistantText && p.result) {
|
|
541
|
-
|
|
595
|
+
appendText(p.result);
|
|
542
596
|
hasAssistantText = true;
|
|
543
597
|
}
|
|
544
598
|
turn.result = { duration_ms: p.duration_ms, total_cost_usd: p.total_cost_usd, stopReason: p.stopReason, is_error: p.is_error };
|