@zengxingyuan/aamp-acp-bridge 0.1.28-dev.10

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/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # aamp-acp-bridge
2
+
3
+ Config-driven bridge that connects ACP-compatible agents to the AAMP email network.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install aamp-acp-bridge
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Initialize the bridge:
14
+
15
+ ```bash
16
+ npx aamp-acp-bridge init
17
+ ```
18
+
19
+ The init wizard scans installed ACP-capable agents, including Hermes, then lets you select multiple entries with arrow keys, Space, and Enter. For each selected agent, choose one authorization setup method:
20
+
21
+ - Pair with a five-minute terminal QR code plus the matching `aamp://connect?...` URL.
22
+ - Manually enter `senderPolicies`.
23
+ - Reuse existing `senderPolicies`, when any are available.
24
+ - Configure sender authorization later; `task.dispatch` is rejected until pairing or policy setup is complete.
25
+
26
+ If you choose QR pairing, `init` starts the bridge immediately after writing config, so scanning the QR code with AAMP App works right away. The bridge answers each `pair.request` with `pair.respond`; rejected responses include the failure reason.
27
+
28
+ Use `--no-start` only when you need to generate config in a script without
29
+ keeping the bridge process running:
30
+
31
+ ```bash
32
+ npx aamp-acp-bridge init --agent claude --no-start
33
+ ```
34
+
35
+ After an agent has been initialized, generate a fresh pairing QR code without
36
+ re-running setup:
37
+
38
+ ```bash
39
+ npx aamp-acp-bridge pair --agent claude
40
+ ```
41
+
42
+ Start the bridge:
43
+
44
+ ```bash
45
+ npx aamp-acp-bridge start
46
+ ```
47
+
48
+ When debugging task routing, add `--debug` to print the exact prompt sent to
49
+ the ACP agent for each `task.dispatch`:
50
+
51
+ ```bash
52
+ npx aamp-acp-bridge start --debug
53
+ ```
54
+
55
+ By default, the bridge stores its config under `~/.aamp/acp-bridge/config.json` and agent credentials under `~/.aamp/acp-bridge/credentials/`.
56
+ Legacy `./bridge.json` and `~/.acp-bridge/` data are migrated automatically on first use without deleting the original files.
57
+
58
+ The bridge understands these task lifecycle intents:
59
+
60
+ - `task.dispatch`
61
+ - `task.stream.opened`
62
+ - `task.help_needed`
63
+ - `task.result`
64
+ - `task.cancel`
65
+
66
+ Dispatch tasks can also carry:
67
+
68
+ - `priority`: `urgent | high | normal`
69
+ - `expiresAt`: an ISO-8601 timestamp after which the task should no longer run
70
+ - `promptRules`: an optional complete text block that replaces the default task
71
+ prompt rules
72
+
73
+ If a `task.cancel` arrives before the ACP agent returns a final answer, the bridge suppresses any later result send for that task.
74
+
75
+ When `promptRules` is present on `task.dispatch`, the ACP prompt keeps its
76
+ standard task identity, metadata, dispatch context, description, and thread
77
+ context, then replaces the default task rule block with the provided text.
78
+
79
+ While ACP execution is in progress, the bridge can:
80
+
81
+ - create an AAMP task stream for the task
82
+ - send `task.stream.opened`
83
+ - append `status`, `progress`, and `text.delta` events
84
+ - forward ACP `agent_thought_chunk` / `agent_message_chunk` updates into the AAMP stream in realtime
85
+ - expose tool progress as stream progress labels while the agent is working
86
+ - close the stream before the authoritative `task.result` or `task.help_needed`
87
+
88
+ When `acpx` supports `--format json --json-strict`, the bridge consumes the structured ACP NDJSON stream so reasoning / reply chunks can be forwarded live. Older `acpx` builds automatically fall back to plain-text mode, which preserves compatibility but cannot expose thought chunks incrementally.
89
+
90
+ ## Config
91
+
92
+ Minimal example:
93
+
94
+ ```json
95
+ {
96
+ "aampHost": "https://meshmail.ai",
97
+ "rejectUnauthorized": false,
98
+ "agents": [
99
+ {
100
+ "name": "claude",
101
+ "acpCommand": "claude",
102
+ "slug": "claude-bridge",
103
+ "taskDispatchConcurrency": 10,
104
+ "credentialsFile": "~/.aamp/acp-bridge/credentials/claude.json",
105
+ "senderPolicies": [
106
+ {
107
+ "sender": "system@aamp.local",
108
+ "dispatchContextRules": {
109
+ "project_key": ["proj_123"]
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ ]
115
+ }
116
+ ```
117
+
118
+ `senderPolicies` is optional, but omitted policies do not authorize anyone by default. Use QR pairing or configure at least one policy before sending `task.dispatch`; matching policies can also enforce exact-match `X-AAMP-Dispatch-Context` rules.
119
+ Legacy `senderWhitelist` configs still load and are normalized into `senderPolicies`.
120
+ When editing the `senderPoliciesFile` directly, `pairedAt` is optional; the bridge accepts manually added records with just `sender` and optional `dispatchContextRules`.
121
+ `credentialsFile` is optional. If omitted, the bridge uses `~/.aamp/acp-bridge/credentials/<agent>.json`.
122
+ `taskDispatchConcurrency` is optional and defaults to `10`.
123
+
124
+ ### Hermes
125
+
126
+ Hermes exposes ACP through `hermes acp`, so its bridge config uses a raw ACP command:
127
+
128
+ ```json
129
+ {
130
+ "name": "hermes",
131
+ "acpCommand": "hermes acp",
132
+ "slug": "hermes-bridge"
133
+ }
134
+ ```
135
+
136
+ `init --agent hermes` writes this command automatically when Hermes is installed.
@@ -0,0 +1,81 @@
1
+ export interface AcpEvent {
2
+ eventVersion?: number;
3
+ sessionId?: string;
4
+ requestId?: string;
5
+ seq?: number;
6
+ type?: string;
7
+ messageId?: string;
8
+ content?: unknown;
9
+ [key: string]: unknown;
10
+ }
11
+ export type AcpTextChunkChannel = 'assistant' | 'thought';
12
+ export interface AcpTextChunk {
13
+ channel: AcpTextChunkChannel;
14
+ text: string;
15
+ messageId?: string;
16
+ }
17
+ export interface AcpToolUpdate {
18
+ toolCallId?: string;
19
+ title?: string;
20
+ status?: string;
21
+ kind?: string;
22
+ text?: string;
23
+ locations?: Array<{
24
+ path: string;
25
+ line?: number;
26
+ }>;
27
+ }
28
+ export interface AcpPlanEntry {
29
+ content: string;
30
+ status?: string;
31
+ priority?: string;
32
+ }
33
+ export interface AcpPromptHandlers {
34
+ onEvent?: (event: AcpEvent) => void;
35
+ onTextChunk?: (chunk: AcpTextChunk) => void;
36
+ onToolUpdate?: (update: AcpToolUpdate) => void;
37
+ onPlanUpdate?: (entries: AcpPlanEntry[]) => void;
38
+ }
39
+ export interface AcpResult {
40
+ output: string;
41
+ events: AcpEvent[];
42
+ stopReason?: string;
43
+ streamedAssistantText: boolean;
44
+ }
45
+ /**
46
+ * Wrapper around acpx CLI.
47
+ * Invokes acpx as a subprocess and parses NDJSON output.
48
+ */
49
+ export declare class AcpxClient {
50
+ private cwd;
51
+ constructor(cwd?: string);
52
+ private isRawAgentCommand;
53
+ private buildAcpxArgs;
54
+ private formatArgForLog;
55
+ private formatFailedCommand;
56
+ private buildSubprocessEnv;
57
+ private formatProcessFailure;
58
+ /**
59
+ * Ensure a named ACP session exists for the given agent.
60
+ */
61
+ ensureSession(agent: string, sessionName: string): Promise<string>;
62
+ /**
63
+ * Send a prompt to an ACP agent and wait for completion.
64
+ * Collects all stdout + stderr output and extracts the agent's response.
65
+ */
66
+ prompt(agent: string, sessionName: string, text: string, handlers?: AcpPromptHandlers): Promise<AcpResult>;
67
+ private promptJsonMode;
68
+ private promptTextMode;
69
+ /**
70
+ * Cancel the current operation in a session.
71
+ */
72
+ cancel(agent: string, sessionName: string): Promise<void>;
73
+ /**
74
+ * Close a session.
75
+ */
76
+ close(agent: string, sessionName: string): Promise<void>;
77
+ /**
78
+ * Execute an acpx command and return stdout.
79
+ */
80
+ private exec;
81
+ }
@@ -0,0 +1,485 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ function asRecord(value) {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value))
6
+ return null;
7
+ return value;
8
+ }
9
+ function asString(value) {
10
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
11
+ }
12
+ function extractContentText(content) {
13
+ if (typeof content === 'string')
14
+ return content;
15
+ if (Array.isArray(content)) {
16
+ return content.map((item) => extractContentText(item)).join('');
17
+ }
18
+ const record = asRecord(content);
19
+ if (!record)
20
+ return '';
21
+ if (typeof record.text === 'string')
22
+ return record.text;
23
+ if (typeof record.thinking === 'string')
24
+ return record.thinking;
25
+ const resource = asRecord(record.resource);
26
+ if (resource && typeof resource.text === 'string')
27
+ return resource.text;
28
+ return '';
29
+ }
30
+ function extractToolLocations(value) {
31
+ if (!Array.isArray(value))
32
+ return undefined;
33
+ const locations = value.flatMap((item) => {
34
+ const record = asRecord(item);
35
+ if (!record)
36
+ return [];
37
+ const path = asString(record.path);
38
+ if (!path)
39
+ return [];
40
+ const line = typeof record.line === 'number' && Number.isFinite(record.line)
41
+ ? record.line
42
+ : undefined;
43
+ return [{ path, ...(line != null ? { line } : {}) }];
44
+ });
45
+ return locations.length > 0 ? locations : undefined;
46
+ }
47
+ function extractPlanEntries(value) {
48
+ if (!Array.isArray(value))
49
+ return [];
50
+ return value.flatMap((item) => {
51
+ const record = asRecord(item);
52
+ const content = asString(record?.content);
53
+ if (!content)
54
+ return [];
55
+ return [{
56
+ content,
57
+ ...(asString(record?.status) ? { status: asString(record?.status) } : {}),
58
+ ...(asString(record?.priority) ? { priority: asString(record?.priority) } : {}),
59
+ }];
60
+ });
61
+ }
62
+ function normalizeLegacyEvent(record) {
63
+ const rawType = asString(record.type);
64
+ if (!rawType)
65
+ return null;
66
+ const mappedType = rawType === 'thinking' ? 'agent_thought_chunk' : rawType;
67
+ return {
68
+ ...record,
69
+ type: mappedType,
70
+ ...(asString(record.sessionId) ? { sessionId: asString(record.sessionId) } : {}),
71
+ ...(asString(record.requestId) ? { requestId: asString(record.requestId) } : {}),
72
+ ...(typeof record.seq === 'number' ? { seq: record.seq } : {}),
73
+ };
74
+ }
75
+ function normalizeJsonRpcEvent(record) {
76
+ if (record.method === 'session/update') {
77
+ const params = asRecord(record.params);
78
+ if (!params)
79
+ return null;
80
+ const explicitUpdate = asRecord(params.update);
81
+ const fallbackUpdate = asString(params.sessionUpdate)
82
+ ? {
83
+ ...params,
84
+ sessionUpdate: params.sessionUpdate,
85
+ }
86
+ : null;
87
+ const update = explicitUpdate ?? fallbackUpdate;
88
+ if (!update)
89
+ return null;
90
+ const type = asString(update.sessionUpdate);
91
+ if (!type)
92
+ return null;
93
+ const normalized = {
94
+ type,
95
+ ...(asString(params.sessionId) ? { sessionId: asString(params.sessionId) } : {}),
96
+ };
97
+ for (const [key, value] of Object.entries(update)) {
98
+ if (key === 'sessionUpdate')
99
+ continue;
100
+ normalized[key] = value;
101
+ }
102
+ return normalized;
103
+ }
104
+ if (record.result) {
105
+ const result = asRecord(record.result);
106
+ if (!result)
107
+ return null;
108
+ return {
109
+ type: 'result',
110
+ ...(asString(record.id) ? { requestId: asString(record.id) } : {}),
111
+ ...result,
112
+ };
113
+ }
114
+ if (record.error) {
115
+ return {
116
+ type: 'error',
117
+ ...(asString(record.id) ? { requestId: asString(record.id) } : {}),
118
+ error: record.error,
119
+ };
120
+ }
121
+ return null;
122
+ }
123
+ function parseAcpLine(line) {
124
+ const trimmed = line.trim();
125
+ if (!trimmed)
126
+ return { event: null, isJson: false };
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(trimmed);
130
+ }
131
+ catch {
132
+ return { event: null, isJson: false };
133
+ }
134
+ const record = asRecord(parsed);
135
+ if (!record)
136
+ return { event: null, isJson: true };
137
+ if (record.jsonrpc === '2.0') {
138
+ return { event: normalizeJsonRpcEvent(record), isJson: true };
139
+ }
140
+ return { event: normalizeLegacyEvent(record), isJson: true };
141
+ }
142
+ function supportsJsonStreamingFallback(stderr) {
143
+ return /unknown option|unknown argument|unexpected argument|invalid value|--format|--json-strict/i.test(stderr);
144
+ }
145
+ function isCliTranscriptHeader(line) {
146
+ return /^\[(acpx|client|tool|done|error|warning)\](?:\s|$)/i.test(line);
147
+ }
148
+ function extractFinalReplyFromTranscript(output) {
149
+ const trimmed = output.trim();
150
+ if (!trimmed)
151
+ return '';
152
+ const lines = trimmed.replace(/\r\n/g, '\n').split('\n');
153
+ const textBlocks = [];
154
+ let currentBlock = [];
155
+ let skippingTranscriptDetails = false;
156
+ const flushBlock = () => {
157
+ const block = currentBlock.join('\n').trim();
158
+ if (block)
159
+ textBlocks.push(block);
160
+ currentBlock = [];
161
+ };
162
+ for (const line of lines) {
163
+ if (isCliTranscriptHeader(line)) {
164
+ flushBlock();
165
+ skippingTranscriptDetails = true;
166
+ continue;
167
+ }
168
+ if (skippingTranscriptDetails) {
169
+ if (!line || /^[ \t]+/.test(line)) {
170
+ continue;
171
+ }
172
+ skippingTranscriptDetails = false;
173
+ }
174
+ currentBlock.push(line);
175
+ }
176
+ flushBlock();
177
+ return textBlocks.at(-1) ?? '';
178
+ }
179
+ function sanitizePromptOutput(output) {
180
+ const trimmed = output.trim();
181
+ if (!trimmed)
182
+ return '';
183
+ if (!trimmed.split(/\r?\n/).some((line) => isCliTranscriptHeader(line))) {
184
+ return trimmed;
185
+ }
186
+ return extractFinalReplyFromTranscript(trimmed);
187
+ }
188
+ /**
189
+ * Wrapper around acpx CLI.
190
+ * Invokes acpx as a subprocess and parses NDJSON output.
191
+ */
192
+ export class AcpxClient {
193
+ cwd;
194
+ constructor(cwd) {
195
+ this.cwd = cwd ?? process.cwd();
196
+ }
197
+ isRawAgentCommand(agent) {
198
+ return /\s/.test(agent.trim());
199
+ }
200
+ buildAcpxArgs(agent, args, globalArgs = []) {
201
+ if (this.isRawAgentCommand(agent)) {
202
+ return ['--approve-all', '--cwd', this.cwd, ...globalArgs, '--agent', agent, ...args];
203
+ }
204
+ return ['--approve-all', '--cwd', this.cwd, ...globalArgs, agent, ...args];
205
+ }
206
+ formatArgForLog(arg) {
207
+ const normalized = arg.replace(/\s+/g, ' ').trim();
208
+ if (normalized.length <= 160)
209
+ return normalized;
210
+ return `${normalized.slice(0, 157)}...`;
211
+ }
212
+ formatFailedCommand(agent, args, globalArgs = []) {
213
+ return ['acpx', ...this.buildAcpxArgs(agent, args, globalArgs)]
214
+ .map((arg) => this.formatArgForLog(arg))
215
+ .join(' ');
216
+ }
217
+ buildSubprocessEnv() {
218
+ const env = { ...process.env };
219
+ const registry = env.npm_config_registry || env.NPM_CONFIG_REGISTRY || 'https://registry.npmjs.org/';
220
+ const cache = env.npm_config_cache || env.NPM_CONFIG_CACHE || `${tmpdir()}/aamp-acpx-npm-cache`;
221
+ for (const key of Object.keys(env)) {
222
+ const lower = key.toLowerCase();
223
+ if (lower.startsWith('npm_config_')
224
+ || lower.startsWith('npm_package_')
225
+ || lower.startsWith('npm_lifecycle_')
226
+ || lower === 'npm_command'
227
+ || lower === 'npm_execpath'
228
+ || lower === 'npm_node_execpath'
229
+ || lower === 'init_cwd') {
230
+ delete env[key];
231
+ }
232
+ }
233
+ mkdirSync(cache, { recursive: true });
234
+ env.npm_config_registry = registry;
235
+ env.NPM_CONFIG_REGISTRY = registry;
236
+ env.npm_config_cache = cache;
237
+ env.NPM_CONFIG_CACHE = cache;
238
+ return env;
239
+ }
240
+ formatProcessFailure(agent, args, code, stdout, stderr, globalArgs = []) {
241
+ const details = [
242
+ stderr.trim() ? `stderr: ${stderr.trim()}` : '',
243
+ stdout.trim() ? `stdout: ${stdout.trim()}` : '',
244
+ ].filter(Boolean);
245
+ return `${this.formatFailedCommand(agent, args, globalArgs)} failed (${code ?? 'unknown'}): ${details.join('\n') || 'no output from acpx'}`;
246
+ }
247
+ /**
248
+ * Ensure a named ACP session exists for the given agent.
249
+ */
250
+ async ensureSession(agent, sessionName) {
251
+ const result = await this.exec(agent, ['sessions', 'ensure', '--name', sessionName]);
252
+ // Try to extract sessionId from the JSON output
253
+ try {
254
+ const data = JSON.parse(result.trim().split('\n').pop() ?? '{}');
255
+ return data.sessionId ?? sessionName;
256
+ }
257
+ catch {
258
+ return sessionName;
259
+ }
260
+ }
261
+ /**
262
+ * Send a prompt to an ACP agent and wait for completion.
263
+ * Collects all stdout + stderr output and extracts the agent's response.
264
+ */
265
+ async prompt(agent, sessionName, text, handlers) {
266
+ try {
267
+ return await this.promptJsonMode(agent, sessionName, text, handlers);
268
+ }
269
+ catch (err) {
270
+ if (supportsJsonStreamingFallback(err.message)) {
271
+ return await this.promptTextMode(agent, sessionName, text);
272
+ }
273
+ throw err;
274
+ }
275
+ }
276
+ async promptJsonMode(agent, sessionName, text, handlers) {
277
+ const events = [];
278
+ let stopReason;
279
+ let streamedAssistantText = false;
280
+ const assistantMessages = new Map();
281
+ const assistantMessageOrder = [];
282
+ let lastAssistantMessageKey;
283
+ let lastThoughtMessageKey;
284
+ let thoughtMessageCount = 0;
285
+ let previousEventType;
286
+ return new Promise((resolve, reject) => {
287
+ const proc = spawn('acpx', this.buildAcpxArgs(agent, [
288
+ 'prompt',
289
+ '-s', sessionName,
290
+ text,
291
+ ], ['--format', 'json', '--json-strict']), {
292
+ stdio: ['pipe', 'pipe', 'pipe'],
293
+ cwd: this.cwd,
294
+ env: this.buildSubprocessEnv(),
295
+ });
296
+ let stdoutBuffer = '';
297
+ let rawStdout = '';
298
+ let stderr = '';
299
+ const processLine = (line) => {
300
+ const parsed = parseAcpLine(line);
301
+ const event = parsed.event;
302
+ if (!event) {
303
+ if (!parsed.isJson) {
304
+ rawStdout += `${line}\n`;
305
+ }
306
+ return;
307
+ }
308
+ events.push(event);
309
+ handlers?.onEvent?.(event);
310
+ if (event.type === 'agent_message_chunk') {
311
+ const textChunk = extractContentText(event.content);
312
+ if (textChunk) {
313
+ const explicitMessageId = asString(event.messageId);
314
+ const messageKey = explicitMessageId
315
+ ?? (previousEventType === 'agent_message_chunk' && lastAssistantMessageKey
316
+ ? lastAssistantMessageKey
317
+ : `anonymous:${assistantMessageOrder.length}`);
318
+ if (!assistantMessages.has(messageKey)) {
319
+ assistantMessages.set(messageKey, '');
320
+ assistantMessageOrder.push(messageKey);
321
+ }
322
+ assistantMessages.set(messageKey, `${assistantMessages.get(messageKey) ?? ''}${textChunk}`);
323
+ lastAssistantMessageKey = messageKey;
324
+ streamedAssistantText = true;
325
+ handlers?.onTextChunk?.({
326
+ channel: 'assistant',
327
+ text: textChunk,
328
+ messageId: messageKey,
329
+ });
330
+ }
331
+ previousEventType = event.type;
332
+ return;
333
+ }
334
+ if (event.type === 'agent_thought_chunk') {
335
+ const textChunk = extractContentText(event.content);
336
+ if (textChunk) {
337
+ const messageId = asString(event.messageId)
338
+ ?? (previousEventType === 'agent_thought_chunk' && lastThoughtMessageKey
339
+ ? lastThoughtMessageKey
340
+ : `anonymous-thought:${thoughtMessageCount++}`);
341
+ lastThoughtMessageKey = messageId;
342
+ handlers?.onTextChunk?.({
343
+ channel: 'thought',
344
+ text: textChunk,
345
+ messageId,
346
+ });
347
+ }
348
+ previousEventType = event.type;
349
+ return;
350
+ }
351
+ if (event.type === 'tool_call' || event.type === 'tool_call_update') {
352
+ handlers?.onToolUpdate?.({
353
+ toolCallId: asString(event.toolCallId),
354
+ title: asString(event.title),
355
+ status: asString(event.status),
356
+ kind: asString(event.kind),
357
+ text: extractContentText(event.content),
358
+ locations: extractToolLocations(event.locations),
359
+ });
360
+ previousEventType = event.type;
361
+ return;
362
+ }
363
+ if (event.type === 'plan') {
364
+ const entries = extractPlanEntries(event.entries);
365
+ if (entries.length > 0) {
366
+ handlers?.onPlanUpdate?.(entries);
367
+ }
368
+ previousEventType = event.type;
369
+ return;
370
+ }
371
+ if (event.type === 'result') {
372
+ stopReason = asString(event.stopReason);
373
+ }
374
+ previousEventType = event.type;
375
+ };
376
+ const processStdoutChunk = (chunk) => {
377
+ stdoutBuffer += chunk.toString();
378
+ let newlineIndex = stdoutBuffer.indexOf('\n');
379
+ while (newlineIndex >= 0) {
380
+ const line = stdoutBuffer.slice(0, newlineIndex).replace(/\r$/, '');
381
+ stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
382
+ processLine(line);
383
+ newlineIndex = stdoutBuffer.indexOf('\n');
384
+ }
385
+ };
386
+ proc.stdout.on('data', processStdoutChunk);
387
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
388
+ proc.on('close', (code) => {
389
+ if (stdoutBuffer.trim()) {
390
+ processLine(stdoutBuffer.replace(/\r$/, ''));
391
+ }
392
+ const finalAssistantOutput = [...assistantMessageOrder]
393
+ .reverse()
394
+ .map((messageKey) => assistantMessages.get(messageKey)?.trim() ?? '')
395
+ .find((message) => message.length > 0) ?? '';
396
+ const output = finalAssistantOutput
397
+ || sanitizePromptOutput(rawStdout)
398
+ || sanitizePromptOutput(stderr);
399
+ if (code !== 0 && !output) {
400
+ reject(new Error(this.formatProcessFailure(agent, ['prompt', '-s', sessionName, text], code, rawStdout, stderr, ['--format', 'json', '--json-strict'])));
401
+ }
402
+ else {
403
+ resolve({
404
+ output,
405
+ events,
406
+ ...(stopReason ? { stopReason } : {}),
407
+ streamedAssistantText,
408
+ });
409
+ }
410
+ });
411
+ proc.on('error', (err) => {
412
+ reject(new Error(`Failed to spawn acpx: ${err.message}. Is acpx installed?`));
413
+ });
414
+ });
415
+ }
416
+ async promptTextMode(agent, sessionName, text) {
417
+ const events = [];
418
+ return await new Promise((resolve, reject) => {
419
+ // Old acpx builds may not support JSON output yet.
420
+ const proc = spawn('acpx', this.buildAcpxArgs(agent, ['prompt', '-s', sessionName, text]), {
421
+ stdio: ['pipe', 'pipe', 'pipe'],
422
+ cwd: this.cwd,
423
+ env: this.buildSubprocessEnv(),
424
+ });
425
+ let stdout = '';
426
+ let stderr = '';
427
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
428
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
429
+ proc.on('close', (code) => {
430
+ const output = sanitizePromptOutput(stdout) || sanitizePromptOutput(stderr);
431
+ if (code !== 0 && !output) {
432
+ reject(new Error(this.formatProcessFailure(agent, ['prompt', '-s', sessionName, text], code, stdout, stderr)));
433
+ }
434
+ else {
435
+ resolve({
436
+ output,
437
+ events,
438
+ streamedAssistantText: false,
439
+ });
440
+ }
441
+ });
442
+ proc.on('error', (err) => {
443
+ reject(new Error(`Failed to spawn acpx: ${err.message}. Is acpx installed?`));
444
+ });
445
+ });
446
+ }
447
+ /**
448
+ * Cancel the current operation in a session.
449
+ */
450
+ async cancel(agent, sessionName) {
451
+ await this.exec(agent, ['cancel', '-s', sessionName]);
452
+ }
453
+ /**
454
+ * Close a session.
455
+ */
456
+ async close(agent, sessionName) {
457
+ await this.exec(agent, ['sessions', 'close', sessionName]);
458
+ }
459
+ /**
460
+ * Execute an acpx command and return stdout.
461
+ */
462
+ exec(agent, args) {
463
+ return new Promise((resolve, reject) => {
464
+ const proc = spawn('acpx', this.buildAcpxArgs(agent, args), {
465
+ stdio: ['pipe', 'pipe', 'pipe'],
466
+ cwd: this.cwd,
467
+ env: this.buildSubprocessEnv(),
468
+ });
469
+ let stdout = '';
470
+ let stderr = '';
471
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
472
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
473
+ proc.on('close', (code) => {
474
+ if (code !== 0)
475
+ reject(new Error(this.formatProcessFailure(agent, args, code, stdout, stderr)));
476
+ else
477
+ resolve(stdout);
478
+ });
479
+ proc.on('error', (err) => {
480
+ reject(new Error(`Failed to spawn acpx: ${err.message}`));
481
+ });
482
+ });
483
+ }
484
+ }
485
+ //# sourceMappingURL=acpx-client.js.map