@shipers-dev/multi 0.5.1 → 0.6.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 +208 -10
- package/package.json +1 -1
- package/src/acpx-runner.ts +136 -0
- package/src/index.ts +40 -8
package/dist/index.js
CHANGED
|
@@ -5380,6 +5380,145 @@ function extractText(content) {
|
|
|
5380
5380
|
return "";
|
|
5381
5381
|
}
|
|
5382
5382
|
|
|
5383
|
+
// src/acpx-runner.ts
|
|
5384
|
+
async function runAcpx(opts) {
|
|
5385
|
+
const args = ["acpx", "--format", "json", "--json-strict", "--approve-all"];
|
|
5386
|
+
if (opts.cwd)
|
|
5387
|
+
args.push("--cwd", opts.cwd);
|
|
5388
|
+
args.push(opts.agentType);
|
|
5389
|
+
if (opts.sessionName) {
|
|
5390
|
+
const ensureArgs = ["acpx", ...opts.cwd ? ["--cwd", opts.cwd] : [], opts.agentType, "sessions", "ensure", "--name", opts.sessionName];
|
|
5391
|
+
try {
|
|
5392
|
+
const ensure = Bun.spawn(ensureArgs, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
5393
|
+
await ensure.exited;
|
|
5394
|
+
} catch {}
|
|
5395
|
+
args.push("prompt", "-s", opts.sessionName);
|
|
5396
|
+
} else {
|
|
5397
|
+
args.push("prompt");
|
|
5398
|
+
}
|
|
5399
|
+
args.push(opts.prompt);
|
|
5400
|
+
const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
5401
|
+
let stopReason = "end_turn";
|
|
5402
|
+
const reader = proc.stdout.getReader();
|
|
5403
|
+
const dec = new TextDecoder;
|
|
5404
|
+
let buf = "";
|
|
5405
|
+
while (true) {
|
|
5406
|
+
const { value, done } = await reader.read();
|
|
5407
|
+
if (done)
|
|
5408
|
+
break;
|
|
5409
|
+
buf += dec.decode(value, { stream: true });
|
|
5410
|
+
let nl;
|
|
5411
|
+
while ((nl = buf.indexOf(`
|
|
5412
|
+
`)) !== -1) {
|
|
5413
|
+
const line = buf.slice(0, nl).trim();
|
|
5414
|
+
buf = buf.slice(nl + 1);
|
|
5415
|
+
if (!line)
|
|
5416
|
+
continue;
|
|
5417
|
+
const events = parseAcpLine(line, (r) => {
|
|
5418
|
+
stopReason = r;
|
|
5419
|
+
});
|
|
5420
|
+
for (const ev of events)
|
|
5421
|
+
await opts.onEvent(ev);
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
if (buf.trim()) {
|
|
5425
|
+
const events = parseAcpLine(buf.trim(), (r) => {
|
|
5426
|
+
stopReason = r;
|
|
5427
|
+
});
|
|
5428
|
+
for (const ev of events)
|
|
5429
|
+
await opts.onEvent(ev);
|
|
5430
|
+
}
|
|
5431
|
+
const code = await proc.exited;
|
|
5432
|
+
if (code !== 0 && code !== 130) {
|
|
5433
|
+
await opts.onEvent({ event_type: "error", payload: { message: `acpx exited with code ${code}` } });
|
|
5434
|
+
}
|
|
5435
|
+
return { stopReason };
|
|
5436
|
+
}
|
|
5437
|
+
function parseAcpLine(line, setStop) {
|
|
5438
|
+
let msg;
|
|
5439
|
+
try {
|
|
5440
|
+
msg = JSON.parse(line);
|
|
5441
|
+
} catch {
|
|
5442
|
+
return [];
|
|
5443
|
+
}
|
|
5444
|
+
const out = [];
|
|
5445
|
+
if (msg.method === "session/update" && msg.params?.sessionUpdate) {
|
|
5446
|
+
return handleSessionUpdate(msg.params);
|
|
5447
|
+
}
|
|
5448
|
+
if (msg.id && msg.result && typeof msg.result === "object" && "stopReason" in msg.result) {
|
|
5449
|
+
setStop(msg.result.stopReason);
|
|
5450
|
+
out.push({ event_type: "result", payload: { stopReason: msg.result.stopReason } });
|
|
5451
|
+
return out;
|
|
5452
|
+
}
|
|
5453
|
+
if (msg.error) {
|
|
5454
|
+
out.push({ event_type: "error", payload: { message: msg.error.message || JSON.stringify(msg.error) } });
|
|
5455
|
+
return out;
|
|
5456
|
+
}
|
|
5457
|
+
return out;
|
|
5458
|
+
}
|
|
5459
|
+
function handleSessionUpdate(params) {
|
|
5460
|
+
const u = params.update || {};
|
|
5461
|
+
const kind = u.sessionUpdate;
|
|
5462
|
+
const out = [];
|
|
5463
|
+
switch (kind) {
|
|
5464
|
+
case "agent_message_chunk":
|
|
5465
|
+
case "agent_thought_chunk": {
|
|
5466
|
+
const text = extractText2(u.content);
|
|
5467
|
+
if (text)
|
|
5468
|
+
out.push({ event_type: "assistant_text", payload: { text } });
|
|
5469
|
+
break;
|
|
5470
|
+
}
|
|
5471
|
+
case "tool_call":
|
|
5472
|
+
case "tool_call_update": {
|
|
5473
|
+
out.push({ event_type: "tool_call", payload: {
|
|
5474
|
+
id: u.toolCallId || u.id,
|
|
5475
|
+
tool: u.title || u.toolName || "tool",
|
|
5476
|
+
kind: u.kind,
|
|
5477
|
+
status: u.status,
|
|
5478
|
+
input: u.rawInput,
|
|
5479
|
+
locations: u.locations
|
|
5480
|
+
} });
|
|
5481
|
+
if (Array.isArray(u.content)) {
|
|
5482
|
+
for (const c of u.content) {
|
|
5483
|
+
if (c.type === "content" && c.content?.type === "text") {
|
|
5484
|
+
out.push({ event_type: "tool_result", payload: { tool_use_id: u.toolCallId || u.id, content: String(c.content.text).slice(0, 4000) } });
|
|
5485
|
+
} else if (c.type === "diff") {
|
|
5486
|
+
const diff = `--- ${c.path}
|
|
5487
|
+
${c.oldText || ""}
|
|
5488
|
+
+++ ${c.path}
|
|
5489
|
+
${c.newText || ""}`.slice(0, 4000);
|
|
5490
|
+
out.push({ event_type: "tool_result", payload: { tool_use_id: u.toolCallId || u.id, content: diff } });
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
break;
|
|
5495
|
+
}
|
|
5496
|
+
case "plan": {
|
|
5497
|
+
const entries = (u.entries || []).map((e, i) => `${i + 1}. [${e.status || "pending"}] ${e.content}`).join(`
|
|
5498
|
+
`);
|
|
5499
|
+
if (entries)
|
|
5500
|
+
out.push({ event_type: "assistant_text", payload: { text: `**Plan**
|
|
5501
|
+
|
|
5502
|
+
${entries}` } });
|
|
5503
|
+
break;
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
return out;
|
|
5507
|
+
}
|
|
5508
|
+
function extractText2(content) {
|
|
5509
|
+
if (!content)
|
|
5510
|
+
return "";
|
|
5511
|
+
if (typeof content === "string")
|
|
5512
|
+
return content;
|
|
5513
|
+
if (Array.isArray(content))
|
|
5514
|
+
return content.map(extractText2).join("");
|
|
5515
|
+
if (content.type === "text" && typeof content.text === "string")
|
|
5516
|
+
return content.text;
|
|
5517
|
+
if (content.text)
|
|
5518
|
+
return content.text;
|
|
5519
|
+
return "";
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5383
5522
|
// src/index.ts
|
|
5384
5523
|
import { parseArgs } from "util";
|
|
5385
5524
|
import { mkdirSync as mkdirSync2, existsSync as existsSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, appendFileSync, unlinkSync, readdirSync, statSync } from "fs";
|
|
@@ -5392,7 +5531,7 @@ var LOG_PATH = join(MULTI_DIR, "logs", "agent.log");
|
|
|
5392
5531
|
var SKILLS_DIR = join(MULTI_DIR, "skills");
|
|
5393
5532
|
var STOP_PATH = join(MULTI_DIR, "stop.flag");
|
|
5394
5533
|
var TASKS_DB_PATH = join(MULTI_DIR, "tasks.db");
|
|
5395
|
-
var VERSION = "0.
|
|
5534
|
+
var VERSION = "0.6.1";
|
|
5396
5535
|
var COMMANDS = {
|
|
5397
5536
|
setup: "Register this device with a workspace",
|
|
5398
5537
|
connect: "Connect device to realtime hub and execute assigned tasks",
|
|
@@ -6037,6 +6176,7 @@ _${bits.join(" \xB7 ")}_`);
|
|
|
6037
6176
|
} catch {}
|
|
6038
6177
|
const acpCapable = detected.filter((d) => d.type === "claude-code");
|
|
6039
6178
|
const useAcp = preferType !== "pi" && acpCapable.length > 0 && process.env.MULTI_LEGACY !== "1";
|
|
6179
|
+
const useAcpx = !useAcp && preferType && ["pi", "codex", "openclaw"].includes(preferType) && process.env.MULTI_LEGACY !== "1";
|
|
6040
6180
|
try {
|
|
6041
6181
|
if (useAcp) {
|
|
6042
6182
|
const base = `${task.title}
|
|
@@ -6053,15 +6193,15 @@ Context (original task ${task.key}): ${task.title}` : base || task.title;
|
|
|
6053
6193
|
userPart += `
|
|
6054
6194
|
|
|
6055
6195
|
---
|
|
6056
|
-
Attached files (
|
|
6196
|
+
Attached input files (read them with your tools if useful):
|
|
6057
6197
|
${lines}
|
|
6058
6198
|
|
|
6059
|
-
|
|
6199
|
+
Note: if (and only if) you produce binary or large artifact outputs (screenshots, data exports, generated source files), write them under ${outDir}. Always put your human-facing reply in the chat response itself \u2014 do NOT write your answer as a file.`;
|
|
6060
6200
|
} else {
|
|
6061
6201
|
userPart += `
|
|
6062
6202
|
|
|
6063
6203
|
---
|
|
6064
|
-
|
|
6204
|
+
Respond in the chat. Only if you produce large artifact files (screenshots, data exports, generated source code), write them under ${outDir}. Do not put your answer in a file.`;
|
|
6065
6205
|
}
|
|
6066
6206
|
let preamble = "";
|
|
6067
6207
|
try {
|
|
@@ -6124,14 +6264,72 @@ ${userPart}` : userPart;
|
|
|
6124
6264
|
}
|
|
6125
6265
|
});
|
|
6126
6266
|
log(` acp session ${sessionId.slice(0, 8)}`);
|
|
6127
|
-
} else {
|
|
6128
|
-
let
|
|
6267
|
+
} else if (useAcpx) {
|
|
6268
|
+
let preamble = "";
|
|
6129
6269
|
try {
|
|
6130
|
-
const
|
|
6131
|
-
|
|
6132
|
-
|
|
6270
|
+
const agentRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}`);
|
|
6271
|
+
const agent = agentRes.data;
|
|
6272
|
+
if (agent?.prompt)
|
|
6273
|
+
preamble += `# Agent instructions
|
|
6274
|
+
|
|
6275
|
+
${agent.prompt}
|
|
6276
|
+
|
|
6277
|
+
`;
|
|
6278
|
+
const skillsRes = await apiClient.get(`${apiUrl}/api/agents/${task.agent_id}/skills`);
|
|
6279
|
+
const skillList = Array.isArray(skillsRes.data) ? skillsRes.data : [];
|
|
6280
|
+
if (skillList.length) {
|
|
6281
|
+
preamble += `# Attached skills (${skillList.length})
|
|
6282
|
+
|
|
6283
|
+
`;
|
|
6284
|
+
for (const s of skillList) {
|
|
6285
|
+
const body = String(s.body || s.description || "").trim();
|
|
6286
|
+
if (body)
|
|
6287
|
+
preamble += `## ${s.name}${s.version ? ` v${s.version}` : ""}
|
|
6288
|
+
|
|
6289
|
+
${body}
|
|
6290
|
+
|
|
6291
|
+
`;
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6133
6294
|
} catch {}
|
|
6134
|
-
const
|
|
6295
|
+
const base = `${task.title}
|
|
6296
|
+
|
|
6297
|
+
${task.description || ""}`.trim();
|
|
6298
|
+
let userPart = task.followup ? `${task.followup}
|
|
6299
|
+
|
|
6300
|
+
---
|
|
6301
|
+
Context (${task.key}): ${task.title}` : base || task.title;
|
|
6302
|
+
userPart = stripSelfMention(userPart, preferType);
|
|
6303
|
+
if (attachmentRefs.length) {
|
|
6304
|
+
const lines = attachmentRefs.map((a) => `- ${a.filename}: ${a.path}`).join(`
|
|
6305
|
+
`);
|
|
6306
|
+
userPart += `
|
|
6307
|
+
|
|
6308
|
+
---
|
|
6309
|
+
Attached files:
|
|
6310
|
+
${lines}
|
|
6311
|
+
|
|
6312
|
+
Write generated files to: ${outDir}`;
|
|
6313
|
+
} else {
|
|
6314
|
+
userPart += `
|
|
6315
|
+
|
|
6316
|
+
---
|
|
6317
|
+
Write generated files to: ${outDir}`;
|
|
6318
|
+
}
|
|
6319
|
+
const full = preamble ? `${preamble}
|
|
6320
|
+
---
|
|
6321
|
+
|
|
6322
|
+
${userPart}` : userPart;
|
|
6323
|
+
log(` acpx runner: ${preferType}`);
|
|
6324
|
+
await runAcpx({
|
|
6325
|
+
agentType: preferType,
|
|
6326
|
+
prompt: full,
|
|
6327
|
+
cwd: workingDir,
|
|
6328
|
+
sessionName: `issue-${issueId}`,
|
|
6329
|
+
onEvent: eventHandler
|
|
6330
|
+
});
|
|
6331
|
+
} else {
|
|
6332
|
+
const runner = pickRunner(detected, preferType);
|
|
6135
6333
|
for await (const event of runner(task))
|
|
6136
6334
|
await eventHandler(event);
|
|
6137
6335
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// acpx-runner — delegate to `acpx` CLI for pi/codex/openclaw agents.
|
|
2
|
+
// acpx emits raw ACP JSON-RPC NDJSON on stdout with --format json; we reuse the
|
|
3
|
+
// same parse semantics as our direct ACP runner to produce AcpEvent stream.
|
|
4
|
+
//
|
|
5
|
+
// Requires: `npm install -g acpx` on device host (or npx acpx@latest).
|
|
6
|
+
// Runs with --approve-all for now (no permission UI forwarding).
|
|
7
|
+
|
|
8
|
+
import type { AcpEvent } from './acp-runner';
|
|
9
|
+
|
|
10
|
+
export interface AcpxRunOpts {
|
|
11
|
+
agentType: 'pi' | 'codex' | 'openclaw' | 'claude' | string;
|
|
12
|
+
prompt: string;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
sessionName?: string; // reuse same session across follow-ups (e.g. "issue-<id>")
|
|
15
|
+
onEvent: (ev: AcpEvent) => void | Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runAcpx(opts: AcpxRunOpts): Promise<{ stopReason: string }> {
|
|
19
|
+
const args = ['acpx', '--format', 'json', '--json-strict', '--approve-all'];
|
|
20
|
+
if (opts.cwd) args.push('--cwd', opts.cwd);
|
|
21
|
+
args.push(opts.agentType);
|
|
22
|
+
// Ensure a session exists for this name (idempotent). ensure = get-or-create.
|
|
23
|
+
// We run it as a separate invocation, then prompt.
|
|
24
|
+
if (opts.sessionName) {
|
|
25
|
+
const ensureArgs = ['acpx', ...(opts.cwd ? ['--cwd', opts.cwd] : []), opts.agentType, 'sessions', 'ensure', '--name', opts.sessionName];
|
|
26
|
+
try {
|
|
27
|
+
const ensure = Bun.spawn(ensureArgs, { stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' });
|
|
28
|
+
await ensure.exited;
|
|
29
|
+
} catch {}
|
|
30
|
+
args.push('prompt', '-s', opts.sessionName);
|
|
31
|
+
} else {
|
|
32
|
+
args.push('prompt');
|
|
33
|
+
}
|
|
34
|
+
args.push(opts.prompt);
|
|
35
|
+
|
|
36
|
+
const proc = Bun.spawn(args, { stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' });
|
|
37
|
+
let stopReason = 'end_turn';
|
|
38
|
+
|
|
39
|
+
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
|
|
40
|
+
const dec = new TextDecoder();
|
|
41
|
+
let buf = '';
|
|
42
|
+
|
|
43
|
+
while (true) {
|
|
44
|
+
const { value, done } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
buf += dec.decode(value, { stream: true });
|
|
47
|
+
let nl: number;
|
|
48
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
49
|
+
const line = buf.slice(0, nl).trim();
|
|
50
|
+
buf = buf.slice(nl + 1);
|
|
51
|
+
if (!line) continue;
|
|
52
|
+
const events = parseAcpLine(line, (r) => { stopReason = r; });
|
|
53
|
+
for (const ev of events) await opts.onEvent(ev);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (buf.trim()) {
|
|
57
|
+
const events = parseAcpLine(buf.trim(), (r) => { stopReason = r; });
|
|
58
|
+
for (const ev of events) await opts.onEvent(ev);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const code = await proc.exited;
|
|
62
|
+
if (code !== 0 && code !== 130) {
|
|
63
|
+
await opts.onEvent({ event_type: 'error', payload: { message: `acpx exited with code ${code}` } });
|
|
64
|
+
}
|
|
65
|
+
return { stopReason };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseAcpLine(line: string, setStop: (r: string) => void): AcpEvent[] {
|
|
69
|
+
let msg: any;
|
|
70
|
+
try { msg = JSON.parse(line); } catch { return []; }
|
|
71
|
+
const out: AcpEvent[] = [];
|
|
72
|
+
|
|
73
|
+
// session/update notification
|
|
74
|
+
if (msg.method === 'session/update' && msg.params?.sessionUpdate) {
|
|
75
|
+
return handleSessionUpdate(msg.params);
|
|
76
|
+
}
|
|
77
|
+
// prompt result
|
|
78
|
+
if (msg.id && msg.result && typeof msg.result === 'object' && 'stopReason' in msg.result) {
|
|
79
|
+
setStop(msg.result.stopReason);
|
|
80
|
+
out.push({ event_type: 'result', payload: { stopReason: msg.result.stopReason } });
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
// errors
|
|
84
|
+
if (msg.error) {
|
|
85
|
+
out.push({ event_type: 'error', payload: { message: msg.error.message || JSON.stringify(msg.error) } });
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleSessionUpdate(params: any): AcpEvent[] {
|
|
92
|
+
const u = params.update || {};
|
|
93
|
+
const kind = u.sessionUpdate;
|
|
94
|
+
const out: AcpEvent[] = [];
|
|
95
|
+
switch (kind) {
|
|
96
|
+
case 'agent_message_chunk':
|
|
97
|
+
case 'agent_thought_chunk': {
|
|
98
|
+
const text = extractText(u.content);
|
|
99
|
+
if (text) out.push({ event_type: 'assistant_text', payload: { text } });
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case 'tool_call':
|
|
103
|
+
case 'tool_call_update': {
|
|
104
|
+
out.push({ event_type: 'tool_call', payload: {
|
|
105
|
+
id: u.toolCallId || u.id, tool: u.title || u.toolName || 'tool',
|
|
106
|
+
kind: u.kind, status: u.status, input: u.rawInput, locations: u.locations,
|
|
107
|
+
}});
|
|
108
|
+
if (Array.isArray(u.content)) {
|
|
109
|
+
for (const c of u.content) {
|
|
110
|
+
if (c.type === 'content' && c.content?.type === 'text') {
|
|
111
|
+
out.push({ event_type: 'tool_result', payload: { tool_use_id: u.toolCallId || u.id, content: String(c.content.text).slice(0, 4000) } });
|
|
112
|
+
} else if (c.type === 'diff') {
|
|
113
|
+
const diff = `--- ${c.path}\n${c.oldText || ''}\n+++ ${c.path}\n${c.newText || ''}`.slice(0, 4000);
|
|
114
|
+
out.push({ event_type: 'tool_result', payload: { tool_use_id: u.toolCallId || u.id, content: diff } });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'plan': {
|
|
121
|
+
const entries = (u.entries || []).map((e: any, i: number) => `${i + 1}. [${e.status || 'pending'}] ${e.content}`).join('\n');
|
|
122
|
+
if (entries) out.push({ event_type: 'assistant_text', payload: { text: `**Plan**\n\n${entries}` } });
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractText(content: any): string {
|
|
130
|
+
if (!content) return '';
|
|
131
|
+
if (typeof content === 'string') return content;
|
|
132
|
+
if (Array.isArray(content)) return content.map(extractText).join('');
|
|
133
|
+
if (content.type === 'text' && typeof content.text === 'string') return content.text;
|
|
134
|
+
if (content.text) return content.text;
|
|
135
|
+
return '';
|
|
136
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { detectAgents } from './detect';
|
|
|
4
4
|
import { apiClient, setAuthToken } from './client';
|
|
5
5
|
import { Database } from 'bun:sqlite';
|
|
6
6
|
import { runAcp } from './acp-runner';
|
|
7
|
+
import { runAcpx } from './acpx-runner';
|
|
7
8
|
import { parseArgs } from 'util';
|
|
8
9
|
import { mkdirSync, existsSync, writeFileSync, readFileSync, appendFileSync, unlinkSync, readdirSync, statSync } from 'fs';
|
|
9
10
|
import { join, dirname } from 'path';
|
|
@@ -16,7 +17,7 @@ const LOG_PATH = join(MULTI_DIR, 'logs', 'agent.log');
|
|
|
16
17
|
const SKILLS_DIR = join(MULTI_DIR, 'skills');
|
|
17
18
|
const STOP_PATH = join(MULTI_DIR, 'stop.flag');
|
|
18
19
|
const TASKS_DB_PATH = join(MULTI_DIR, 'tasks.db');
|
|
19
|
-
const VERSION = '0.
|
|
20
|
+
const VERSION = '0.6.1';
|
|
20
21
|
|
|
21
22
|
const COMMANDS = {
|
|
22
23
|
setup: 'Register this device with a workspace',
|
|
@@ -616,6 +617,8 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
616
617
|
} catch {}
|
|
617
618
|
const acpCapable = detected.filter(d => d.type === 'claude-code');
|
|
618
619
|
const useAcp = preferType !== 'pi' && acpCapable.length > 0 && process.env.MULTI_LEGACY !== '1';
|
|
620
|
+
// Route pi/codex/openclaw through acpx (handles these agent types natively)
|
|
621
|
+
const useAcpx = !useAcp && preferType && ['pi', 'codex', 'openclaw'].includes(preferType) && process.env.MULTI_LEGACY !== '1';
|
|
619
622
|
|
|
620
623
|
try {
|
|
621
624
|
if (useAcp) {
|
|
@@ -626,9 +629,9 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
626
629
|
userPart = stripSelfMention(userPart, preferType);
|
|
627
630
|
if (attachmentRefs.length) {
|
|
628
631
|
const lines = attachmentRefs.map(a => `- ${a.filename}: ${a.path}`).join('\n');
|
|
629
|
-
userPart += `\n\n---\nAttached files (
|
|
632
|
+
userPart += `\n\n---\nAttached input files (read them with your tools if useful):\n${lines}\n\nNote: if (and only if) you produce binary or large artifact outputs (screenshots, data exports, generated source files), write them under ${outDir}. Always put your human-facing reply in the chat response itself — do NOT write your answer as a file.`;
|
|
630
633
|
} else {
|
|
631
|
-
userPart += `\n\n---\
|
|
634
|
+
userPart += `\n\n---\nRespond in the chat. Only if you produce large artifact files (screenshots, data exports, generated source code), write them under ${outDir}. Do not put your answer in a file.`;
|
|
632
635
|
}
|
|
633
636
|
|
|
634
637
|
// Pull agent + linked skills to construct system/context preamble
|
|
@@ -676,13 +679,42 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
676
679
|
},
|
|
677
680
|
});
|
|
678
681
|
log(` acp session ${sessionId.slice(0, 8)}`);
|
|
679
|
-
} else {
|
|
680
|
-
//
|
|
681
|
-
let
|
|
682
|
+
} else if (useAcpx) {
|
|
683
|
+
// Build prompt with preamble (same logic as ACP path, but as one string)
|
|
684
|
+
let preamble = '';
|
|
682
685
|
try {
|
|
683
|
-
const
|
|
684
|
-
|
|
686
|
+
const agentRes = await apiClient.get<any>(`${apiUrl}/api/agents/${task.agent_id}`);
|
|
687
|
+
const agent = agentRes.data;
|
|
688
|
+
if (agent?.prompt) preamble += `# Agent instructions\n\n${agent.prompt}\n\n`;
|
|
689
|
+
const skillsRes = await apiClient.get<any[]>(`${apiUrl}/api/agents/${task.agent_id}/skills`);
|
|
690
|
+
const skillList = Array.isArray(skillsRes.data) ? skillsRes.data : [];
|
|
691
|
+
if (skillList.length) {
|
|
692
|
+
preamble += `# Attached skills (${skillList.length})\n\n`;
|
|
693
|
+
for (const s of skillList) {
|
|
694
|
+
const body = String(s.body || s.description || '').trim();
|
|
695
|
+
if (body) preamble += `## ${s.name}${s.version ? ` v${s.version}` : ''}\n\n${body}\n\n`;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
685
698
|
} catch {}
|
|
699
|
+
const base = `${task.title}\n\n${task.description || ''}`.trim();
|
|
700
|
+
let userPart = task.followup ? `${task.followup}\n\n---\nContext (${task.key}): ${task.title}` : (base || task.title);
|
|
701
|
+
userPart = stripSelfMention(userPart, preferType);
|
|
702
|
+
if (attachmentRefs.length) {
|
|
703
|
+
const lines = attachmentRefs.map(a => `- ${a.filename}: ${a.path}`).join('\n');
|
|
704
|
+
userPart += `\n\n---\nAttached files:\n${lines}\n\nWrite generated files to: ${outDir}`;
|
|
705
|
+
} else {
|
|
706
|
+
userPart += `\n\n---\nWrite generated files to: ${outDir}`;
|
|
707
|
+
}
|
|
708
|
+
const full = preamble ? `${preamble}\n---\n\n${userPart}` : userPart;
|
|
709
|
+
log(` acpx runner: ${preferType}`);
|
|
710
|
+
await runAcpx({
|
|
711
|
+
agentType: preferType!,
|
|
712
|
+
prompt: full,
|
|
713
|
+
cwd: workingDir,
|
|
714
|
+
sessionName: `issue-${issueId}`,
|
|
715
|
+
onEvent: eventHandler,
|
|
716
|
+
});
|
|
717
|
+
} else {
|
|
686
718
|
const runner = pickRunner(detected, preferType);
|
|
687
719
|
for await (const event of runner(task)) await eventHandler(event);
|
|
688
720
|
}
|