@genspark/cli 1.0.9 → 1.0.11

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 CHANGED
@@ -463,9 +463,35 @@ gsk task cross_check --task_name "Earth shape" --query "The Earth is flat" --ins
463
463
  | `--task_name <name>` | — | Name for the task (**required**) |
464
464
  | `--query <text>` | — | Query describing what to create (**required**) |
465
465
  | `--instructions <text>` | — | Detailed instructions (**required**) |
466
+ | `--acp` | `false` | Start as ACP (Agent Client Protocol) stdio agent for multi-turn use with Genspark Claw |
466
467
 
467
468
  **Supported task types:** `super_agent`, `podcasts`, `docs`, `slides`, `sheets`, `deep_research`, `website`, `video_generation`, `audio_generation`, `meeting_notes`, `cross_check`
468
469
 
470
+ #### ACP Mode
471
+
472
+ Use `--acp` to start a task agent as an [Agent Client Protocol](https://agentclientprotocol.com/) stdio server. This enables AI agent platforms like [Genspark Claw](https://openclaw.ai) to natively discover and interact with GSK agents, with multi-turn conversation support.
473
+
474
+ ```bash
475
+ # Start an ACP agent for slides (used by acpx, not typically run manually)
476
+ gsk task slides --acp
477
+
478
+ # Start an ACP agent for documents
479
+ gsk task docs --acp
480
+ ```
481
+
482
+ **acpx configuration** (`~/.acpx/config.json`):
483
+ ```json
484
+ {
485
+ "agents": {
486
+ "gsk-slides": { "command": "gsk task slides --acp" },
487
+ "gsk-docs": { "command": "gsk task docs --acp" },
488
+ "gsk-sheets": { "command": "gsk task sheets --acp" }
489
+ }
490
+ }
491
+ ```
492
+
493
+ Then in Genspark Claw: `/acp spawn gsk-slides` to create and iterate on presentations via natural language.
494
+
469
495
  ### Stock Prices
470
496
 
471
497
  ### stock_price (alias: `stock`)
@@ -662,19 +688,19 @@ gsk calendar create --summary "Personal Event" --start_time "..." --end_time "..
662
688
 
663
689
  ### AI Phone Calls
664
690
 
665
- ### call
691
+ ### phone-call (alias: `call-for-me`)
666
692
 
667
693
  Make an AI phone call on your behalf. The AI validates prerequisites, resolves contact info, and initiates the call.
668
694
 
669
695
  ```bash
670
696
  # Call a business by phone number
671
- gsk call "Pizza Hut" -c "+1-555-123-4567" -p "Check if they deliver to my area"
697
+ gsk phone-call "Pizza Hut" -c "+1-555-123-4567" -p "Check if they deliver to my area"
672
698
 
673
699
  # Call a business by Google Maps place_id
674
- gsk call "Joe's Pizza" -c "ChIJxxxxxxxx" --is_place_id -p "Reserve a table for 4"
700
+ gsk phone-call "Joe's Pizza" -c "ChIJxxxxxxxx" --is_place_id -p "Reserve a table for 4"
675
701
 
676
702
  # Dry run: validate and resolve contact info without initiating the call
677
- gsk call "Pizza Hut" -c "+1-555-123-4567" -p "Check hours" --dry-run
703
+ gsk phone-call "Pizza Hut" -c "+1-555-123-4567" -p "Check hours" --dry-run
678
704
  ```
679
705
 
680
706
  | Option | Default | Description |
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ACP (Agent Client Protocol) stdio bridge for GSK task agents.
3
+ *
4
+ * Speaks ACP JSON-RPC over stdin/stdout, internally calls GSK backend APIs.
5
+ * Usage: gsk task <type> --acp
6
+ *
7
+ * Protocol: NDJSON (one JSON-RPC 2.0 message per line on stdin/stdout)
8
+ */
9
+ import type { GlobalOptions } from './types.js';
10
+ export interface PersistedSession {
11
+ sessionId: string;
12
+ taskType: string;
13
+ projectId: string;
14
+ createdAt: string;
15
+ }
16
+ export declare function getSessionStorePath(): string;
17
+ export declare function sanitizeId(id: string): string;
18
+ export declare function loadPersistedSession(sessionId: string): PersistedSession | null;
19
+ /**
20
+ * Start the ACP stdio bridge for a given task type.
21
+ */
22
+ export declare function startAcpBridge(taskType: string, globalOpts: GlobalOptions): Promise<void>;
23
+ //# sourceMappingURL=acp-serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acp-serve.d.ts","sourceRoot":"","sources":["../src/acp-serve.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,YAAY,CAAA;AAkC5D,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAU5C;AAGD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE7C;AAiBD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAU/E;AAwBD;;GAEG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,aAAa,GACxB,OAAO,CAAC,IAAI,CAAC,CAmgBf"}
@@ -0,0 +1,603 @@
1
+ /**
2
+ * ACP (Agent Client Protocol) stdio bridge for GSK task agents.
3
+ *
4
+ * Speaks ACP JSON-RPC over stdin/stdout, internally calls GSK backend APIs.
5
+ * Usage: gsk task <type> --acp
6
+ *
7
+ * Protocol: NDJSON (one JSON-RPC 2.0 message per line on stdin/stdout)
8
+ */
9
+ import { ApiClient } from './client.js';
10
+ import { debug } from './logger.js';
11
+ import * as readline from 'readline';
12
+ import * as crypto from 'crypto';
13
+ import * as fs from 'fs';
14
+ import * as pathModule from 'path';
15
+ function isRequest(msg) {
16
+ return 'id' in msg;
17
+ }
18
+ export function getSessionStorePath() {
19
+ const dir = pathModule.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.genspark-tool-cli', 'acp-sessions');
20
+ if (!fs.existsSync(dir)) {
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ }
23
+ return dir;
24
+ }
25
+ // Sanitize session ID to prevent path traversal
26
+ export function sanitizeId(id) {
27
+ return id.replace(/[^a-zA-Z0-9_-]/g, '');
28
+ }
29
+ function persistSession(session) {
30
+ if (!session.projectId)
31
+ return;
32
+ const data = {
33
+ sessionId: session.sessionId,
34
+ taskType: session.taskType,
35
+ projectId: session.projectId,
36
+ createdAt: new Date().toISOString(),
37
+ };
38
+ const filePath = pathModule.join(getSessionStorePath(), `${sanitizeId(session.sessionId)}.json`);
39
+ fs.writeFileSync(filePath, JSON.stringify(data));
40
+ }
41
+ export function loadPersistedSession(sessionId) {
42
+ const safe = sanitizeId(sessionId);
43
+ if (!safe)
44
+ return null;
45
+ const filePath = pathModule.join(getSessionStorePath(), `${safe}.json`);
46
+ if (!fs.existsSync(filePath))
47
+ return null;
48
+ try {
49
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ function listPersistedSessions(taskType) {
56
+ const dir = getSessionStorePath();
57
+ const sessions = [];
58
+ if (!fs.existsSync(dir))
59
+ return sessions;
60
+ for (const file of fs.readdirSync(dir)) {
61
+ if (!file.endsWith('.json'))
62
+ continue;
63
+ try {
64
+ const data = JSON.parse(fs.readFileSync(pathModule.join(dir, file), 'utf-8'));
65
+ if (!taskType || data.taskType === taskType) {
66
+ sessions.push(data);
67
+ }
68
+ }
69
+ catch {
70
+ // skip corrupted files
71
+ }
72
+ }
73
+ return sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
74
+ }
75
+ /**
76
+ * Start the ACP stdio bridge for a given task type.
77
+ */
78
+ export async function startAcpBridge(taskType, globalOpts) {
79
+ const client = new ApiClient(globalOpts);
80
+ const session = {
81
+ sessionId: crypto.randomUUID(),
82
+ taskType,
83
+ projectId: null,
84
+ };
85
+ let promptInFlight = false;
86
+ let stdinClosed = false;
87
+ let currentAbortController = null;
88
+ const rl = readline.createInterface({
89
+ input: process.stdin,
90
+ terminal: false,
91
+ });
92
+ function send(obj) {
93
+ const line = JSON.stringify({ jsonrpc: '2.0', ...obj });
94
+ process.stdout.write(line + '\n');
95
+ }
96
+ function sendResponse(id, result) {
97
+ send({ id, result });
98
+ }
99
+ function sendError(id, code, message) {
100
+ send({ id, error: { code, message } });
101
+ }
102
+ function sendNotification(method, params) {
103
+ send({ method, params });
104
+ }
105
+ function sendAgentMessage(sessionId, text) {
106
+ sendNotification('session/update', {
107
+ sessionId,
108
+ update: {
109
+ sessionUpdate: 'agent_message_chunk',
110
+ content: { type: 'text', text },
111
+ },
112
+ });
113
+ }
114
+ function sendToolCall(sessionId, toolCallId, title, status) {
115
+ sendNotification('session/update', {
116
+ sessionId,
117
+ update: {
118
+ sessionUpdate: 'tool_call',
119
+ toolCallId,
120
+ title,
121
+ kind: 'other',
122
+ status,
123
+ },
124
+ });
125
+ }
126
+ function sendToolCallUpdate(sessionId, toolCallId, status, content) {
127
+ const update = {
128
+ sessionUpdate: 'tool_call_update',
129
+ toolCallId,
130
+ status,
131
+ };
132
+ if (content) {
133
+ update.content = [
134
+ { type: 'content', content: { type: 'text', text: content } },
135
+ ];
136
+ }
137
+ sendNotification('session/update', { sessionId, update });
138
+ }
139
+ // --- Handler: initialize ---
140
+ async function handleInitialize(msg) {
141
+ sendResponse(msg.id, {
142
+ protocolVersion: 1,
143
+ agentCapabilities: {
144
+ loadSession: true,
145
+ sessionCapabilities: { list: true },
146
+ promptCapabilities: {},
147
+ },
148
+ agentInfo: {
149
+ name: `gsk-${taskType}`,
150
+ title: `Genspark ${taskType.charAt(0).toUpperCase() + taskType.slice(1)} Agent`,
151
+ version: '1.0.0',
152
+ },
153
+ authMethods: [],
154
+ });
155
+ }
156
+ // --- Handler: session/new ---
157
+ async function handleSessionNew(msg) {
158
+ if (promptInFlight) {
159
+ sendError(msg.id, -32603, 'Cannot create new session while a prompt is in progress');
160
+ return;
161
+ }
162
+ session.sessionId = crypto.randomUUID();
163
+ session.projectId = null;
164
+ sendResponse(msg.id, {
165
+ sessionId: session.sessionId,
166
+ configOptions: [],
167
+ modes: {
168
+ currentModeId: 'default',
169
+ availableModes: [
170
+ {
171
+ id: 'default',
172
+ name: 'Default',
173
+ description: `Create and iterate on ${taskType}`,
174
+ },
175
+ ],
176
+ },
177
+ });
178
+ }
179
+ // --- Handler: session/load ---
180
+ async function handleSessionLoad(msg) {
181
+ if (promptInFlight) {
182
+ sendError(msg.id, -32603, 'Cannot load session while a prompt is in progress');
183
+ return;
184
+ }
185
+ const params = msg.params || {};
186
+ const targetSessionId = params.sessionId;
187
+ if (!targetSessionId) {
188
+ sendError(msg.id, -32602, 'sessionId is required');
189
+ return;
190
+ }
191
+ const persisted = loadPersistedSession(targetSessionId);
192
+ if (!persisted) {
193
+ sendError(msg.id, -32002, `Session not found: ${targetSessionId}`);
194
+ return;
195
+ }
196
+ // Validate task type matches this bridge's type
197
+ if (persisted.taskType !== taskType) {
198
+ sendError(msg.id, -32602, `Task type mismatch: session is ${persisted.taskType}, but this bridge is ${taskType}`);
199
+ return;
200
+ }
201
+ // Restore session state
202
+ session.sessionId = persisted.sessionId;
203
+ session.projectId = persisted.projectId;
204
+ // Check if there's an in-progress agent and reconnect to its event stream.
205
+ // This must complete BEFORE sending the session/load response, otherwise
206
+ // the caller may send session/prompt while promptInFlight is still true,
207
+ // causing a "A prompt is already in progress" race condition.
208
+ if (session.projectId) {
209
+ promptInFlight = true;
210
+ currentAbortController = new AbortController();
211
+ try {
212
+ const eventsResult = await client.agentAskEvents(session.projectId, session.taskType, undefined, currentAbortController.signal);
213
+ if (eventsResult.status === 'ok') {
214
+ const data = eventsResult.data;
215
+ const text = extractResponseText(data);
216
+ if (text && text !== '{}') {
217
+ // Intentional: this notification is sent before the session/load
218
+ // response below. JSON-RPC allows notifications at any time and
219
+ // they carry sessionId for correlation. The response must come
220
+ // after the probe so the caller doesn't race with session/prompt.
221
+ sendAgentMessage(session.sessionId, text);
222
+ }
223
+ if (eventsResult.message === 'no_running_agent') {
224
+ debug('[ACP] Agent finished, replayed last result');
225
+ }
226
+ else {
227
+ debug('[ACP] Reconnected to in-progress agent and got result');
228
+ }
229
+ }
230
+ else {
231
+ debug('[ACP] No in-progress agent, session ready for new prompts');
232
+ }
233
+ }
234
+ catch {
235
+ debug('[ACP] Failed to check for in-progress agent, continuing');
236
+ }
237
+ finally {
238
+ promptInFlight = false;
239
+ currentAbortController = null;
240
+ }
241
+ }
242
+ // Send response AFTER reconnect probe completes, so the caller knows
243
+ // it's safe to send session/prompt without hitting promptInFlight guard.
244
+ sendResponse(msg.id, {
245
+ sessionId: session.sessionId,
246
+ configOptions: [],
247
+ modes: {
248
+ currentModeId: 'default',
249
+ availableModes: [
250
+ {
251
+ id: 'default',
252
+ name: 'Default',
253
+ description: `Create and iterate on ${taskType}`,
254
+ },
255
+ ],
256
+ },
257
+ });
258
+ if (stdinClosed)
259
+ process.exit(0);
260
+ }
261
+ // --- Handler: session/list ---
262
+ async function handleSessionList(msg) {
263
+ const sessions = listPersistedSessions(taskType);
264
+ sendResponse(msg.id, sessions.map(s => ({
265
+ sessionId: s.sessionId,
266
+ taskType: s.taskType,
267
+ projectId: s.projectId,
268
+ createdAt: s.createdAt,
269
+ })));
270
+ }
271
+ // --- Handler: session/prompt ---
272
+ async function handleSessionPrompt(msg) {
273
+ const params = msg.params || {};
274
+ const promptParts = params.prompt;
275
+ const text = promptParts
276
+ ?.filter(p => p.type === 'text' && p.text)
277
+ .map(p => p.text)
278
+ .join('\n') || '';
279
+ const useModel = params.use_model || undefined;
280
+ // Reject concurrent prompts — ACP processes one prompt at a time
281
+ if (promptInFlight) {
282
+ sendError(msg.id, -32603, 'A prompt is already in progress');
283
+ return;
284
+ }
285
+ if (!text) {
286
+ sendError(msg.id, -32602, 'Empty prompt');
287
+ return;
288
+ }
289
+ promptInFlight = true;
290
+ currentAbortController = new AbortController();
291
+ const toolCallId = `task-${Date.now()}`;
292
+ const activeAgentToolCalls = [];
293
+ try {
294
+ // Announce tool call for progress visibility
295
+ sendToolCall(session.sessionId, toolCallId, `Processing ${taskType} request`, 'pending');
296
+ let streamedContent = '';
297
+ const responseText = await askAgent(client, session, text, currentAbortController.signal, progressMsg => {
298
+ sendToolCallUpdate(session.sessionId, toolCallId, 'in_progress', progressMsg);
299
+ },
300
+ // Stream deltas as agent_message_chunk in real-time
301
+ delta => {
302
+ sendAgentMessage(session.sessionId, delta);
303
+ streamedContent += delta;
304
+ },
305
+ // Show agent's internal tool calls as ACP tool_call notifications
306
+ (tcId, tcName) => {
307
+ // Mark previous agent tool calls as completed
308
+ for (const prevId of activeAgentToolCalls) {
309
+ sendToolCallUpdate(session.sessionId, prevId, 'completed');
310
+ }
311
+ activeAgentToolCalls.length = 0;
312
+ // Start new tool call
313
+ sendToolCall(session.sessionId, tcId, tcName, 'in_progress');
314
+ activeAgentToolCalls.push(tcId);
315
+ // Also emit as text delta so the tool name is visible in the
316
+ // OpenClaw web UI. The gateway's ACP event callback only
317
+ // forwards text_delta events, so tool_call notifications are
318
+ // invisible to WS clients without this.
319
+ sendAgentMessage(session.sessionId, `\nUsing Tool: ${tcName}\n`);
320
+ }, useModel,
321
+ // Persist session as soon as project_id is available so
322
+ // session/load works even if the stream disconnects mid-flight
323
+ () => {
324
+ persistSession(session);
325
+ });
326
+ // Mark remaining agent tool calls as completed
327
+ for (const prevId of activeAgentToolCalls) {
328
+ sendToolCallUpdate(session.sessionId, prevId, 'completed');
329
+ }
330
+ // Mark top-level tool call completed
331
+ sendToolCallUpdate(session.sessionId, toolCallId, 'completed');
332
+ // Persist session for future session/load
333
+ persistSession(session);
334
+ // Send final response only if it differs from what was streamed
335
+ // (e.g., content was replaced by message_result after deltas),
336
+ // or if nothing was streamed at all.
337
+ if (responseText && responseText !== streamedContent) {
338
+ sendAgentMessage(session.sessionId, responseText);
339
+ }
340
+ // Complete the prompt
341
+ sendResponse(msg.id, { stopReason: 'end_turn' });
342
+ }
343
+ catch (err) {
344
+ // Clean up any in-progress agent tool calls
345
+ for (const prevId of activeAgentToolCalls) {
346
+ sendToolCallUpdate(session.sessionId, prevId, 'completed');
347
+ }
348
+ if (err.name === 'AbortError') {
349
+ sendToolCallUpdate(session.sessionId, toolCallId, 'completed', 'Cancelled');
350
+ sendResponse(msg.id, { stopReason: 'cancelled' });
351
+ }
352
+ else {
353
+ sendToolCallUpdate(session.sessionId, toolCallId, 'completed', 'Error');
354
+ sendAgentMessage(session.sessionId, `Error: ${err.message}`);
355
+ sendResponse(msg.id, { stopReason: 'end_turn' });
356
+ }
357
+ }
358
+ finally {
359
+ // Ensure session is persisted even on error/cancellation, as long
360
+ // as we captured a project_id mid-stream. Wrapped in try-catch so
361
+ // a disk-full / permission error doesn't prevent the critical state
362
+ // reset below (which would permanently lock the bridge).
363
+ try {
364
+ persistSession(session);
365
+ }
366
+ catch (e) {
367
+ debug(`[ACP] Failed to persist session: ${e.message}`);
368
+ }
369
+ promptInFlight = false;
370
+ currentAbortController = null;
371
+ if (stdinClosed)
372
+ process.exit(0);
373
+ }
374
+ }
375
+ // --- Handler: session/cancel ---
376
+ function handleSessionCancel() {
377
+ if (currentAbortController) {
378
+ currentAbortController.abort();
379
+ debug('[ACP] session/cancel: aborting in-flight request');
380
+ }
381
+ }
382
+ // --- Handler: session/export ---
383
+ async function handleSessionExport(msg) {
384
+ if (!session.projectId) {
385
+ sendError(msg.id, -32603, 'No active project to export. Send a prompt first.');
386
+ return;
387
+ }
388
+ const params = msg.params || {};
389
+ const format = params.format || 'auto';
390
+ const outputPath = params.outputPath;
391
+ debug(`[ACP] session/export: format=${format}, outputPath=${outputPath || '(none)'}`);
392
+ try {
393
+ const result = await client.exportArtifact(session.projectId, session.taskType, format);
394
+ if (result.status !== 'ok' || !result.data) {
395
+ sendError(msg.id, -32603, `Export failed: ${result.message || 'unknown error'}`);
396
+ return;
397
+ }
398
+ const data = result.data;
399
+ const downloadUrl = data.download_url;
400
+ const fileName = data.file_name;
401
+ const exportFormat = data.format;
402
+ const responseData = {
403
+ download_url: downloadUrl,
404
+ file_name: fileName,
405
+ format: exportFormat,
406
+ };
407
+ // If outputPath specified, download to local file
408
+ if (outputPath && downloadUrl) {
409
+ try {
410
+ const resp = await fetch(downloadUrl);
411
+ if (resp.ok) {
412
+ const buffer = Buffer.from(await resp.arrayBuffer());
413
+ fs.writeFileSync(outputPath, buffer);
414
+ responseData.local_path = pathModule.resolve(outputPath);
415
+ responseData.size_bytes = buffer.length;
416
+ debug(`[ACP] Exported ${buffer.length} bytes → ${outputPath}`);
417
+ }
418
+ else {
419
+ responseData.download_error = `HTTP ${resp.status}`;
420
+ }
421
+ }
422
+ catch (dlErr) {
423
+ responseData.download_error = dlErr.message;
424
+ }
425
+ }
426
+ sendResponse(msg.id, responseData);
427
+ }
428
+ catch (err) {
429
+ sendError(msg.id, -32603, `Export error: ${err.message}`);
430
+ }
431
+ }
432
+ // --- Message router ---
433
+ rl.on('line', async (line) => {
434
+ if (!line.trim())
435
+ return;
436
+ let msg;
437
+ try {
438
+ msg = JSON.parse(line);
439
+ }
440
+ catch {
441
+ return;
442
+ }
443
+ if (msg.jsonrpc !== '2.0')
444
+ return;
445
+ const method = msg.method;
446
+ debug(`[ACP] Received: ${method}`);
447
+ try {
448
+ switch (method) {
449
+ case 'initialize':
450
+ if (isRequest(msg))
451
+ await handleInitialize(msg);
452
+ break;
453
+ case 'session/new':
454
+ if (isRequest(msg))
455
+ await handleSessionNew(msg);
456
+ break;
457
+ case 'session/load':
458
+ if (isRequest(msg))
459
+ await handleSessionLoad(msg);
460
+ break;
461
+ case 'session/list':
462
+ if (isRequest(msg))
463
+ await handleSessionList(msg);
464
+ break;
465
+ case 'session/prompt':
466
+ if (isRequest(msg))
467
+ await handleSessionPrompt(msg);
468
+ break;
469
+ case 'session/export':
470
+ if (isRequest(msg))
471
+ await handleSessionExport(msg);
472
+ break;
473
+ case 'session/cancel':
474
+ handleSessionCancel();
475
+ break;
476
+ default:
477
+ if (isRequest(msg)) {
478
+ sendError(msg.id, -32601, `Method not found: ${method}`);
479
+ }
480
+ }
481
+ }
482
+ catch (err) {
483
+ if (isRequest(msg)) {
484
+ sendError(msg.id, -32603, err.message);
485
+ }
486
+ }
487
+ });
488
+ rl.on('close', () => {
489
+ stdinClosed = true;
490
+ if (!promptInFlight)
491
+ process.exit(0);
492
+ });
493
+ }
494
+ /**
495
+ * Send a message to the agent via agent_ask endpoint (streaming).
496
+ * First call (no projectId) creates the project; subsequent calls continue it.
497
+ */
498
+ async function askAgent(client, session, message, signal, onProgress, onDelta, onToolCall, useModel, onProjectId) {
499
+ const result = await client.agentAsk(session.projectId, message, session.taskType, msg => {
500
+ // Capture project_id from intermediate stream message as early as
501
+ // possible so the session can be persisted before the stream ends.
502
+ if (msg.project_id && !session.projectId) {
503
+ session.projectId = msg.project_id;
504
+ debug(`[ACP] Project created (mid-stream): ${session.projectId}`);
505
+ if (onProjectId)
506
+ onProjectId();
507
+ }
508
+ if (msg.delta && onDelta) {
509
+ onDelta(msg.delta);
510
+ }
511
+ else if (msg.tool_call_name && onToolCall) {
512
+ onToolCall(msg.tool_call_id || `agent-tool-${Date.now()}`, msg.tool_call_name);
513
+ }
514
+ else if (msg.heartbeat) {
515
+ onProgress(`Processing... (${msg.elapsed_seconds}s)`);
516
+ }
517
+ }, signal, useModel);
518
+ if (result.status !== 'ok' || !result.data) {
519
+ throw new Error(result.message || 'agent_ask failed');
520
+ }
521
+ const data = result.data;
522
+ // Extract project_id from final result as fallback (in case the
523
+ // intermediate project_id message was not received, e.g. older server)
524
+ const projectId = data.project_id;
525
+ if (projectId && !session.projectId) {
526
+ session.projectId = projectId;
527
+ debug(`[ACP] Project created (final result): ${session.projectId}`);
528
+ }
529
+ return extractResponseText(data);
530
+ }
531
+ /**
532
+ * Extract human-readable response text from task result.
533
+ * Handles artifact URLs for various task types:
534
+ * - docs/slides/sheets: file_url + optional file_name
535
+ * - podcasts: file_url (audio) + video_file_url
536
+ * - image/video/audio generation: results[] array with URLs
537
+ */
538
+ function extractResponseText(data) {
539
+ const resultContent = data.result_content;
540
+ if (resultContent) {
541
+ const lastMessage = resultContent.last_message;
542
+ const parts = lastMessage && lastMessage.length > 0 ? [...lastMessage] : [];
543
+ // Artifacts are nested under result_content.artifacts
544
+ const artifacts = resultContent.artifacts;
545
+ if (artifacts) {
546
+ // Single file artifact (docs, slides, sheets, podcasts audio)
547
+ const fileUrl = artifacts.file_url;
548
+ if (fileUrl) {
549
+ const fileName = artifacts.file_name;
550
+ parts.push(fileName ? `\nFile: ${fileUrl} (${fileName})` : `\nFile: ${fileUrl}`);
551
+ }
552
+ // Podcasts video
553
+ const videoFileUrl = artifacts.video_file_url;
554
+ if (videoFileUrl) {
555
+ parts.push(`Video: ${videoFileUrl}`);
556
+ }
557
+ // Media generation results (image/video/audio arrays)
558
+ const results = artifacts.results;
559
+ if (results && results.length > 0) {
560
+ parts.push(''); // blank line separator
561
+ for (const item of results) {
562
+ const url = (item.image_url ||
563
+ item.video_url ||
564
+ item.audio_url ||
565
+ item.url);
566
+ const prompt = item.prompt;
567
+ if (url) {
568
+ parts.push(prompt ? `- ${url} (${prompt})` : `- ${url}`);
569
+ }
570
+ }
571
+ }
572
+ // Append any remaining artifact fields as JSON
573
+ const knownKeys = new Set([
574
+ 'file_url',
575
+ 'file_name',
576
+ 'video_file_url',
577
+ 'results',
578
+ ]);
579
+ const extra = {};
580
+ for (const [k, v] of Object.entries(artifacts)) {
581
+ if (!knownKeys.has(k) && v)
582
+ extra[k] = v;
583
+ }
584
+ if (Object.keys(extra).length > 0) {
585
+ parts.push(`\n${JSON.stringify(extra)}`);
586
+ }
587
+ }
588
+ // Artifacts note (project URL etc.)
589
+ const artifactsNote = resultContent.artifacts_note;
590
+ if (artifactsNote) {
591
+ parts.push(`\n${artifactsNote}`);
592
+ }
593
+ if (parts.length > 0) {
594
+ return parts.join('\n');
595
+ }
596
+ }
597
+ const msg = data.message;
598
+ if (msg)
599
+ return msg;
600
+ // Don't expose raw JSON to the user
601
+ return '';
602
+ }
603
+ //# sourceMappingURL=acp-serve.js.map