@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 +30 -4
- package/dist/acp-serve.d.ts +23 -0
- package/dist/acp-serve.d.ts.map +1 -0
- package/dist/acp-serve.js +603 -0
- package/dist/acp-serve.js.map +1 -0
- package/dist/client.d.ts +26 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +87 -22
- package/dist/client.js.map +1 -1
- package/dist/index.js +136 -19
- package/dist/index.js.map +1 -1
- package/docs/skills.md +1 -1
- package/package.json +1 -1
- package/skills/gsk-acp-agents/SKILL.md +179 -0
- package/skills/gsk-aidrive/SKILL.md +1 -1
- package/skills/gsk-analyze-media/SKILL.md +1 -1
- package/skills/gsk-audio-generation/SKILL.md +1 -1
- package/skills/gsk-audio-transcribe/SKILL.md +1 -1
- package/skills/gsk-crawler/SKILL.md +2 -1
- package/skills/gsk-create-task/SKILL.md +1 -1
- package/skills/gsk-email-read/SKILL.md +2 -0
- package/skills/gsk-email-send/SKILL.md +29 -6
- package/skills/gsk-get-service-url/SKILL.md +1 -1
- package/skills/gsk-image-generation/SKILL.md +1 -1
- package/skills/gsk-image-search/SKILL.md +1 -1
- package/skills/gsk-phone-call/SKILL.md +6 -4
- package/skills/gsk-shared/SKILL.md +18 -0
- package/skills/gsk-stock-price/SKILL.md +1 -1
- package/skills/gsk-summarize-large-document/SKILL.md +1 -1
- package/skills/gsk-understand-images/SKILL.md +1 -1
- package/skills/gsk-video-generation/SKILL.md +1 -1
- package/skills/gsk-web-search/SKILL.md +1 -1
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
|