@shipers-dev/multi 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16272,18 +16272,23 @@ import { join as join4, dirname as dirname3 } from "path";
16272
16272
  // package.json
16273
16273
  var package_default = {
16274
16274
  name: "@shipers-dev/multi",
16275
- version: "0.12.0",
16275
+ version: "0.13.0",
16276
16276
  type: "module",
16277
16277
  bin: {
16278
16278
  "multi-agent": "./dist/index.js"
16279
16279
  },
16280
+ files: [
16281
+ "dist"
16282
+ ],
16280
16283
  scripts: {
16281
16284
  dev: "bun run src/index.ts",
16282
16285
  build: "bun build src/index.ts --outdir=dist --target=bun"
16283
16286
  },
16284
16287
  dependencies: {
16285
16288
  "@agentclientprotocol/sdk": "^0.20.0",
16286
- "@agentclientprotocol/claude-agent-acp": "^0.31.0",
16289
+ "@agentclientprotocol/claude-agent-acp": "^0.31.0"
16290
+ },
16291
+ devDependencies: {
16287
16292
  "@multi/lib": "workspace:*"
16288
16293
  }
16289
16294
  };
@@ -16710,7 +16715,7 @@ async function cmdConnect(apiUrl, config2) {
16710
16715
  try {
16711
16716
  writeFileSync4(PORT_PATH, String(port));
16712
16717
  } catch {}
16713
- let tunnel = await startTunnel(port);
16718
+ let tunnel = await startTunnel(port, log);
16714
16719
  if (!tunnel) {
16715
16720
  log("\u274C cloudflared did not emit a tunnel URL \u2014 is `cloudflared` installed? (`brew install cloudflared`)");
16716
16721
  try {
@@ -16780,7 +16785,7 @@ async function cmdConnect(apiUrl, config2) {
16780
16785
  old?.child.kill();
16781
16786
  } catch {}
16782
16787
  for (let attempt = 1;alive; attempt++) {
16783
- const next = await startTunnel(port);
16788
+ const next = await startTunnel(port, log);
16784
16789
  if (next) {
16785
16790
  tunnel = next;
16786
16791
  log(`\u2601\uFE0F Tunnel up: ${tunnel.url}`);
@@ -16849,21 +16854,77 @@ async function cmdConnect(apiUrl, config2) {
16849
16854
  }
16850
16855
  }
16851
16856
  }
16852
- async function startTunnel(port) {
16857
+ async function startTunnel(port, log2 = () => {}) {
16858
+ const named = process.env.MULTI_TUNNEL_NAME?.trim();
16859
+ const hostname3 = process.env.MULTI_TUNNEL_HOSTNAME?.trim();
16860
+ if (named) {
16861
+ if (!hostname3) {
16862
+ log2("\u274C MULTI_TUNNEL_NAME set but MULTI_TUNNEL_HOSTNAME missing \u2014 set the public hostname routed to this tunnel");
16863
+ return null;
16864
+ }
16865
+ const args = ["tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${port}`, "run", named];
16866
+ const child2 = Bun.spawn(["cloudflared", ...args], { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
16867
+ const ok = await waitNamedTunnelReady(child2.stderr);
16868
+ if (!ok.ready) {
16869
+ try {
16870
+ child2.kill();
16871
+ } catch {}
16872
+ if (ok.tail) {
16873
+ const lines = ok.tail.split(`
16874
+ `).filter(Boolean).slice(-6);
16875
+ for (const l of lines)
16876
+ log2(` cloudflared: ${l}`);
16877
+ }
16878
+ return null;
16879
+ }
16880
+ const url3 = hostname3.startsWith("http") ? hostname3.replace(/\/+$/, "") : `https://${hostname3}`;
16881
+ return { child: child2, url: url3 };
16882
+ }
16853
16883
  const child = Bun.spawn(["cloudflared", "tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${port}`], {
16854
16884
  stdout: "pipe",
16855
16885
  stderr: "pipe",
16856
16886
  stdin: "ignore"
16857
16887
  });
16858
- const url2 = await parseTunnelUrl(child.stderr);
16888
+ const { url: url2, tail } = await parseTunnelUrl(child.stderr);
16859
16889
  if (!url2) {
16860
16890
  try {
16861
16891
  child.kill();
16862
16892
  } catch {}
16893
+ if (tail) {
16894
+ const lines = tail.split(`
16895
+ `).filter(Boolean).slice(-6);
16896
+ for (const l of lines)
16897
+ log2(` cloudflared: ${l}`);
16898
+ }
16863
16899
  return null;
16864
16900
  }
16865
16901
  return { child, url: url2 };
16866
16902
  }
16903
+ async function waitNamedTunnelReady(stream2) {
16904
+ const reader = stream2.getReader();
16905
+ const dec = new TextDecoder;
16906
+ const deadline = Date.now() + 30000;
16907
+ let buf = "";
16908
+ while (Date.now() < deadline) {
16909
+ const { value, done } = await reader.read();
16910
+ if (done)
16911
+ break;
16912
+ buf += dec.decode(value, { stream: true });
16913
+ if (/Registered tunnel connection|Connection [a-z0-9-]+ registered/i.test(buf)) {
16914
+ (async () => {
16915
+ try {
16916
+ while (true) {
16917
+ const { done: done2 } = await reader.read();
16918
+ if (done2)
16919
+ break;
16920
+ }
16921
+ } catch {}
16922
+ })();
16923
+ return { ready: true, tail: buf };
16924
+ }
16925
+ }
16926
+ return { ready: false, tail: buf };
16927
+ }
16867
16928
  async function probeTunnel(url2) {
16868
16929
  try {
16869
16930
  const ctrl = new AbortController;
@@ -16929,10 +16990,10 @@ async function parseTunnelUrl(stream2) {
16929
16990
  }
16930
16991
  } catch {}
16931
16992
  })();
16932
- return m[1];
16993
+ return { url: m[1], tail: buf };
16933
16994
  }
16934
16995
  }
16935
- return null;
16996
+ return { url: null, tail: buf };
16936
16997
  }
16937
16998
  async function markStopped(apiUrl, issueId, reason) {
16938
16999
  try {
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"
7
7
  },
8
+ "files": [
9
+ "dist"
10
+ ],
8
11
  "scripts": {
9
12
  "dev": "bun run src/index.ts",
10
13
  "build": "bun build src/index.ts --outdir=dist --target=bun"
11
14
  },
12
15
  "dependencies": {
13
16
  "@agentclientprotocol/sdk": "^0.20.0",
14
- "@agentclientprotocol/claude-agent-acp": "^0.31.0",
17
+ "@agentclientprotocol/claude-agent-acp": "^0.31.0"
18
+ },
19
+ "devDependencies": {
15
20
  "@multi/lib": "workspace:*"
16
21
  }
17
22
  }
package/src/acp-runner.ts DELETED
@@ -1,274 +0,0 @@
1
- // ACP runner — speaks Agent Client Protocol to an agent adapter (claude-code-acp etc).
2
- // Spawns the adapter as a subprocess over stdio, converts ACP events → our StreamEvent shape
3
- // and handles client-side callbacks (requestPermission forwarded to server, fs read/write local).
4
-
5
- import { ClientSideConnection, ndJsonStream } from '@agentclientprotocol/sdk';
6
- import type { Client, SessionNotification, RequestPermissionRequest, RequestPermissionResponse, ReadTextFileRequest, ReadTextFileResponse, WriteTextFileRequest, WriteTextFileResponse } from '@agentclientprotocol/sdk';
7
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
8
- import { dirname } from 'path';
9
- import { apiClient } from './client';
10
-
11
- function fmtErr(e: any): string {
12
- if (e == null) return 'unknown error';
13
- if (typeof e === 'string') return e;
14
- if (e instanceof Error) return e.message;
15
- if (typeof e === 'object') {
16
- const inner = e.error ?? e.cause ?? e;
17
- const msg = inner?.message ?? e.message ?? e.reason ?? e.statusText;
18
- const code = inner?.code ?? e.code ?? e.status;
19
- if (msg) return code ? `${msg} (code ${code})` : String(msg);
20
- try { return JSON.stringify(e).slice(0, 500); } catch {}
21
- }
22
- return String(e);
23
- }
24
-
25
- import type { CliStreamEmit } from '@multi/lib';
26
- export type AcpEvent = CliStreamEmit;
27
-
28
- export type AcpRunOpts = {
29
- apiUrl: string;
30
- issueId: string;
31
- deviceId: string;
32
- prompt: string;
33
- cwd?: string;
34
- sessionId?: string | null;
35
- adapterBin: string | string[]; // command + args to spawn ACP agent
36
- autonomy?: 'manual' | 'ask' | 'auto';
37
- onEvent: (ev: AcpEvent) => void | Promise<void>;
38
- onSession?: (sessionId: string) => void | Promise<void>;
39
- onSpawn?: (child: any) => void;
40
- };
41
-
42
- export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; sessionId: string }> {
43
- const cleanEnv: Record<string, string> = {};
44
- for (const [k, v] of Object.entries(process.env)) {
45
- if (v === undefined) continue;
46
- if (k === 'CLAUDECODE' || k.startsWith('CLAUDE_CODE_')) continue;
47
- cleanEnv[k] = v;
48
- }
49
- const argv = Array.isArray(opts.adapterBin) ? opts.adapterBin : [opts.adapterBin];
50
- // Daemon has no TTY — default to bypass (claude's --dangerously-skip-permissions equivalent).
51
- // Set MULTI_CLAUDE_SAFE=1 to restore interactive permission prompts.
52
- const permMode = process.env.MULTI_CLAUDE_SAFE === '1' ? 'default' : 'bypassPermissions';
53
- const child = Bun.spawn(argv, {
54
- stdio: ['pipe', 'pipe', 'inherit'],
55
- cwd: opts.cwd || process.cwd(),
56
- env: { ...cleanEnv, ACP_PERMISSION_MODE: permMode },
57
- });
58
- try { opts.onSpawn?.(child); } catch {}
59
-
60
- // Adapter expects plain newline-delimited JSON on stdio. Wrap child streams as Web Streams.
61
- const output = new WritableStream<Uint8Array>({
62
- write(chunk) {
63
- (child.stdin as any).write(chunk);
64
- },
65
- close() { try { (child.stdin as any).end?.(); } catch {} },
66
- });
67
- const input = child.stdout as ReadableStream<Uint8Array>;
68
- const stream = ndJsonStream(output, input);
69
-
70
- let activeSessionId: string | null = opts.sessionId || null;
71
- let recording = false; // only forward events after prompt() starts
72
- let chunkCount = 0; // assistant_text + tool_call chunks seen during prompt()
73
- const alwaysAllow = new Set<string>(); // keys: toolName|kind that user said "always allow"
74
-
75
- const client: Client = {
76
- async sessionUpdate(params: SessionNotification): Promise<void> {
77
- if (!recording) return; // drop replay chatter during loadSession
78
- await handleSessionUpdate(params, opts);
79
- },
80
- async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
81
- return await handleRequestPermission(params, opts, alwaysAllow);
82
- },
83
- async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
84
- try {
85
- const content = readFileSync(params.path, 'utf8');
86
- const sliced = typeof params.line === 'number' && typeof params.limit === 'number'
87
- ? content.split('\n').slice(params.line, params.line + params.limit).join('\n')
88
- : content;
89
- return { content: sliced };
90
- } catch (e) {
91
- throw new Error(`readTextFile failed: ${fmtErr(e)}`);
92
- }
93
- },
94
- async writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
95
- try {
96
- const dir = dirname(params.path);
97
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
98
- writeFileSync(params.path, params.content, 'utf8');
99
- return {};
100
- } catch (e) {
101
- throw new Error(`writeTextFile failed: ${fmtErr(e)}`);
102
- }
103
- },
104
- };
105
-
106
- const conn = new ClientSideConnection(() => client, stream);
107
-
108
- try {
109
- await conn.initialize({
110
- protocolVersion: 1,
111
- clientCapabilities: {
112
- fs: { readTextFile: true, writeTextFile: true },
113
- terminal: false,
114
- },
115
- } as any);
116
-
117
- if (!activeSessionId) {
118
- const { sessionId } = await conn.newSession({
119
- cwd: opts.cwd || process.cwd(),
120
- mcpServers: [],
121
- } as any);
122
- activeSessionId = sessionId;
123
- if (opts.onSession) await opts.onSession(sessionId);
124
- await opts.onEvent({ event_type: 'progress', payload: { message: `ACP session ${sessionId.slice(0, 8)} created` } });
125
- } else {
126
- try {
127
- await conn.loadSession?.({ sessionId: activeSessionId, cwd: opts.cwd || process.cwd(), mcpServers: [] } as any);
128
- await opts.onEvent({ event_type: 'progress', payload: { message: `ACP session ${activeSessionId.slice(0, 8)} resumed` } });
129
- } catch (e) {
130
- await opts.onEvent({ event_type: 'progress', payload: { message: `load_session failed; starting new. ${fmtErr(e)}` } });
131
- const { sessionId } = await conn.newSession({ cwd: opts.cwd || process.cwd(), mcpServers: [] } as any);
132
- activeSessionId = sessionId;
133
- if (opts.onSession) await opts.onSession(sessionId);
134
- }
135
- }
136
-
137
- recording = true;
138
- const runPrompt = async () => {
139
- chunkCount = 0;
140
- return await conn.prompt({
141
- sessionId: activeSessionId!,
142
- prompt: [{ type: 'text', text: opts.prompt }],
143
- } as any);
144
- };
145
-
146
- let res = await runPrompt();
147
- let stopReason = (res as any).stopReason;
148
-
149
- // Resumed session that returned zero chunks → session is likely stale in the adapter.
150
- // Retry once with a fresh session so user gets an actual response.
151
- if (chunkCount === 0 && opts.sessionId) {
152
- await opts.onEvent({ event_type: 'progress', payload: { message: `resumed session produced no output (stopReason=${stopReason}); retrying with fresh session` } });
153
- try {
154
- const fresh = await conn.newSession({ cwd: opts.cwd || process.cwd(), mcpServers: [] } as any);
155
- activeSessionId = fresh.sessionId;
156
- if (opts.onSession) await opts.onSession(fresh.sessionId);
157
- res = await runPrompt();
158
- stopReason = (res as any).stopReason;
159
- } catch (e) {
160
- await opts.onEvent({ event_type: 'error', payload: { message: `retry with fresh session failed: ${fmtErr(e)}` } });
161
- }
162
- }
163
-
164
- if (chunkCount === 0) {
165
- await opts.onEvent({ event_type: 'error', payload: { message: `agent produced no output (stopReason=${stopReason})` } });
166
- }
167
-
168
- await opts.onEvent({ event_type: 'result', payload: { stopReason } });
169
- return { stopReason, sessionId: activeSessionId! };
170
- } finally {
171
- try { child.kill(); } catch {}
172
- }
173
-
174
- async function handleSessionUpdate(params: SessionNotification, o: AcpRunOpts) {
175
- const u: any = params.update;
176
- const kind = u.sessionUpdate;
177
- switch (kind) {
178
- case 'agent_message_chunk':
179
- case 'agent_thought_chunk': {
180
- const text = extractText(u.content);
181
- if (text) { chunkCount++; await o.onEvent({ event_type: 'assistant_text', payload: { text } }); }
182
- break;
183
- }
184
- case 'tool_call':
185
- case 'tool_call_update': {
186
- chunkCount++;
187
- await o.onEvent({ event_type: 'tool_call', payload: {
188
- id: u.toolCallId || u.id, tool: u.title || u.toolName || 'tool', kind: u.kind, status: u.status, input: u.rawInput, locations: u.locations,
189
- }});
190
- // Surface tool_call content (e.g. diff/text/terminal) as tool_result if present
191
- if (Array.isArray(u.content)) {
192
- for (const c of u.content) {
193
- if (c.type === 'content' && c.content?.type === 'text') {
194
- await o.onEvent({ event_type: 'tool_result', payload: { tool_use_id: u.toolCallId || u.id, content: c.content.text.slice(0, 4000) } });
195
- } else if (c.type === 'diff') {
196
- const diff = `--- ${c.path}\n${c.oldText || ''}\n+++ ${c.path}\n${c.newText || ''}`.slice(0, 4000);
197
- await o.onEvent({ event_type: 'tool_result', payload: { tool_use_id: u.toolCallId || u.id, content: diff } });
198
- }
199
- }
200
- }
201
- break;
202
- }
203
- case 'plan': {
204
- const entries = (u.entries || []).map((e: any, i: number) => `${i + 1}. [${e.status || 'pending'}] ${e.content}`).join('\n');
205
- if (entries) await o.onEvent({ event_type: 'assistant_text', payload: { text: `**Plan**\n\n${entries}` } });
206
- break;
207
- }
208
- case 'current_mode_update': {
209
- await o.onEvent({ event_type: 'progress', payload: { message: `mode=${u.currentModeId}` } });
210
- break;
211
- }
212
- default: break;
213
- }
214
- }
215
-
216
- async function handleRequestPermission(params: RequestPermissionRequest, o: AcpRunOpts, allowCache: Set<string>): Promise<RequestPermissionResponse> {
217
- const tc: any = params.toolCall || {};
218
- const toolKey = `${tc.toolName || tc.title || ''}|${tc.kind || ''}`.toLowerCase();
219
-
220
- // Autonomy=auto: auto-approve everything, prefer "always" variant so adapter caches it.
221
- if (o.autonomy === 'auto') {
222
- const opts = params.options as any[];
223
- const alwaysOpt = opts.find(op => /always/i.test(op.name || '') || /allow_always/i.test(op.kind || ''));
224
- const allowOpt = opts.find(op => /allow/i.test(op.kind || '') || /allow/i.test(op.name || ''));
225
- const chosen = alwaysOpt || allowOpt;
226
- if (chosen) return { outcome: { outcome: 'selected', optionId: chosen.optionId } as any };
227
- }
228
-
229
- // Auto-approve if user previously chose "always allow" for same tool/kind
230
- if (toolKey && allowCache.has(toolKey)) {
231
- const allowOpt = (params.options as any[]).find(op => /allow/i.test(op.kind || '') || /allow/i.test(op.name || ''));
232
- if (allowOpt) return { outcome: { outcome: 'selected', optionId: allowOpt.optionId } as any };
233
- }
234
-
235
- const created = await apiClient.post<any>(`${o.apiUrl}/api/permissions`, {
236
- issue_id: o.issueId,
237
- device_id: o.deviceId,
238
- tool_call_id: tc.id || tc.toolCallId,
239
- tool_name: tc.title || tc.toolName,
240
- summary: tc.title,
241
- options: params.options.map((op: any) => ({ optionId: op.optionId, name: op.name, kind: op.kind })),
242
- });
243
- const permId = created.data?.id;
244
- if (!permId) return { outcome: { outcome: 'cancelled' } as any };
245
-
246
- const deadline = Date.now() + 5 * 60 * 1000;
247
- while (Date.now() < deadline) {
248
- await new Promise(r => setTimeout(r, 500));
249
- const got = await apiClient.get<any>(`${o.apiUrl}/api/permissions/${permId}`);
250
- const row = got.data;
251
- if (!row) break;
252
- if (row.status === 'resolved' && row.chosen) {
253
- // If the chosen option is an "always" variant, cache it
254
- const chosenOpt = (params.options as any[]).find(op => op.optionId === row.chosen);
255
- const isAlways = chosenOpt && (/always/i.test(chosenOpt.name || '') || /allow_always/i.test(chosenOpt.kind || ''));
256
- if (isAlways && toolKey) allowCache.add(toolKey);
257
- return { outcome: { outcome: 'selected', optionId: row.chosen } as any };
258
- }
259
- if (row.status === 'cancelled') {
260
- return { outcome: { outcome: 'cancelled' } as any };
261
- }
262
- }
263
- return { outcome: { outcome: 'cancelled' } as any };
264
- }
265
- }
266
-
267
- function extractText(content: any): string {
268
- if (!content) return '';
269
- if (typeof content === 'string') return content;
270
- if (Array.isArray(content)) return content.map(extractText).join('');
271
- if (content.type === 'text' && typeof content.text === 'string') return content.text;
272
- if (content.text) return content.text;
273
- return '';
274
- }
@@ -1,177 +0,0 @@
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
- import { appendFileSync } from 'fs';
10
- import { join } from 'path';
11
-
12
- const HOME = process.env.HOME || process.env.USERPROFILE || '.';
13
- const LOG_PATH = join(HOME, '.multi', 'logs', 'agent.log');
14
- function dlog(msg: string) {
15
- try { appendFileSync(LOG_PATH, `[${new Date().toISOString()}] ${msg}\n`); } catch {}
16
- }
17
-
18
- export interface AcpxRunOpts {
19
- agentType: 'pi' | 'codex' | 'openclaw' | 'claude' | string;
20
- prompt: string;
21
- cwd?: string;
22
- sessionName?: string; // reuse same session across follow-ups (e.g. "issue-<id>")
23
- onEvent: (ev: AcpEvent) => void | Promise<void>;
24
- onSpawn?: (child: any) => void;
25
- }
26
-
27
- export async function runAcpx(opts: AcpxRunOpts): Promise<{ stopReason: string }> {
28
- const args = ['acpx', '--format', 'json', '--json-strict', '--approve-all', '--ttl', '0'];
29
- if (opts.cwd) args.push('--cwd', opts.cwd);
30
- args.push(opts.agentType);
31
- // Ensure a session exists for this name (idempotent). ensure = get-or-create.
32
- // We run it as a separate invocation, then prompt.
33
- if (opts.sessionName) {
34
- const ensureArgs = ['acpx', '--ttl', '0', ...(opts.cwd ? ['--cwd', opts.cwd] : []), opts.agentType, 'sessions', 'ensure', '--name', opts.sessionName];
35
- dlog(`[acpx] ensure: ${ensureArgs.join(' ')}`);
36
- try {
37
- const ensure = Bun.spawn(ensureArgs, { stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' });
38
- const ensureOut = await new Response(ensure.stdout as any).text();
39
- const ensureErr = await new Response(ensure.stderr as any).text();
40
- const ensureCode = await ensure.exited;
41
- dlog(`[acpx] ensure exit=${ensureCode} stdout=${ensureOut.slice(0, 300)} stderr=${ensureErr.slice(0, 300)}`);
42
- } catch (e) {
43
- dlog(`[acpx] ensure spawn error: ${String(e)}`);
44
- }
45
- args.push('prompt', '-s', opts.sessionName);
46
- } else {
47
- args.push('prompt');
48
- }
49
- args.push(opts.prompt);
50
-
51
- dlog(`[acpx] prompt: ${args.slice(0, 10).join(' ')} ... (prompt len=${opts.prompt.length})`);
52
- const proc = Bun.spawn(args, { stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' });
53
- try { opts.onSpawn?.(proc); } catch {}
54
- let stopReason = 'end_turn';
55
-
56
- // Forward stderr to our progress stream so we can debug in the UI/logs
57
- (async () => {
58
- try {
59
- const r = (proc.stderr as ReadableStream<Uint8Array>).getReader();
60
- const d = new TextDecoder();
61
- let sb = '';
62
- while (true) {
63
- const { value, done } = await r.read();
64
- if (done) break;
65
- sb += d.decode(value, { stream: true });
66
- let nl: number;
67
- while ((nl = sb.indexOf('\n')) !== -1) {
68
- const ln = sb.slice(0, nl).trim();
69
- sb = sb.slice(nl + 1);
70
- if (ln) dlog(`[acpx stderr] ${ln.slice(0, 500)}`);
71
- }
72
- }
73
- } catch {}
74
- })();
75
-
76
- const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
77
- const dec = new TextDecoder();
78
- let buf = '';
79
-
80
- let lineCount = 0;
81
- while (true) {
82
- const { value, done } = await reader.read();
83
- if (done) break;
84
- buf += dec.decode(value, { stream: true });
85
- let nl: number;
86
- while ((nl = buf.indexOf('\n')) !== -1) {
87
- const line = buf.slice(0, nl).trim();
88
- buf = buf.slice(nl + 1);
89
- if (!line) continue;
90
- lineCount++;
91
- dlog(`[acpx stdout] ${line.slice(0, 500)}`);
92
- const events = parseAcpLine(line, (r) => { stopReason = r; });
93
- for (const ev of events) await opts.onEvent(ev);
94
- }
95
- }
96
- dlog(`[acpx] stdout lines total=${lineCount}`);
97
- if (buf.trim()) {
98
- const events = parseAcpLine(buf.trim(), (r) => { stopReason = r; });
99
- for (const ev of events) await opts.onEvent(ev);
100
- }
101
-
102
- const code = await proc.exited;
103
- if (code !== 0 && code !== 130) {
104
- await opts.onEvent({ event_type: 'error', payload: { message: `acpx exited with code ${code}` } });
105
- }
106
- return { stopReason };
107
- }
108
-
109
- function parseAcpLine(line: string, setStop: (r: string) => void): AcpEvent[] {
110
- let msg: any;
111
- try { msg = JSON.parse(line); } catch { return []; }
112
- const out: AcpEvent[] = [];
113
-
114
- // session/update notification
115
- if (msg.method === 'session/update' && msg.params?.update?.sessionUpdate) {
116
- return handleSessionUpdate(msg.params);
117
- }
118
- // prompt result
119
- if (msg.id && msg.result && typeof msg.result === 'object' && 'stopReason' in msg.result) {
120
- setStop(msg.result.stopReason);
121
- out.push({ event_type: 'result', payload: { stopReason: msg.result.stopReason } });
122
- return out;
123
- }
124
- // errors
125
- if (msg.error) {
126
- out.push({ event_type: 'error', payload: { message: msg.error.message || JSON.stringify(msg.error) } });
127
- return out;
128
- }
129
- return out;
130
- }
131
-
132
- function handleSessionUpdate(params: any): AcpEvent[] {
133
- const u = params.update || {};
134
- const kind = u.sessionUpdate;
135
- const out: AcpEvent[] = [];
136
- switch (kind) {
137
- case 'agent_message_chunk':
138
- case 'agent_thought_chunk': {
139
- const text = extractText(u.content);
140
- if (text) out.push({ event_type: 'assistant_text', payload: { text } });
141
- break;
142
- }
143
- case 'tool_call':
144
- case 'tool_call_update': {
145
- out.push({ event_type: 'tool_call', payload: {
146
- id: u.toolCallId || u.id, tool: u.title || u.toolName || 'tool',
147
- kind: u.kind, status: u.status, input: u.rawInput, locations: u.locations,
148
- }});
149
- if (Array.isArray(u.content)) {
150
- for (const c of u.content) {
151
- if (c.type === 'content' && c.content?.type === 'text') {
152
- out.push({ event_type: 'tool_result', payload: { tool_use_id: u.toolCallId || u.id, content: String(c.content.text).slice(0, 4000) } });
153
- } else if (c.type === 'diff') {
154
- const diff = `--- ${c.path}\n${c.oldText || ''}\n+++ ${c.path}\n${c.newText || ''}`.slice(0, 4000);
155
- out.push({ event_type: 'tool_result', payload: { tool_use_id: u.toolCallId || u.id, content: diff } });
156
- }
157
- }
158
- }
159
- break;
160
- }
161
- case 'plan': {
162
- const entries = (u.entries || []).map((e: any, i: number) => `${i + 1}. [${e.status || 'pending'}] ${e.content}`).join('\n');
163
- if (entries) out.push({ event_type: 'assistant_text', payload: { text: `**Plan**\n\n${entries}` } });
164
- break;
165
- }
166
- }
167
- return out;
168
- }
169
-
170
- function extractText(content: any): string {
171
- if (!content) return '';
172
- if (typeof content === 'string') return content;
173
- if (Array.isArray(content)) return content.map(extractText).join('');
174
- if (content.type === 'text' && typeof content.text === 'string') return content.text;
175
- if (content.text) return content.text;
176
- return '';
177
- }
package/src/client.ts DELETED
@@ -1,46 +0,0 @@
1
- interface ApiResponse<T = unknown> {
2
- success: boolean;
3
- data?: T;
4
- error?: string;
5
- status?: number;
6
- }
7
-
8
- let authToken: string | null = null;
9
-
10
- export function setAuthToken(token: string | null) {
11
- authToken = token;
12
- }
13
-
14
- async function request<T>(url: string, options?: RequestInit): Promise<ApiResponse<T>> {
15
- try {
16
- const headers: Record<string, string> = {
17
- 'Content-Type': 'application/json',
18
- ...(options?.headers as Record<string, string> | undefined),
19
- };
20
- if (authToken && !headers['Authorization']) headers['Authorization'] = `Bearer ${authToken}`;
21
-
22
- const response = await fetch(url, { ...options, headers });
23
- const raw = await response.json().catch(() => ({}));
24
-
25
- if (!response.ok) {
26
- return { success: false, error: (raw as any)?.error || `HTTP ${response.status}`, status: response.status };
27
- }
28
-
29
- const data = (raw && typeof raw === 'object' && 'results' in (raw as any))
30
- ? (raw as any).results
31
- : raw;
32
-
33
- return { success: true, data, status: response.status };
34
- } catch (error) {
35
- return { success: false, error: String(error) };
36
- }
37
- }
38
-
39
- export const apiClient = {
40
- get: <T = unknown>(url: string) => request<T>(url),
41
- post: <T = unknown>(url: string, body: unknown, opts?: { headers?: Record<string, string> }) =>
42
- request<T>(url, { method: 'POST', body: JSON.stringify(body), headers: opts?.headers }),
43
- patch: <T = unknown>(url: string, body: unknown, opts?: { headers?: Record<string, string> }) =>
44
- request<T>(url, { method: 'PATCH', body: JSON.stringify(body), headers: opts?.headers }),
45
- delete: <T = unknown>(url: string) => request<T>(url, { method: 'DELETE' }),
46
- };