@geminilight/mindos 0.6.28 → 0.6.30
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 +10 -4
- package/app/app/api/a2a/agents/route.ts +9 -0
- package/app/app/api/a2a/delegations/route.ts +9 -0
- package/app/app/api/a2a/discover/route.ts +2 -0
- package/app/app/api/a2a/route.ts +6 -6
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +114 -0
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/registry/route.ts +31 -0
- package/app/app/api/acp/session/route.ts +185 -0
- package/app/app/api/ask/route.ts +116 -13
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/layout.tsx +2 -0
- package/app/app/page.tsx +7 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/DirView.tsx +64 -2
- package/app/components/FileTree.tsx +40 -10
- package/app/components/GuideCard.tsx +7 -17
- package/app/components/HomeContent.tsx +1 -0
- package/app/components/MarkdownView.tsx +2 -0
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/SearchModal.tsx +234 -80
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/agents/AgentDetailContent.tsx +266 -52
- package/app/components/agents/AgentsContentPage.tsx +32 -6
- package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/SkillDetailPopover.tsx +4 -9
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/help/HelpContent.tsx +9 -9
- package/app/components/panels/AgentsPanel.tsx +2 -0
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
- package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
- package/app/components/panels/EchoPanel.tsx +5 -1
- package/app/components/panels/EchoSidebarStats.tsx +136 -0
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +177 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/KnowledgeTab.tsx +3 -6
- package/app/components/settings/McpSkillsSection.tsx +4 -5
- package/app/components/settings/McpTab.tsx +6 -8
- package/app/components/setup/StepSecurity.tsx +4 -5
- package/app/components/setup/index.tsx +5 -11
- package/app/components/ui/Toaster.tsx +39 -0
- package/app/hooks/useA2aRegistry.ts +6 -1
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +120 -0
- package/app/hooks/useAcpRegistry.ts +86 -0
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useDelegationHistory.ts +49 -0
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/a2a/client.ts +49 -5
- package/app/lib/a2a/orchestrator.ts +0 -1
- package/app/lib/a2a/task-handler.ts +4 -4
- package/app/lib/a2a/types.ts +15 -0
- package/app/lib/acp/acp-tools.ts +95 -0
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +144 -0
- package/app/lib/acp/index.ts +40 -0
- package/app/lib/acp/registry.ts +202 -0
- package/app/lib/acp/session.ts +717 -0
- package/app/lib/acp/subprocess.ts +495 -0
- package/app/lib/acp/types.ts +274 -0
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +2 -1
- package/app/lib/i18n/_core.ts +22 -0
- package/app/lib/i18n/index.ts +35 -0
- package/app/lib/i18n/modules/ai-chat.ts +215 -0
- package/app/lib/i18n/modules/common.ts +71 -0
- package/app/lib/i18n/modules/features.ts +153 -0
- package/app/lib/i18n/modules/knowledge.ts +429 -0
- package/app/lib/i18n/modules/navigation.ts +153 -0
- package/app/lib/i18n/modules/onboarding.ts +523 -0
- package/app/lib/i18n/modules/panels.ts +1196 -0
- package/app/lib/i18n/modules/settings.ts +585 -0
- package/app/lib/i18n-en.ts +2 -1518
- package/app/lib/i18n-zh.ts +2 -1542
- package/app/lib/i18n.ts +3 -6
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/toast.ts +79 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +3 -1
- package/bin/cli.js +25 -25
- package/bin/commands/file.js +29 -2
- package/bin/commands/space.js +249 -91
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP Subprocess Manager — Spawn and communicate with ACP agent processes.
|
|
3
|
+
* ACP agents communicate via JSON-RPC 2.0 over stdio (stdin/stdout).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
7
|
+
import type {
|
|
8
|
+
AcpJsonRpcRequest,
|
|
9
|
+
AcpJsonRpcResponse,
|
|
10
|
+
AcpRegistryEntry,
|
|
11
|
+
} from './types';
|
|
12
|
+
import { resolveAgentCommand } from './agent-descriptors';
|
|
13
|
+
import { readSettings } from '../settings';
|
|
14
|
+
|
|
15
|
+
/** Incoming JSON-RPC request from agent (bidirectional — agent asks US for permission). */
|
|
16
|
+
export interface AcpIncomingRequest {
|
|
17
|
+
jsonrpc: '2.0';
|
|
18
|
+
id: string | number;
|
|
19
|
+
method: string;
|
|
20
|
+
params?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ── Types ─────────────────────────────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
export interface AcpProcess {
|
|
26
|
+
id: string;
|
|
27
|
+
agentId: string;
|
|
28
|
+
proc: ChildProcess;
|
|
29
|
+
alive: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type MessageCallback = (msg: AcpJsonRpcResponse) => void;
|
|
33
|
+
type RequestCallback = (req: AcpIncomingRequest) => void;
|
|
34
|
+
|
|
35
|
+
/* ── State ─────────────────────────────────────────────────────────────── */
|
|
36
|
+
|
|
37
|
+
const processes = new Map<string, AcpProcess>();
|
|
38
|
+
const messageListeners = new Map<string, Set<MessageCallback>>();
|
|
39
|
+
const requestListeners = new Map<string, Set<RequestCallback>>();
|
|
40
|
+
let rpcIdCounter = 1;
|
|
41
|
+
|
|
42
|
+
/* ── Public API ────────────────────────────────────────────────────────── */
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Spawn an ACP agent subprocess.
|
|
46
|
+
*/
|
|
47
|
+
export function spawnAcpAgent(
|
|
48
|
+
entry: AcpRegistryEntry,
|
|
49
|
+
options?: { env?: Record<string, string>; cwd?: string },
|
|
50
|
+
): AcpProcess {
|
|
51
|
+
const settings = readSettings();
|
|
52
|
+
const userOverride = settings.acpAgents?.[entry.id];
|
|
53
|
+
const resolved = resolveAgentCommand(entry.id, entry, userOverride);
|
|
54
|
+
const { cmd, args } = { cmd: resolved.cmd, args: resolved.args };
|
|
55
|
+
|
|
56
|
+
const mergedEnv = {
|
|
57
|
+
...process.env,
|
|
58
|
+
...(entry.env ?? {}),
|
|
59
|
+
...(resolved.env ?? {}),
|
|
60
|
+
...(options?.env ?? {}),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const proc = spawn(cmd, args, {
|
|
64
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
65
|
+
env: mergedEnv,
|
|
66
|
+
shell: false,
|
|
67
|
+
...(options?.cwd ? { cwd: options.cwd } : {}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const id = `acp-${entry.id}-${Date.now()}`;
|
|
71
|
+
const acpProc: AcpProcess = { id, agentId: entry.id, proc, alive: true };
|
|
72
|
+
|
|
73
|
+
processes.set(id, acpProc);
|
|
74
|
+
messageListeners.set(id, new Set());
|
|
75
|
+
requestListeners.set(id, new Set());
|
|
76
|
+
|
|
77
|
+
// Parse newline-delimited JSON from stdout
|
|
78
|
+
let buffer = '';
|
|
79
|
+
proc.stdout?.on('data', (chunk: Buffer) => {
|
|
80
|
+
buffer += chunk.toString();
|
|
81
|
+
const lines = buffer.split('\n');
|
|
82
|
+
buffer = lines.pop() ?? '';
|
|
83
|
+
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed) continue;
|
|
87
|
+
try {
|
|
88
|
+
const msg = JSON.parse(trimmed);
|
|
89
|
+
|
|
90
|
+
// Distinguish incoming requests (agent → client) from responses (to our requests).
|
|
91
|
+
// Requests have `method` and `id` but no `result`/`error`.
|
|
92
|
+
const isRequest = msg.method && msg.id !== undefined
|
|
93
|
+
&& !('result' in msg) && !('error' in msg);
|
|
94
|
+
|
|
95
|
+
if (isRequest) {
|
|
96
|
+
const reqListeners = requestListeners.get(id);
|
|
97
|
+
if (reqListeners) {
|
|
98
|
+
for (const cb of reqListeners) cb(msg as AcpIncomingRequest);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
const listeners = messageListeners.get(id);
|
|
102
|
+
if (listeners) {
|
|
103
|
+
for (const cb of listeners) cb(msg as AcpJsonRpcResponse);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Not valid JSON — skip (could be agent debug output)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Capture stderr for debugging (agents may log startup errors there)
|
|
113
|
+
let stderrBuf = '';
|
|
114
|
+
proc.stderr?.on('data', (chunk: Buffer) => {
|
|
115
|
+
stderrBuf += chunk.toString();
|
|
116
|
+
// Keep only last 4KB to avoid unbounded memory
|
|
117
|
+
if (stderrBuf.length > 4096) stderrBuf = stderrBuf.slice(-4096);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
proc.on('close', (code) => {
|
|
121
|
+
acpProc.alive = false;
|
|
122
|
+
if (code && code !== 0 && stderrBuf.trim()) {
|
|
123
|
+
console.error(`[ACP] ${entry.id} exited with code ${code}: ${stderrBuf.trim().slice(0, 500)}`);
|
|
124
|
+
}
|
|
125
|
+
messageListeners.delete(id);
|
|
126
|
+
requestListeners.delete(id);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
proc.on('error', (err) => {
|
|
130
|
+
acpProc.alive = false;
|
|
131
|
+
console.error(`[ACP] ${entry.id} spawn error:`, err.message);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return acpProc;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Send a JSON-RPC message to an ACP agent's stdin.
|
|
139
|
+
*/
|
|
140
|
+
export function sendMessage(acpProc: AcpProcess, method: string, params?: Record<string, unknown>): string {
|
|
141
|
+
if (!acpProc.alive || !acpProc.proc.stdin?.writable) {
|
|
142
|
+
throw new Error(`ACP process ${acpProc.id} is not alive`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const id = `rpc-${rpcIdCounter++}`;
|
|
146
|
+
const request: AcpJsonRpcRequest = {
|
|
147
|
+
jsonrpc: '2.0',
|
|
148
|
+
id,
|
|
149
|
+
method,
|
|
150
|
+
...(params ? { params } : {}),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
acpProc.proc.stdin.write(JSON.stringify(request) + '\n');
|
|
154
|
+
return id;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Register a callback for messages from an ACP agent.
|
|
159
|
+
* Returns an unsubscribe function.
|
|
160
|
+
*/
|
|
161
|
+
export function onMessage(acpProc: AcpProcess, callback: MessageCallback): () => void {
|
|
162
|
+
const listeners = messageListeners.get(acpProc.id);
|
|
163
|
+
if (!listeners) throw new Error(`ACP process ${acpProc.id} not found`);
|
|
164
|
+
|
|
165
|
+
listeners.add(callback);
|
|
166
|
+
return () => { listeners.delete(callback); };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Send a JSON-RPC request and wait for a response with the matching ID.
|
|
171
|
+
*/
|
|
172
|
+
export function sendAndWait(
|
|
173
|
+
acpProc: AcpProcess,
|
|
174
|
+
method: string,
|
|
175
|
+
params?: Record<string, unknown>,
|
|
176
|
+
timeoutMs = 30_000,
|
|
177
|
+
): Promise<AcpJsonRpcResponse> {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const rpcId = sendMessage(acpProc, method, params);
|
|
180
|
+
|
|
181
|
+
const timer = setTimeout(() => {
|
|
182
|
+
unsub();
|
|
183
|
+
reject(new Error(`ACP RPC timeout after ${timeoutMs}ms for method: ${method}`));
|
|
184
|
+
}, timeoutMs);
|
|
185
|
+
|
|
186
|
+
const unsub = onMessage(acpProc, (msg) => {
|
|
187
|
+
if (String(msg.id) === rpcId) {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
unsub();
|
|
190
|
+
resolve(msg);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Kill an ACP agent process.
|
|
198
|
+
*/
|
|
199
|
+
export function killAgent(acpProc: AcpProcess): void {
|
|
200
|
+
if (acpProc.alive && acpProc.proc.pid) {
|
|
201
|
+
acpProc.proc.kill('SIGTERM');
|
|
202
|
+
// Force kill after 5s if still alive
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
if (acpProc.alive) {
|
|
205
|
+
acpProc.proc.kill('SIGKILL');
|
|
206
|
+
}
|
|
207
|
+
}, 5000);
|
|
208
|
+
}
|
|
209
|
+
acpProc.alive = false;
|
|
210
|
+
processes.delete(acpProc.id);
|
|
211
|
+
messageListeners.delete(acpProc.id);
|
|
212
|
+
requestListeners.delete(acpProc.id);
|
|
213
|
+
// Clean up any terminals spawned by this process
|
|
214
|
+
const terms = terminalMaps.get(acpProc.id);
|
|
215
|
+
if (terms) {
|
|
216
|
+
for (const entry of terms.values()) {
|
|
217
|
+
if (entry.child.exitCode === null) entry.child.kill('SIGTERM');
|
|
218
|
+
}
|
|
219
|
+
terminalMaps.delete(acpProc.id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get a process by its ID.
|
|
225
|
+
*/
|
|
226
|
+
export function getProcess(id: string): AcpProcess | undefined {
|
|
227
|
+
return processes.get(id);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get all active processes.
|
|
232
|
+
*/
|
|
233
|
+
export function getActiveProcesses(): AcpProcess[] {
|
|
234
|
+
return [...processes.values()].filter(p => p.alive);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Kill all active ACP processes. Used for cleanup.
|
|
239
|
+
*/
|
|
240
|
+
export function killAllAgents(): void {
|
|
241
|
+
for (const proc of processes.values()) {
|
|
242
|
+
killAgent(proc);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Register a callback for incoming JSON-RPC REQUESTS from the agent
|
|
248
|
+
* (bidirectional: agent asks client for permission / capability).
|
|
249
|
+
* Returns an unsubscribe function.
|
|
250
|
+
*/
|
|
251
|
+
export function onRequest(acpProc: AcpProcess, callback: RequestCallback): () => void {
|
|
252
|
+
const listeners = requestListeners.get(acpProc.id);
|
|
253
|
+
if (!listeners) throw new Error(`ACP process ${acpProc.id} not found`);
|
|
254
|
+
|
|
255
|
+
listeners.add(callback);
|
|
256
|
+
return () => { listeners.delete(callback); };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Send a raw JSON-RPC response back to the agent's stdin.
|
|
261
|
+
* Used for replying to incoming requests (e.g. permission approvals).
|
|
262
|
+
*/
|
|
263
|
+
export function sendResponse(
|
|
264
|
+
acpProc: AcpProcess,
|
|
265
|
+
id: string | number,
|
|
266
|
+
result: unknown,
|
|
267
|
+
): void {
|
|
268
|
+
if (!acpProc.alive || !acpProc.proc.stdin?.writable) {
|
|
269
|
+
throw new Error(`ACP process ${acpProc.id} is not alive`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const response: AcpJsonRpcResponse = {
|
|
273
|
+
jsonrpc: '2.0',
|
|
274
|
+
id,
|
|
275
|
+
result,
|
|
276
|
+
};
|
|
277
|
+
acpProc.proc.stdin.write(JSON.stringify(response) + '\n');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Install auto-approval for all incoming permission/capability requests.
|
|
282
|
+
* Agents in ACP mode send requests like fs/read, fs/write, terminal/execute etc.
|
|
283
|
+
* Without approval, the agent hangs waiting for TTY input that never comes.
|
|
284
|
+
* Returns an unsubscribe function.
|
|
285
|
+
*/
|
|
286
|
+
export function installAutoApproval(acpProc: AcpProcess): () => void {
|
|
287
|
+
return onRequest(acpProc, (req) => {
|
|
288
|
+
const method = req.method;
|
|
289
|
+
const params = (req.params ?? {}) as Record<string, unknown>;
|
|
290
|
+
|
|
291
|
+
switch (method) {
|
|
292
|
+
// ── File system: read ──
|
|
293
|
+
case 'fs/read_text_file': {
|
|
294
|
+
const filePath = String(params.path ?? '');
|
|
295
|
+
if (!filePath) {
|
|
296
|
+
sendResponse(acpProc, req.id, { error: { code: -32602, message: 'path is required' } });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const fs = require('fs');
|
|
301
|
+
const line = typeof params.line === 'number' ? params.line : undefined;
|
|
302
|
+
const limit = typeof params.limit === 'number' ? params.limit : undefined;
|
|
303
|
+
let content = fs.readFileSync(filePath, 'utf-8') as string;
|
|
304
|
+
if (line !== undefined || limit !== undefined) {
|
|
305
|
+
const lines = content.split('\n');
|
|
306
|
+
const start = (line ?? 1) - 1; // 1-based to 0-based
|
|
307
|
+
const end = limit !== undefined ? start + limit : lines.length;
|
|
308
|
+
content = lines.slice(Math.max(0, start), end).join('\n');
|
|
309
|
+
}
|
|
310
|
+
sendResponse(acpProc, req.id, { content });
|
|
311
|
+
} catch (err) {
|
|
312
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: (err as Error).message } });
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── File system: write ──
|
|
318
|
+
case 'fs/write_text_file': {
|
|
319
|
+
const filePath = String(params.path ?? '');
|
|
320
|
+
const content = String(params.content ?? '');
|
|
321
|
+
if (!filePath) {
|
|
322
|
+
sendResponse(acpProc, req.id, { error: { code: -32602, message: 'path is required' } });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const fs = require('fs');
|
|
327
|
+
const path = require('path');
|
|
328
|
+
const dir = path.dirname(filePath);
|
|
329
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
330
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
331
|
+
sendResponse(acpProc, req.id, {});
|
|
332
|
+
} catch (err) {
|
|
333
|
+
sendResponse(acpProc, req.id, { error: { code: -32603, message: (err as Error).message } });
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── Terminal: create ──
|
|
339
|
+
case 'terminal/create': {
|
|
340
|
+
const command = String(params.command ?? '');
|
|
341
|
+
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
342
|
+
const cwd = typeof params.cwd === 'string' ? params.cwd : undefined;
|
|
343
|
+
const env = (params.env && typeof params.env === 'object') ? params.env as Record<string, string> : undefined;
|
|
344
|
+
const outputByteLimit = typeof params.outputByteLimit === 'number' ? params.outputByteLimit : 1_000_000;
|
|
345
|
+
|
|
346
|
+
if (!command) {
|
|
347
|
+
sendResponse(acpProc, req.id, { error: { code: -32602, message: 'command is required' } });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const terminalId = `term-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
352
|
+
try {
|
|
353
|
+
const { spawn: spawnChild } = require('child_process');
|
|
354
|
+
const child = spawnChild(command, args, {
|
|
355
|
+
cwd,
|
|
356
|
+
env: { ...process.env, ...(env ?? {}) },
|
|
357
|
+
shell: true,
|
|
358
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
let output = '';
|
|
362
|
+
let truncated = false;
|
|
363
|
+
|
|
364
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
365
|
+
if (output.length < outputByteLimit) {
|
|
366
|
+
output += chunk.toString();
|
|
367
|
+
if (output.length > outputByteLimit) {
|
|
368
|
+
output = output.slice(0, outputByteLimit);
|
|
369
|
+
truncated = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
374
|
+
if (output.length < outputByteLimit) {
|
|
375
|
+
output += chunk.toString();
|
|
376
|
+
if (output.length > outputByteLimit) {
|
|
377
|
+
output = output.slice(0, outputByteLimit);
|
|
378
|
+
truncated = true;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Store terminal in process-scoped map
|
|
384
|
+
const terminalMap = getOrCreateTerminalMap(acpProc.id);
|
|
385
|
+
terminalMap.set(terminalId, { child, output: () => output, truncated: () => truncated });
|
|
386
|
+
|
|
387
|
+
sendResponse(acpProc, req.id, { terminalId });
|
|
388
|
+
} catch (err) {
|
|
389
|
+
sendResponse(acpProc, req.id, { error: { code: -32603, message: (err as Error).message } });
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── Terminal: output ──
|
|
395
|
+
case 'terminal/output': {
|
|
396
|
+
const terminalId = String(params.terminalId ?? '');
|
|
397
|
+
const terminal = getTerminal(acpProc.id, terminalId);
|
|
398
|
+
if (!terminal) {
|
|
399
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const exitStatus = terminal.child.exitCode !== null ? { exitCode: terminal.child.exitCode } : undefined;
|
|
403
|
+
sendResponse(acpProc, req.id, { output: terminal.output(), truncated: terminal.truncated(), exitStatus });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Terminal: kill ──
|
|
408
|
+
case 'terminal/kill': {
|
|
409
|
+
const terminalId = String(params.terminalId ?? '');
|
|
410
|
+
const terminal = getTerminal(acpProc.id, terminalId);
|
|
411
|
+
if (!terminal) {
|
|
412
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
terminal.child.kill('SIGTERM');
|
|
416
|
+
sendResponse(acpProc, req.id, {});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Terminal: wait_for_exit ──
|
|
421
|
+
case 'terminal/wait_for_exit': {
|
|
422
|
+
const terminalId = String(params.terminalId ?? '');
|
|
423
|
+
const terminal = getTerminal(acpProc.id, terminalId);
|
|
424
|
+
if (!terminal) {
|
|
425
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (terminal.child.exitCode !== null) {
|
|
429
|
+
sendResponse(acpProc, req.id, { exitCode: terminal.child.exitCode, signal: terminal.child.signalCode });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
terminal.child.on('exit', (code: number | null, signal: string | null) => {
|
|
433
|
+
sendResponse(acpProc, req.id, { exitCode: code, signal });
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── Terminal: release ──
|
|
439
|
+
case 'terminal/release': {
|
|
440
|
+
const terminalId = String(params.terminalId ?? '');
|
|
441
|
+
const terminal = getTerminal(acpProc.id, terminalId);
|
|
442
|
+
if (!terminal) {
|
|
443
|
+
sendResponse(acpProc, req.id, { error: { code: -32002, message: `Terminal not found: ${terminalId}` } });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (terminal.child.exitCode === null) terminal.child.kill('SIGTERM');
|
|
447
|
+
removeTerminal(acpProc.id, terminalId);
|
|
448
|
+
sendResponse(acpProc, req.id, {});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Permission requests (auto-approve all) ──
|
|
453
|
+
case 'session/request_permission': {
|
|
454
|
+
console.log(`[ACP] Auto-approving permission: ${JSON.stringify(params.toolCall ?? {}).slice(0, 200)}`);
|
|
455
|
+
sendResponse(acpProc, req.id, { outcome: { selected: { optionId: 'allow_once' } } });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Unknown methods: auto-approve for backwards compat ──
|
|
460
|
+
default: {
|
|
461
|
+
console.log(`[ACP] Auto-approving unknown agent request: ${method} (id=${req.id})`);
|
|
462
|
+
sendResponse(acpProc, req.id, {});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ── Terminal management (per ACP process) ─────────────────────────────── */
|
|
469
|
+
|
|
470
|
+
interface TerminalEntry {
|
|
471
|
+
child: import('child_process').ChildProcess;
|
|
472
|
+
output: () => string;
|
|
473
|
+
truncated: () => boolean;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const terminalMaps = new Map<string, Map<string, TerminalEntry>>();
|
|
477
|
+
|
|
478
|
+
function getOrCreateTerminalMap(procId: string): Map<string, TerminalEntry> {
|
|
479
|
+
let map = terminalMaps.get(procId);
|
|
480
|
+
if (!map) {
|
|
481
|
+
map = new Map();
|
|
482
|
+
terminalMaps.set(procId, map);
|
|
483
|
+
}
|
|
484
|
+
return map;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getTerminal(procId: string, terminalId: string): TerminalEntry | undefined {
|
|
488
|
+
return terminalMaps.get(procId)?.get(terminalId);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function removeTerminal(procId: string, terminalId: string): void {
|
|
492
|
+
terminalMaps.get(procId)?.delete(terminalId);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* ── Internal — agent command resolution moved to agent-descriptors.ts ─ */
|