@victor-software-house/pi-acp 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/client-CTg5Oiz5.mjs +84 -0
- package/dist/client-CTg5Oiz5.mjs.map +1 -0
- package/dist/daemon-irIzm1zJ.mjs +189 -0
- package/dist/daemon-irIzm1zJ.mjs.map +1 -0
- package/dist/in-process-DcAV6Sgx.mjs +31 -0
- package/dist/in-process-DcAV6Sgx.mjs.map +1 -0
- package/dist/index.mjs +24 -2124
- package/dist/index.mjs.map +1 -1
- package/dist/serve-DLukbpF4.mjs +2263 -0
- package/dist/serve-DLukbpF4.mjs.map +1 -0
- package/dist/socket-BUNWxnAN.mjs +100 -0
- package/dist/socket-BUNWxnAN.mjs.map +1 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,2097 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
-
import { SessionManager, createAgentSession } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import * as z from "zod";
|
|
9
|
-
//#region src/acp/auth.ts
|
|
10
|
-
const AUTH_METHOD_ID = "pi_terminal_login";
|
|
11
|
-
function buildAuthMethods(opts) {
|
|
12
|
-
const supportsTerminalAuthMeta = opts?.supportsTerminalAuthMeta ?? true;
|
|
13
|
-
const method = {
|
|
14
|
-
id: AUTH_METHOD_ID,
|
|
15
|
-
name: "Launch pi in the terminal",
|
|
16
|
-
description: "Start pi in an interactive terminal to configure API keys or login",
|
|
17
|
-
type: "terminal",
|
|
18
|
-
args: ["--terminal-login"],
|
|
19
|
-
env: {}
|
|
20
|
-
};
|
|
21
|
-
if (supportsTerminalAuthMeta) method._meta = { "terminal-auth": {
|
|
22
|
-
...resolveTerminalLaunchCommand(),
|
|
23
|
-
label: "Launch pi"
|
|
24
|
-
} };
|
|
25
|
-
return [method];
|
|
26
|
-
}
|
|
27
|
-
function resolveTerminalLaunchCommand() {
|
|
28
|
-
const argv0 = process.argv[0] ?? "node";
|
|
29
|
-
const argv1 = process.argv[1];
|
|
30
|
-
if (argv1 !== void 0 && argv0.includes("node") && argv1.endsWith(".js")) return {
|
|
31
|
-
command: argv0,
|
|
32
|
-
args: [argv1, "--terminal-login"]
|
|
33
|
-
};
|
|
34
|
-
return {
|
|
35
|
-
command: "pi-acp",
|
|
36
|
-
args: ["--terminal-login"]
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/acp/auth-required.ts
|
|
41
|
-
/**
|
|
42
|
-
* Detect common auth/credential errors from pi and surface them as ACP AUTH_REQUIRED.
|
|
43
|
-
*/
|
|
44
|
-
const AUTH_ERROR_PATTERNS = [
|
|
45
|
-
"api key",
|
|
46
|
-
"apikey",
|
|
47
|
-
"missing key",
|
|
48
|
-
"no key",
|
|
49
|
-
"not configured",
|
|
50
|
-
"unauthorized",
|
|
51
|
-
"authentication",
|
|
52
|
-
"permission denied",
|
|
53
|
-
"forbidden",
|
|
54
|
-
"401",
|
|
55
|
-
"403"
|
|
56
|
-
];
|
|
57
|
-
function detectAuthError(err) {
|
|
58
|
-
const lower = (err instanceof Error ? err.message : String(err ?? "")).toLowerCase();
|
|
59
|
-
if (!AUTH_ERROR_PATTERNS.some((p) => lower.includes(p))) return null;
|
|
60
|
-
return RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
|
|
61
|
-
}
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/acp/client-capabilities.ts
|
|
64
|
-
/**
|
|
65
|
-
* Extract well-known capability flags from ACP `ClientCapabilities`.
|
|
66
|
-
*
|
|
67
|
-
* Reads from:
|
|
68
|
-
* - `_meta.terminal_output` (terminal output rendering)
|
|
69
|
-
* - `_meta.terminal-auth` (terminal auth with command metadata)
|
|
70
|
-
* - `auth._meta.gateway` (gateway auth, future use)
|
|
71
|
-
*/
|
|
72
|
-
function parseClientCapabilities(caps) {
|
|
73
|
-
if (caps === void 0 || caps === null) return {
|
|
74
|
-
terminalOutput: false,
|
|
75
|
-
terminalAuth: false,
|
|
76
|
-
gatewayAuth: false
|
|
77
|
-
};
|
|
78
|
-
const meta = caps._meta;
|
|
79
|
-
const terminalOutput = typeof meta === "object" && meta !== null && meta["terminal_output"] === true;
|
|
80
|
-
const terminalAuth = typeof meta === "object" && meta !== null && meta["terminal-auth"] === true;
|
|
81
|
-
let gatewayAuth = false;
|
|
82
|
-
if ("auth" in caps) {
|
|
83
|
-
const auth = caps.auth;
|
|
84
|
-
if (typeof auth === "object" && auth !== null && "_meta" in auth) {
|
|
85
|
-
const authMeta = auth._meta;
|
|
86
|
-
if (typeof authMeta === "object" && authMeta !== null && "gateway" in authMeta) gatewayAuth = authMeta["gateway"] === true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
terminalOutput,
|
|
91
|
-
terminalAuth,
|
|
92
|
-
gatewayAuth
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
//#endregion
|
|
96
|
-
//#region src/acp/model-alias.ts
|
|
97
|
-
/**
|
|
98
|
-
* Tokenize a string: split on non-alphanumeric, lowercase, strip "claude".
|
|
99
|
-
*/
|
|
100
|
-
function tokenize(input) {
|
|
101
|
-
return input.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t !== "" && t !== "claude");
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Extract a context hint in square brackets, e.g. "opus[1m]" -> { base: "opus", hint: "1m" }.
|
|
105
|
-
*/
|
|
106
|
-
function extractContextHint(input) {
|
|
107
|
-
const match = /^(.+?)\[([^\]]+)\]$/.exec(input);
|
|
108
|
-
if (match !== null && match[1] !== void 0 && match[2] !== void 0) return {
|
|
109
|
-
base: match[1],
|
|
110
|
-
hint: match[2]
|
|
111
|
-
};
|
|
112
|
-
return {
|
|
113
|
-
base: input,
|
|
114
|
-
hint: null
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
/** Check if a string is purely numeric. */
|
|
118
|
-
function isNumeric(s) {
|
|
119
|
-
return /^\d+$/.test(s);
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Score how well a model matches the given preference tokens.
|
|
123
|
-
*
|
|
124
|
-
* Returns a score >= 0 (higher is better), or -1 for no match.
|
|
125
|
-
* Requires at least one non-numeric token to match to avoid false positives
|
|
126
|
-
* from bare version numbers (e.g. "4" matching model version suffixes).
|
|
127
|
-
*/
|
|
128
|
-
function scoreModel(model, prefTokens, hint) {
|
|
129
|
-
const modelStr = `${model.provider}/${model.id}/${model.name ?? ""}`.toLowerCase();
|
|
130
|
-
const modelTokens = tokenize(modelStr);
|
|
131
|
-
let matched = 0;
|
|
132
|
-
let hasNonNumericMatch = false;
|
|
133
|
-
for (const pt of prefTokens) if (modelTokens.some((mt) => mt.includes(pt) || pt.includes(mt))) {
|
|
134
|
-
matched++;
|
|
135
|
-
if (!isNumeric(pt)) hasNonNumericMatch = true;
|
|
136
|
-
}
|
|
137
|
-
if (matched === 0) return -1;
|
|
138
|
-
if (!hasNonNumericMatch) return -1;
|
|
139
|
-
let score = matched / prefTokens.length;
|
|
140
|
-
if (hint !== null && modelStr.includes(hint.toLowerCase())) score += .5;
|
|
141
|
-
const pref = prefTokens.join("");
|
|
142
|
-
if (model.id.toLowerCase().includes(pref)) score += .25;
|
|
143
|
-
return score;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Resolve a user-friendly model preference to a concrete model.
|
|
147
|
-
*
|
|
148
|
-
* Matching strategy (in order):
|
|
149
|
-
* 1. Exact match on "provider/id"
|
|
150
|
-
* 2. Exact match on "id" alone
|
|
151
|
-
* 3. Tokenized scored match with optional context hint
|
|
152
|
-
*
|
|
153
|
-
* Returns null if no model matches.
|
|
154
|
-
*/
|
|
155
|
-
function resolveModelPreference(models, preference) {
|
|
156
|
-
const trimmed = preference.trim();
|
|
157
|
-
if (trimmed === "") return null;
|
|
158
|
-
if (trimmed.includes("/")) {
|
|
159
|
-
const [p, ...rest] = trimmed.split("/");
|
|
160
|
-
const provider = p ?? "";
|
|
161
|
-
const id = rest.join("/");
|
|
162
|
-
const exact = models.find((m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === id.toLowerCase());
|
|
163
|
-
if (exact !== void 0) return {
|
|
164
|
-
provider: exact.provider,
|
|
165
|
-
id: exact.id
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
const byId = models.find((m) => m.id.toLowerCase() === trimmed.toLowerCase());
|
|
169
|
-
if (byId !== void 0) return {
|
|
170
|
-
provider: byId.provider,
|
|
171
|
-
id: byId.id
|
|
172
|
-
};
|
|
173
|
-
const { base, hint } = extractContextHint(trimmed);
|
|
174
|
-
const prefTokens = tokenize(base);
|
|
175
|
-
if (prefTokens.length === 0) return null;
|
|
176
|
-
let bestModel = null;
|
|
177
|
-
let bestScore = -1;
|
|
178
|
-
for (const model of models) {
|
|
179
|
-
const s = scoreModel(model, prefTokens, hint);
|
|
180
|
-
if (s > bestScore) {
|
|
181
|
-
bestScore = s;
|
|
182
|
-
bestModel = model;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (bestModel === null || bestScore < .5) return null;
|
|
186
|
-
return {
|
|
187
|
-
provider: bestModel.provider,
|
|
188
|
-
id: bestModel.id
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
//#endregion
|
|
192
|
-
//#region src/acp/pi-settings.ts
|
|
193
|
-
/**
|
|
194
|
-
* Read pi settings from global and project config files.
|
|
195
|
-
*
|
|
196
|
-
* Settings are merged: project overrides global.
|
|
197
|
-
* Paths follow pi-mono conventions:
|
|
198
|
-
* Global: ~/.pi/agent/settings.json
|
|
199
|
-
* Project: <cwd>/.pi/settings.json
|
|
200
|
-
*/
|
|
201
|
-
const piSettingsSchema = z.object({
|
|
202
|
-
enableSkillCommands: z.boolean().optional(),
|
|
203
|
-
quietStartup: z.boolean().optional(),
|
|
204
|
-
quietStart: z.boolean().optional(),
|
|
205
|
-
skills: z.object({ enableSkillCommands: z.boolean().optional() }).optional()
|
|
206
|
-
});
|
|
207
|
-
function isRecord(x) {
|
|
208
|
-
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
209
|
-
}
|
|
210
|
-
function merge(base, override) {
|
|
211
|
-
const result = { ...base };
|
|
212
|
-
for (const [key, val] of Object.entries(override)) {
|
|
213
|
-
const existing = result[key];
|
|
214
|
-
if (isRecord(existing) && isRecord(val)) result[key] = merge(existing, val);
|
|
215
|
-
else result[key] = val;
|
|
216
|
-
}
|
|
217
|
-
return result;
|
|
218
|
-
}
|
|
219
|
-
function readJson(path) {
|
|
220
|
-
try {
|
|
221
|
-
if (!existsSync(path)) return {};
|
|
222
|
-
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
223
|
-
return isRecord(data) ? data : {};
|
|
224
|
-
} catch {
|
|
225
|
-
return {};
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
function piAgentDir() {
|
|
229
|
-
return process.env.PI_CODING_AGENT_DIR !== void 0 ? resolve(process.env.PI_CODING_AGENT_DIR) : join(homedir(), ".pi", "agent");
|
|
230
|
-
}
|
|
231
|
-
function resolvedSettings(cwd) {
|
|
232
|
-
const globalPath = join(piAgentDir(), "settings.json");
|
|
233
|
-
const projectPath = resolve(cwd, ".pi", "settings.json");
|
|
234
|
-
const merged = merge(readJson(globalPath), readJson(projectPath));
|
|
235
|
-
const result = piSettingsSchema.safeParse(merged);
|
|
236
|
-
return result.success ? result.data : {};
|
|
237
|
-
}
|
|
238
|
-
function skillCommandsEnabled(cwd) {
|
|
239
|
-
const settings = resolvedSettings(cwd);
|
|
240
|
-
if (typeof settings.enableSkillCommands === "boolean") return settings.enableSkillCommands;
|
|
241
|
-
if (typeof settings.skills?.enableSkillCommands === "boolean") return settings.skills.enableSkillCommands;
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
//#endregion
|
|
245
|
-
//#region src/acp/translate/tool-content.ts
|
|
246
|
-
const textBlockSchema = z.object({
|
|
247
|
-
type: z.literal("text"),
|
|
248
|
-
text: z.string()
|
|
249
|
-
});
|
|
250
|
-
const imageBlockSchema = z.object({ type: z.literal("image") });
|
|
251
|
-
const contentBlockSchema = z.union([textBlockSchema, imageBlockSchema]);
|
|
252
|
-
const bashDetailsSchema = z.object({
|
|
253
|
-
stdout: z.string().optional(),
|
|
254
|
-
stderr: z.string().optional(),
|
|
255
|
-
output: z.string().optional(),
|
|
256
|
-
exitCode: z.number().optional(),
|
|
257
|
-
code: z.number().optional()
|
|
258
|
-
});
|
|
259
|
-
const bashResultSchema = z.object({
|
|
260
|
-
content: z.array(z.unknown()).optional(),
|
|
261
|
-
details: bashDetailsSchema.optional(),
|
|
262
|
-
stdout: z.string().optional(),
|
|
263
|
-
stderr: z.string().optional(),
|
|
264
|
-
output: z.string().optional(),
|
|
265
|
-
exitCode: z.number().optional(),
|
|
266
|
-
code: z.number().optional()
|
|
267
|
-
});
|
|
268
|
-
/**
|
|
269
|
-
* Extract stdout/stderr and exit code from a pi bash/tmux result.
|
|
270
|
-
*/
|
|
271
|
-
function extractBashOutput(result) {
|
|
272
|
-
if (result === null || result === void 0 || typeof result !== "object") return {
|
|
273
|
-
output: "",
|
|
274
|
-
exitCode: void 0
|
|
275
|
-
};
|
|
276
|
-
const parsed = bashResultSchema.safeParse(result);
|
|
277
|
-
if (!parsed.success) return {
|
|
278
|
-
output: "",
|
|
279
|
-
exitCode: void 0
|
|
280
|
-
};
|
|
281
|
-
const r = parsed.data;
|
|
282
|
-
const d = r.details;
|
|
283
|
-
if (r.content !== void 0) {
|
|
284
|
-
const texts = r.content.map((block) => textBlockSchema.safeParse(block)).filter((res) => res.success).map((res) => res.data.text);
|
|
285
|
-
if (texts.length > 0) {
|
|
286
|
-
const exitCode = d?.exitCode ?? r.exitCode ?? d?.code ?? r.code;
|
|
287
|
-
return {
|
|
288
|
-
output: texts.join(""),
|
|
289
|
-
exitCode
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
const stdout = d?.stdout ?? r.stdout ?? d?.output ?? r.output;
|
|
294
|
-
const stderr = d?.stderr ?? r.stderr;
|
|
295
|
-
const exitCode = d?.exitCode ?? r.exitCode ?? d?.code ?? r.code;
|
|
296
|
-
const parts = [];
|
|
297
|
-
if (stdout !== void 0 && stdout.trim() !== "") parts.push(stdout);
|
|
298
|
-
if (stderr !== void 0 && stderr.trim() !== "") parts.push(stderr);
|
|
299
|
-
return {
|
|
300
|
-
output: parts.join("\n"),
|
|
301
|
-
exitCode
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Extract text content from a pi tool result (generic).
|
|
306
|
-
*/
|
|
307
|
-
function extractTextContent(result) {
|
|
308
|
-
if (result === null || result === void 0 || typeof result !== "object") return "";
|
|
309
|
-
if ("content" in result && Array.isArray(result.content)) {
|
|
310
|
-
const texts = [];
|
|
311
|
-
for (const block of result.content) {
|
|
312
|
-
const parsed = textBlockSchema.safeParse(block);
|
|
313
|
-
if (parsed.success) texts.push(parsed.data.text);
|
|
314
|
-
}
|
|
315
|
-
if (texts.length > 0) return texts.join("");
|
|
316
|
-
}
|
|
317
|
-
try {
|
|
318
|
-
return JSON.stringify(result, null, 2);
|
|
319
|
-
} catch {
|
|
320
|
-
return String(result);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Extract content blocks from a pi result, preserving type information.
|
|
325
|
-
* Used for read results where images need to be preserved.
|
|
326
|
-
*/
|
|
327
|
-
function extractContentBlocks(result) {
|
|
328
|
-
if (result === null || result === void 0 || typeof result !== "object") return [];
|
|
329
|
-
if (!("content" in result) || !Array.isArray(result.content)) return [];
|
|
330
|
-
const blocks = [];
|
|
331
|
-
for (const raw of result.content) {
|
|
332
|
-
const parsed = contentBlockSchema.safeParse(raw);
|
|
333
|
-
if (parsed.success) blocks.push(parsed.data);
|
|
334
|
-
}
|
|
335
|
-
return blocks;
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Find the longest consecutive backtick sequence in a string.
|
|
339
|
-
*/
|
|
340
|
-
function longestBacktickRun(text) {
|
|
341
|
-
let max = 0;
|
|
342
|
-
let current = 0;
|
|
343
|
-
for (const ch of text) if (ch === "`") {
|
|
344
|
-
current++;
|
|
345
|
-
if (current > max) max = current;
|
|
346
|
-
} else current = 0;
|
|
347
|
-
return max;
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Wrap text in a dynamically-sized backtick fence to prevent markdown rendering.
|
|
351
|
-
*
|
|
352
|
-
* Instead of character-level escaping (which fails on files containing backtick
|
|
353
|
-
* sequences, indented code blocks, blockquotes, and list markers), this wraps
|
|
354
|
-
* the entire text in a backtick fence whose length exceeds any backtick sequence
|
|
355
|
-
* in the content. This approach is simpler and strictly more correct (following
|
|
356
|
-
* the claude-agent-acp pattern).
|
|
357
|
-
*/
|
|
358
|
-
function markdownEscape(text) {
|
|
359
|
-
if (text === "") return "";
|
|
360
|
-
const fenceLen = Math.max(3, longestBacktickRun(text) + 1);
|
|
361
|
-
const fence = "`".repeat(fenceLen);
|
|
362
|
-
return `${fence}\n${text.endsWith("\n") ? text.slice(0, -1) : text}\n${fence}`;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Format tool output into `ToolCallContent[]` by tool name.
|
|
366
|
-
*
|
|
367
|
-
* Returns the appropriate content shape for each tool type:
|
|
368
|
-
* - bash/tmux: console code fences
|
|
369
|
-
* - read: markdown-escaped text (images preserved)
|
|
370
|
-
* - edit/write: empty (diff handled separately)
|
|
371
|
-
* - lsp: code fences
|
|
372
|
-
* - errors: code fences with failed status
|
|
373
|
-
* - everything else: plain text
|
|
374
|
-
*/
|
|
375
|
-
function formatToolContent(toolName, result, isError) {
|
|
376
|
-
if (isError) {
|
|
377
|
-
const text = extractTextContent(result);
|
|
378
|
-
if (text === "") return [];
|
|
379
|
-
return [{
|
|
380
|
-
type: "content",
|
|
381
|
-
content: {
|
|
382
|
-
type: "text",
|
|
383
|
-
text: `\`\`\`\n${text}\n\`\`\``
|
|
384
|
-
}
|
|
385
|
-
}];
|
|
386
|
-
}
|
|
387
|
-
switch (toolName) {
|
|
388
|
-
case "bash":
|
|
389
|
-
case "tmux": return formatBashContent(result);
|
|
390
|
-
case "read": return formatReadContent(result);
|
|
391
|
-
case "edit":
|
|
392
|
-
case "write": return [];
|
|
393
|
-
case "lsp": return formatLspContent(result);
|
|
394
|
-
default: return formatFallbackContent(result);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
function formatBashContent(result) {
|
|
398
|
-
const { output, exitCode } = extractBashOutput(result);
|
|
399
|
-
if (output === "" && exitCode === void 0) return [];
|
|
400
|
-
const parts = [];
|
|
401
|
-
if (output !== "") parts.push(`\`\`\`console\n${output}\n\`\`\``);
|
|
402
|
-
if (exitCode !== void 0 && exitCode !== 0) parts.push(`exit code: ${exitCode}`);
|
|
403
|
-
const text = parts.join("\n\n");
|
|
404
|
-
if (text === "") return [];
|
|
405
|
-
return [{
|
|
406
|
-
type: "content",
|
|
407
|
-
content: {
|
|
408
|
-
type: "text",
|
|
409
|
-
text
|
|
410
|
-
}
|
|
411
|
-
}];
|
|
412
|
-
}
|
|
413
|
-
function formatReadContent(result) {
|
|
414
|
-
const blocks = extractContentBlocks(result);
|
|
415
|
-
if (blocks.length === 0) {
|
|
416
|
-
if (typeof result === "object" && result !== null && "content" in result && Array.isArray(result.content) && result.content.length === 0) return [];
|
|
417
|
-
const text = extractTextContent(result);
|
|
418
|
-
if (text === "") return [];
|
|
419
|
-
return [{
|
|
420
|
-
type: "content",
|
|
421
|
-
content: {
|
|
422
|
-
type: "text",
|
|
423
|
-
text: markdownEscape(text)
|
|
424
|
-
}
|
|
425
|
-
}];
|
|
426
|
-
}
|
|
427
|
-
const content = [];
|
|
428
|
-
for (const block of blocks) if (block.type === "text") content.push({
|
|
429
|
-
type: "content",
|
|
430
|
-
content: {
|
|
431
|
-
type: "text",
|
|
432
|
-
text: markdownEscape(block.text)
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
return content;
|
|
436
|
-
}
|
|
437
|
-
function formatLspContent(result) {
|
|
438
|
-
const text = extractTextContent(result);
|
|
439
|
-
if (text === "") return [];
|
|
440
|
-
return [{
|
|
441
|
-
type: "content",
|
|
442
|
-
content: {
|
|
443
|
-
type: "text",
|
|
444
|
-
text: `\`\`\`\n${text}\n\`\`\``
|
|
445
|
-
}
|
|
446
|
-
}];
|
|
447
|
-
}
|
|
448
|
-
function formatFallbackContent(result) {
|
|
449
|
-
const text = extractTextContent(result);
|
|
450
|
-
if (text === "") return [];
|
|
451
|
-
return [{
|
|
452
|
-
type: "content",
|
|
453
|
-
content: {
|
|
454
|
-
type: "text",
|
|
455
|
-
text
|
|
456
|
-
}
|
|
457
|
-
}];
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Wrap streaming output text in a console code fence for bash/tmux.
|
|
461
|
-
*
|
|
462
|
-
* Each streaming update is self-contained (full accumulated buffer),
|
|
463
|
-
* following the codex-acp pattern.
|
|
464
|
-
*/
|
|
465
|
-
function wrapStreamingBashOutput(text) {
|
|
466
|
-
if (text === "") return "";
|
|
467
|
-
return `\`\`\`console\n${text}\n\`\`\``;
|
|
468
|
-
}
|
|
469
|
-
//#endregion
|
|
470
|
-
//#region src/acp/unreachable.ts
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
//#region src/index.ts
|
|
471
4
|
/**
|
|
472
|
-
*
|
|
5
|
+
* pi-acp entry point. Dispatches between four modes:
|
|
473
6
|
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
|
|
478
|
-
function unreachable(value, context) {
|
|
479
|
-
const label = context !== void 0 ? `[${context}] ` : "";
|
|
480
|
-
process.stderr.write(`${label}Unhandled value: ${String(value)}\n`);
|
|
481
|
-
}
|
|
482
|
-
//#endregion
|
|
483
|
-
//#region src/acp/session.ts
|
|
484
|
-
function findUniqueLineNumber(text, needle) {
|
|
485
|
-
if (!needle) return void 0;
|
|
486
|
-
const first = text.indexOf(needle);
|
|
487
|
-
if (first < 0) return void 0;
|
|
488
|
-
if (text.indexOf(needle, first + needle.length) >= 0) return void 0;
|
|
489
|
-
let line = 1;
|
|
490
|
-
for (let i = 0; i < first; i++) if (text.charCodeAt(i) === 10) line++;
|
|
491
|
-
return line;
|
|
492
|
-
}
|
|
493
|
-
function resolveToolPath(args, cwd, line) {
|
|
494
|
-
const p = args.path;
|
|
495
|
-
if (p === void 0) return void 0;
|
|
496
|
-
return [{
|
|
497
|
-
path: isAbsolute(p) ? p : resolve(cwd, p),
|
|
498
|
-
...typeof line === "number" ? { line } : {}
|
|
499
|
-
}];
|
|
500
|
-
}
|
|
501
|
-
function toToolKind(toolName) {
|
|
502
|
-
switch (toolName) {
|
|
503
|
-
case "read": return "read";
|
|
504
|
-
case "write":
|
|
505
|
-
case "edit": return "edit";
|
|
506
|
-
case "bash":
|
|
507
|
-
case "tmux": return "execute";
|
|
508
|
-
case "lsp": return "search";
|
|
509
|
-
default: return "other";
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
const MAX_TITLE_LEN = 80;
|
|
513
|
-
function truncateTitle(text) {
|
|
514
|
-
const oneLine = text.replace(/\n/g, " ").trim();
|
|
515
|
-
if (oneLine.length <= MAX_TITLE_LEN) return oneLine;
|
|
516
|
-
return `${oneLine.slice(0, MAX_TITLE_LEN - 1)}…`;
|
|
517
|
-
}
|
|
518
|
-
function capitalize(s) {
|
|
519
|
-
if (s.length === 0) return s;
|
|
520
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Build a descriptive tool title from tool name and args.
|
|
7
|
+
* --terminal-login → foreground pi for interactive auth (v0.5 flow)
|
|
8
|
+
* --daemon → long-running orchestrator (PRD-003)
|
|
9
|
+
* --no-daemon | PI_ACP_NO_DAEMON=1 → v0.5 in-process server (escape hatch)
|
|
10
|
+
* (default) → thin client; auto-spawns daemon
|
|
524
11
|
*
|
|
525
|
-
*
|
|
12
|
+
* ACP transports JSON-RPC NDJSON over stdout. Any stray byte poisons the
|
|
13
|
+
* protocol stream. Redirect console.{log,info,warn,debug} to stderr at boot
|
|
14
|
+
* so transitive deps (or our own debug prints) can't corrupt it.
|
|
526
15
|
*/
|
|
527
|
-
function buildToolTitle(toolName, args) {
|
|
528
|
-
const p = args.path;
|
|
529
|
-
switch (toolName) {
|
|
530
|
-
case "read": return p !== void 0 ? `Read ${p}` : "Read";
|
|
531
|
-
case "write": return p !== void 0 ? `Write ${p}` : "Write";
|
|
532
|
-
case "edit": return p !== void 0 ? `Edit ${p}` : "Edit";
|
|
533
|
-
case "bash": {
|
|
534
|
-
const command = typeof args["command"] === "string" ? args["command"] : typeof args["cmd"] === "string" ? args["cmd"] : void 0;
|
|
535
|
-
return command !== void 0 ? truncateTitle(`Run ${command}`) : "bash";
|
|
536
|
-
}
|
|
537
|
-
case "lsp": {
|
|
538
|
-
const action = typeof args["action"] === "string" ? args["action"] : void 0;
|
|
539
|
-
const file = typeof args["file"] === "string" ? args["file"] : void 0;
|
|
540
|
-
const query = typeof args["query"] === "string" ? args["query"] : void 0;
|
|
541
|
-
const line = typeof args["line"] === "number" ? args["line"] : void 0;
|
|
542
|
-
if (action !== void 0) {
|
|
543
|
-
const target = file !== void 0 ? line !== void 0 ? `${file}:${line}` : file : query;
|
|
544
|
-
return target !== void 0 ? truncateTitle(`${capitalize(action)} ${target}`) : capitalize(action);
|
|
545
|
-
}
|
|
546
|
-
return "LSP";
|
|
547
|
-
}
|
|
548
|
-
case "tmux": {
|
|
549
|
-
const action = typeof args["action"] === "string" ? args["action"] : void 0;
|
|
550
|
-
const command = typeof args["command"] === "string" ? args["command"] : void 0;
|
|
551
|
-
const name = typeof args["name"] === "string" ? args["name"] : void 0;
|
|
552
|
-
if (action === "run" && command !== void 0) return truncateTitle(`Tmux: ${command}`);
|
|
553
|
-
if (action !== void 0 && name !== void 0) return truncateTitle(`Tmux ${action} ${name}`);
|
|
554
|
-
if (action !== void 0) return `Tmux ${action}`;
|
|
555
|
-
return "Tmux";
|
|
556
|
-
}
|
|
557
|
-
case "context_tag": {
|
|
558
|
-
const name = typeof args["name"] === "string" ? args["name"] : void 0;
|
|
559
|
-
return name !== void 0 ? `Tag ${name}` : "Tag";
|
|
560
|
-
}
|
|
561
|
-
case "context_log": return "Context log";
|
|
562
|
-
case "context_checkout": {
|
|
563
|
-
const target = typeof args["target"] === "string" ? args["target"] : void 0;
|
|
564
|
-
return target !== void 0 ? truncateTitle(`Checkout ${target}`) : "Checkout";
|
|
565
|
-
}
|
|
566
|
-
case "claudemon": return "Check quota";
|
|
567
|
-
default: return toolName;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* Map pi assistant stopReason to ACP StopReason.
|
|
572
|
-
* pi: "stop" | "length" | "toolUse" | "error" | "aborted"
|
|
573
|
-
* ACP: "end_turn" | "cancelled" | "max_tokens" | "error"
|
|
574
|
-
*/
|
|
575
|
-
function mapPiStopReason(piReason) {
|
|
576
|
-
switch (piReason) {
|
|
577
|
-
case "stop":
|
|
578
|
-
case "toolUse": return "end_turn";
|
|
579
|
-
case "length": return "max_tokens";
|
|
580
|
-
case "aborted": return "cancelled";
|
|
581
|
-
case "error": return "error";
|
|
582
|
-
default: return "end_turn";
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
function extractToolCallFromPartial(ame) {
|
|
586
|
-
if (!("partial" in ame)) return void 0;
|
|
587
|
-
const block = ame.partial.content["contentIndex" in ame ? ame.contentIndex : 0];
|
|
588
|
-
if (block && "type" in block && block.type === "toolCall") return block;
|
|
589
|
-
}
|
|
590
|
-
function parseToolInput(tc) {
|
|
591
|
-
return tc.arguments;
|
|
592
|
-
}
|
|
593
|
-
const toolArgsSchema = z.object({
|
|
594
|
-
path: z.string().trim().optional(),
|
|
595
|
-
oldText: z.string().trim().optional()
|
|
596
|
-
}).loose();
|
|
597
|
-
function toToolArgs(raw) {
|
|
598
|
-
const result = toolArgsSchema.safeParse(raw);
|
|
599
|
-
return result.success ? result.data : {};
|
|
600
|
-
}
|
|
601
|
-
/** Build the `_meta.piAcp` tool name metadata. */
|
|
602
|
-
function buildToolMeta(toolName, extra) {
|
|
603
|
-
const base = { piAcp: { toolName } };
|
|
604
|
-
if (extra !== void 0) return {
|
|
605
|
-
...base,
|
|
606
|
-
...extra
|
|
607
|
-
};
|
|
608
|
-
return base;
|
|
609
|
-
}
|
|
610
|
-
/** Tools that produce terminal-style output. */
|
|
611
|
-
function isTerminalTool(toolName) {
|
|
612
|
-
return toolName === "bash" || toolName === "tmux";
|
|
613
|
-
}
|
|
614
|
-
var SessionManager$1 = class {
|
|
615
|
-
sessions = /* @__PURE__ */ new Map();
|
|
616
|
-
disposeAll() {
|
|
617
|
-
for (const id of this.sessions.keys()) this.close(id);
|
|
618
|
-
}
|
|
619
|
-
maybeGet(sessionId) {
|
|
620
|
-
return this.sessions.get(sessionId);
|
|
621
|
-
}
|
|
622
|
-
close(sessionId) {
|
|
623
|
-
const s = this.sessions.get(sessionId);
|
|
624
|
-
if (!s) return;
|
|
625
|
-
try {
|
|
626
|
-
s.dispose();
|
|
627
|
-
} catch {}
|
|
628
|
-
this.sessions.delete(sessionId);
|
|
629
|
-
}
|
|
630
|
-
closeAllExcept(keepSessionId) {
|
|
631
|
-
for (const id of this.sessions.keys()) if (id !== keepSessionId) this.close(id);
|
|
632
|
-
}
|
|
633
|
-
register(session) {
|
|
634
|
-
this.sessions.set(session.sessionId, session);
|
|
635
|
-
}
|
|
636
|
-
get(sessionId) {
|
|
637
|
-
const s = this.sessions.get(sessionId);
|
|
638
|
-
if (!s) throw RequestError.invalidParams(`Unknown sessionId: ${sessionId}`);
|
|
639
|
-
return s;
|
|
640
|
-
}
|
|
641
|
-
};
|
|
642
|
-
var PiAcpSession = class {
|
|
643
|
-
sessionId;
|
|
644
|
-
cwd;
|
|
645
|
-
mcpServers;
|
|
646
|
-
piSession;
|
|
647
|
-
supportsTerminalOutput;
|
|
648
|
-
conn;
|
|
649
|
-
cancelRequested = false;
|
|
650
|
-
promptRunning = false;
|
|
651
|
-
pendingTurn = null;
|
|
652
|
-
/** Queued prompts waiting for the active turn to complete. */
|
|
653
|
-
pendingMessages = [];
|
|
654
|
-
currentToolCalls = /* @__PURE__ */ new Map();
|
|
655
|
-
/** Map of toolCallId -> toolName for streaming updates (Phase 5). */
|
|
656
|
-
toolCallNames = /* @__PURE__ */ new Map();
|
|
657
|
-
editSnapshots = /* @__PURE__ */ new Map();
|
|
658
|
-
lastAssistantStopReason = null;
|
|
659
|
-
lastEmit = Promise.resolve();
|
|
660
|
-
unsubscribe;
|
|
661
|
-
constructor(opts) {
|
|
662
|
-
this.sessionId = opts.sessionId;
|
|
663
|
-
this.cwd = opts.cwd;
|
|
664
|
-
this.mcpServers = opts.mcpServers;
|
|
665
|
-
this.piSession = opts.piSession;
|
|
666
|
-
this.conn = opts.conn;
|
|
667
|
-
this.supportsTerminalOutput = opts.supportsTerminalOutput ?? false;
|
|
668
|
-
this.unsubscribe = this.piSession.subscribe((ev) => this.handlePiEvent(ev));
|
|
669
|
-
}
|
|
670
|
-
dispose() {
|
|
671
|
-
this.unsubscribe?.();
|
|
672
|
-
this.piSession.dispose();
|
|
673
|
-
}
|
|
674
|
-
async prompt(message, images = []) {
|
|
675
|
-
if (this.promptRunning) return new Promise((resolve, reject) => {
|
|
676
|
-
this.pendingMessages.push({
|
|
677
|
-
message,
|
|
678
|
-
images,
|
|
679
|
-
resolve,
|
|
680
|
-
reject
|
|
681
|
-
});
|
|
682
|
-
});
|
|
683
|
-
return this.executePrompt(message, images);
|
|
684
|
-
}
|
|
685
|
-
async cancel() {
|
|
686
|
-
this.cancelRequested = true;
|
|
687
|
-
for (const pending of this.pendingMessages) pending.resolve("cancelled");
|
|
688
|
-
this.pendingMessages = [];
|
|
689
|
-
await this.piSession.abort();
|
|
690
|
-
}
|
|
691
|
-
executePrompt(message, images) {
|
|
692
|
-
this.promptRunning = true;
|
|
693
|
-
const turnPromise = new Promise((resolve, reject) => {
|
|
694
|
-
this.cancelRequested = false;
|
|
695
|
-
this.pendingTurn = {
|
|
696
|
-
resolve,
|
|
697
|
-
reject
|
|
698
|
-
};
|
|
699
|
-
});
|
|
700
|
-
const imageContents = Array.isArray(images) ? images.filter((img) => typeof img === "object" && img !== null && "type" in img && img.type === "image") : [];
|
|
701
|
-
this.piSession.prompt(message, { images: imageContents }).catch(() => {
|
|
702
|
-
this.flushEmits().finally(() => {
|
|
703
|
-
const reason = this.cancelRequested ? "cancelled" : "error";
|
|
704
|
-
this.pendingTurn?.resolve(reason);
|
|
705
|
-
this.pendingTurn = null;
|
|
706
|
-
});
|
|
707
|
-
});
|
|
708
|
-
return turnPromise;
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Dequeue and execute the next pending prompt, if any.
|
|
712
|
-
* Called after a turn completes.
|
|
713
|
-
*/
|
|
714
|
-
dequeueNextPrompt() {
|
|
715
|
-
const next = this.pendingMessages.shift();
|
|
716
|
-
if (next === void 0) {
|
|
717
|
-
this.promptRunning = false;
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
this.executePrompt(next.message, next.images).then(next.resolve, next.reject);
|
|
721
|
-
}
|
|
722
|
-
wasCancelRequested() {
|
|
723
|
-
return this.cancelRequested;
|
|
724
|
-
}
|
|
725
|
-
emit(update) {
|
|
726
|
-
this.lastEmit = this.lastEmit.then(() => this.conn.sessionUpdate({
|
|
727
|
-
sessionId: this.sessionId,
|
|
728
|
-
update
|
|
729
|
-
})).catch(() => {});
|
|
730
|
-
}
|
|
731
|
-
async flushEmits() {
|
|
732
|
-
await this.lastEmit;
|
|
733
|
-
}
|
|
734
|
-
handlePiEvent(ev) {
|
|
735
|
-
if (!isAgentEvent(ev)) return;
|
|
736
|
-
switch (ev.type) {
|
|
737
|
-
case "message_update":
|
|
738
|
-
this.handleMessageUpdate(ev.assistantMessageEvent);
|
|
739
|
-
break;
|
|
740
|
-
case "message_end":
|
|
741
|
-
this.handleMessageEnd(ev.message);
|
|
742
|
-
break;
|
|
743
|
-
case "tool_execution_start":
|
|
744
|
-
this.handleToolStart(ev.toolCallId, ev.toolName, toToolArgs(ev.args));
|
|
745
|
-
break;
|
|
746
|
-
case "tool_execution_update":
|
|
747
|
-
this.handleToolUpdate(ev.toolCallId, ev.toolName, ev.partialResult);
|
|
748
|
-
break;
|
|
749
|
-
case "tool_execution_end":
|
|
750
|
-
this.handleToolEnd(ev.toolCallId, ev.toolName, ev.result, ev.isError);
|
|
751
|
-
break;
|
|
752
|
-
case "agent_end":
|
|
753
|
-
this.handleAgentEnd();
|
|
754
|
-
break;
|
|
755
|
-
default:
|
|
756
|
-
unreachable(ev, "handlePiEvent");
|
|
757
|
-
break;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
handleMessageUpdate(ame) {
|
|
761
|
-
if (ame.type === "text_delta") {
|
|
762
|
-
this.emit({
|
|
763
|
-
sessionUpdate: "agent_message_chunk",
|
|
764
|
-
content: {
|
|
765
|
-
type: "text",
|
|
766
|
-
text: ame.delta
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
if (ame.type === "thinking_delta") {
|
|
772
|
-
this.emit({
|
|
773
|
-
sessionUpdate: "agent_thought_chunk",
|
|
774
|
-
content: {
|
|
775
|
-
type: "text",
|
|
776
|
-
text: ame.delta
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
if (ame.type === "toolcall_start" || ame.type === "toolcall_delta" || ame.type === "toolcall_end") {
|
|
782
|
-
const toolCall = ame.type === "toolcall_end" ? ame.toolCall : extractToolCallFromPartial(ame);
|
|
783
|
-
if (!toolCall) return;
|
|
784
|
-
const rawInput = parseToolInput(toolCall);
|
|
785
|
-
const locations = resolveToolPath(rawInput, this.cwd);
|
|
786
|
-
const existingStatus = this.currentToolCalls.get(toolCall.id);
|
|
787
|
-
const status = existingStatus ?? "pending";
|
|
788
|
-
if (!existingStatus) {
|
|
789
|
-
this.currentToolCalls.set(toolCall.id, "pending");
|
|
790
|
-
this.emit({
|
|
791
|
-
sessionUpdate: "tool_call",
|
|
792
|
-
toolCallId: toolCall.id,
|
|
793
|
-
title: buildToolTitle(toolCall.name, rawInput),
|
|
794
|
-
kind: toToolKind(toolCall.name),
|
|
795
|
-
status,
|
|
796
|
-
...locations ? { locations } : {},
|
|
797
|
-
rawInput,
|
|
798
|
-
_meta: buildToolMeta(toolCall.name)
|
|
799
|
-
});
|
|
800
|
-
} else this.emit({
|
|
801
|
-
sessionUpdate: "tool_call_update",
|
|
802
|
-
toolCallId: toolCall.id,
|
|
803
|
-
status,
|
|
804
|
-
...locations ? { locations } : {},
|
|
805
|
-
rawInput,
|
|
806
|
-
_meta: buildToolMeta(toolCall.name)
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
handleMessageEnd(msg) {
|
|
811
|
-
if ("role" in msg && msg.role === "assistant") this.lastAssistantStopReason = msg.stopReason;
|
|
812
|
-
}
|
|
813
|
-
handleToolStart(toolCallId, toolName, args) {
|
|
814
|
-
this.toolCallNames.set(toolCallId, toolName);
|
|
815
|
-
let line;
|
|
816
|
-
if ((toolName === "edit" || toolName === "write") && args.path !== void 0) try {
|
|
817
|
-
const abs = isAbsolute(args.path) ? args.path : resolve(this.cwd, args.path);
|
|
818
|
-
let oldText = "";
|
|
819
|
-
try {
|
|
820
|
-
oldText = readFileSync(abs, "utf8");
|
|
821
|
-
} catch {}
|
|
822
|
-
this.editSnapshots.set(toolCallId, {
|
|
823
|
-
path: abs,
|
|
824
|
-
oldText
|
|
825
|
-
});
|
|
826
|
-
if (toolName === "edit") line = findUniqueLineNumber(oldText, args.oldText ?? "");
|
|
827
|
-
} catch {}
|
|
828
|
-
const locations = resolveToolPath(args, this.cwd, line);
|
|
829
|
-
const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_info: {
|
|
830
|
-
terminal_id: toolCallId,
|
|
831
|
-
cwd: this.cwd
|
|
832
|
-
} } : void 0);
|
|
833
|
-
const terminalContent = this.supportsTerminalOutput && isTerminalTool(toolName) ? [{
|
|
834
|
-
type: "terminal",
|
|
835
|
-
terminalId: toolCallId
|
|
836
|
-
}] : void 0;
|
|
837
|
-
if (!this.currentToolCalls.has(toolCallId)) {
|
|
838
|
-
this.currentToolCalls.set(toolCallId, "in_progress");
|
|
839
|
-
this.emit({
|
|
840
|
-
sessionUpdate: "tool_call",
|
|
841
|
-
toolCallId,
|
|
842
|
-
title: buildToolTitle(toolName, args),
|
|
843
|
-
kind: toToolKind(toolName),
|
|
844
|
-
status: "in_progress",
|
|
845
|
-
...locations ? { locations } : {},
|
|
846
|
-
...terminalContent !== void 0 ? { content: terminalContent } : {},
|
|
847
|
-
rawInput: args,
|
|
848
|
-
_meta: meta
|
|
849
|
-
});
|
|
850
|
-
} else {
|
|
851
|
-
this.currentToolCalls.set(toolCallId, "in_progress");
|
|
852
|
-
this.emit({
|
|
853
|
-
sessionUpdate: "tool_call_update",
|
|
854
|
-
toolCallId,
|
|
855
|
-
title: buildToolTitle(toolName, args),
|
|
856
|
-
status: "in_progress",
|
|
857
|
-
...locations ? { locations } : {},
|
|
858
|
-
...terminalContent !== void 0 ? { content: terminalContent } : {},
|
|
859
|
-
rawInput: args,
|
|
860
|
-
_meta: meta
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
handleToolUpdate(toolCallId, toolName, partialResult) {
|
|
865
|
-
const name = this.toolCallNames.get(toolCallId) ?? toolName;
|
|
866
|
-
if (this.supportsTerminalOutput && isTerminalTool(name)) {
|
|
867
|
-
const text = extractStreamingText(partialResult);
|
|
868
|
-
this.emit({
|
|
869
|
-
sessionUpdate: "tool_call_update",
|
|
870
|
-
toolCallId,
|
|
871
|
-
status: "in_progress",
|
|
872
|
-
_meta: buildToolMeta(name, { terminal_output: {
|
|
873
|
-
terminal_id: toolCallId,
|
|
874
|
-
data: text
|
|
875
|
-
} }),
|
|
876
|
-
rawOutput: partialResult
|
|
877
|
-
});
|
|
878
|
-
} else if (isTerminalTool(name)) {
|
|
879
|
-
const wrapped = wrapStreamingBashOutput(extractStreamingText(partialResult));
|
|
880
|
-
this.emit({
|
|
881
|
-
sessionUpdate: "tool_call_update",
|
|
882
|
-
toolCallId,
|
|
883
|
-
status: "in_progress",
|
|
884
|
-
content: wrapped ? [{
|
|
885
|
-
type: "content",
|
|
886
|
-
content: {
|
|
887
|
-
type: "text",
|
|
888
|
-
text: wrapped
|
|
889
|
-
}
|
|
890
|
-
}] : null,
|
|
891
|
-
_meta: buildToolMeta(name),
|
|
892
|
-
rawOutput: partialResult
|
|
893
|
-
});
|
|
894
|
-
} else {
|
|
895
|
-
const text = extractStreamingText(partialResult);
|
|
896
|
-
this.emit({
|
|
897
|
-
sessionUpdate: "tool_call_update",
|
|
898
|
-
toolCallId,
|
|
899
|
-
status: "in_progress",
|
|
900
|
-
content: text ? [{
|
|
901
|
-
type: "content",
|
|
902
|
-
content: {
|
|
903
|
-
type: "text",
|
|
904
|
-
text
|
|
905
|
-
}
|
|
906
|
-
}] : null,
|
|
907
|
-
_meta: buildToolMeta(name),
|
|
908
|
-
rawOutput: partialResult
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
handleToolEnd(toolCallId, toolName, result, isError) {
|
|
913
|
-
const snapshot = this.editSnapshots.get(toolCallId);
|
|
914
|
-
let content = null;
|
|
915
|
-
if (!isError && snapshot) try {
|
|
916
|
-
const newText = readFileSync(snapshot.path, "utf8");
|
|
917
|
-
if (newText !== snapshot.oldText) {
|
|
918
|
-
const formatted = formatToolContent(toolName, result, isError);
|
|
919
|
-
content = [{
|
|
920
|
-
type: "diff",
|
|
921
|
-
path: snapshot.path,
|
|
922
|
-
oldText: snapshot.oldText,
|
|
923
|
-
newText
|
|
924
|
-
}, ...formatted];
|
|
925
|
-
}
|
|
926
|
-
} catch {}
|
|
927
|
-
if (content === null) {
|
|
928
|
-
const formatted = formatToolContent(toolName, result, isError);
|
|
929
|
-
content = formatted.length > 0 ? formatted : null;
|
|
930
|
-
}
|
|
931
|
-
if (content === null && !isError && toolName !== "edit" && toolName !== "write") {
|
|
932
|
-
const text = extractStreamingText(result);
|
|
933
|
-
if (text) content = [{
|
|
934
|
-
type: "content",
|
|
935
|
-
content: {
|
|
936
|
-
type: "text",
|
|
937
|
-
text
|
|
938
|
-
}
|
|
939
|
-
}];
|
|
940
|
-
}
|
|
941
|
-
if (this.supportsTerminalOutput && isTerminalTool(toolName)) {
|
|
942
|
-
const outputText = extractStreamingText(result);
|
|
943
|
-
if (outputText !== "") this.emit({
|
|
944
|
-
sessionUpdate: "tool_call_update",
|
|
945
|
-
toolCallId,
|
|
946
|
-
status: "in_progress",
|
|
947
|
-
_meta: buildToolMeta(toolName, { terminal_output: {
|
|
948
|
-
terminal_id: toolCallId,
|
|
949
|
-
data: outputText
|
|
950
|
-
} }),
|
|
951
|
-
rawOutput: result
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_exit: {
|
|
955
|
-
terminal_id: toolCallId,
|
|
956
|
-
exit_code: extractExitCode(result),
|
|
957
|
-
signal: null
|
|
958
|
-
} } : void 0);
|
|
959
|
-
this.emit({
|
|
960
|
-
sessionUpdate: "tool_call_update",
|
|
961
|
-
toolCallId,
|
|
962
|
-
status: isError ? "failed" : "completed",
|
|
963
|
-
content,
|
|
964
|
-
_meta: meta,
|
|
965
|
-
rawOutput: result
|
|
966
|
-
});
|
|
967
|
-
this.currentToolCalls.delete(toolCallId);
|
|
968
|
-
this.editSnapshots.delete(toolCallId);
|
|
969
|
-
this.toolCallNames.delete(toolCallId);
|
|
970
|
-
}
|
|
971
|
-
handleAgentEnd() {
|
|
972
|
-
this.emitUsageUpdate();
|
|
973
|
-
this.flushEmits().finally(() => {
|
|
974
|
-
const reason = this.cancelRequested ? "cancelled" : mapPiStopReason(this.lastAssistantStopReason);
|
|
975
|
-
this.lastAssistantStopReason = null;
|
|
976
|
-
this.pendingTurn?.resolve(reason);
|
|
977
|
-
this.pendingTurn = null;
|
|
978
|
-
this.dequeueNextPrompt();
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Emit a usage_update notification with current context and cost data.
|
|
983
|
-
*/
|
|
984
|
-
emitUsageUpdate() {
|
|
985
|
-
const contextUsage = this.piSession.getContextUsage?.();
|
|
986
|
-
const stats = this.piSession.getSessionStats();
|
|
987
|
-
const used = contextUsage?.tokens ?? 0;
|
|
988
|
-
const size = contextUsage?.contextWindow ?? 0;
|
|
989
|
-
this.emit({
|
|
990
|
-
sessionUpdate: "usage_update",
|
|
991
|
-
used,
|
|
992
|
-
size,
|
|
993
|
-
cost: stats.cost > 0 ? {
|
|
994
|
-
amount: stats.cost,
|
|
995
|
-
currency: "USD"
|
|
996
|
-
} : null
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Build ACP Usage data from pi session stats for prompt response.
|
|
1001
|
-
*/
|
|
1002
|
-
getUsage() {
|
|
1003
|
-
const stats = this.piSession.getSessionStats();
|
|
1004
|
-
return {
|
|
1005
|
-
inputTokens: stats.tokens.input,
|
|
1006
|
-
outputTokens: stats.tokens.output,
|
|
1007
|
-
cachedReadTokens: stats.tokens.cacheRead,
|
|
1008
|
-
cachedWriteTokens: stats.tokens.cacheWrite
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Get cumulative session cost.
|
|
1013
|
-
*/
|
|
1014
|
-
getCost() {
|
|
1015
|
-
return this.piSession.getSessionStats().cost;
|
|
1016
|
-
}
|
|
1017
|
-
};
|
|
1018
|
-
function isTextBlock$1(v) {
|
|
1019
|
-
return typeof v === "object" && v !== null && "type" in v && v.type === "text" && "text" in v && typeof v.text === "string";
|
|
1020
|
-
}
|
|
1021
|
-
function extractStreamingText(result) {
|
|
1022
|
-
if (result === null || result === void 0) return "";
|
|
1023
|
-
if (typeof result === "string") return result;
|
|
1024
|
-
if (typeof result !== "object") return String(result);
|
|
1025
|
-
if ("content" in result && Array.isArray(result.content)) {
|
|
1026
|
-
const texts = [];
|
|
1027
|
-
for (const raw of result.content) if (isTextBlock$1(raw)) texts.push(raw.text);
|
|
1028
|
-
if (texts.length > 0) return texts.join("");
|
|
1029
|
-
}
|
|
1030
|
-
if ("details" in result) {
|
|
1031
|
-
const details = result.details;
|
|
1032
|
-
if (typeof details === "object" && details !== null) {
|
|
1033
|
-
if ("stdout" in details && typeof details.stdout === "string" && details.stdout.trim() !== "") return details.stdout;
|
|
1034
|
-
if ("output" in details && typeof details.output === "string" && details.output.trim() !== "") return details.output;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
if ("output" in result && typeof result.output === "string" && result.output.trim() !== "") return result.output;
|
|
1038
|
-
if ("stdout" in result && typeof result.stdout === "string" && result.stdout.trim() !== "") return result.stdout;
|
|
1039
|
-
return "";
|
|
1040
|
-
}
|
|
1041
|
-
function extractExitCode(result) {
|
|
1042
|
-
if (result === null || result === void 0 || typeof result !== "object") return null;
|
|
1043
|
-
if ("details" in result) {
|
|
1044
|
-
const details = result.details;
|
|
1045
|
-
if (typeof details === "object" && details !== null) {
|
|
1046
|
-
if ("exitCode" in details && typeof details.exitCode === "number") return details.exitCode;
|
|
1047
|
-
if ("code" in details && typeof details.code === "number") return details.code;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
if ("exitCode" in result && typeof result.exitCode === "number") return result.exitCode;
|
|
1051
|
-
if ("code" in result && typeof result.code === "number") return result.code;
|
|
1052
|
-
return null;
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Type guard to narrow AgentSessionEvent to the AgentEvent subset
|
|
1056
|
-
* (the variants we handle). Session-specific events like auto_compaction
|
|
1057
|
-
* are ignored.
|
|
1058
|
-
*/
|
|
1059
|
-
function isAgentEvent(ev) {
|
|
1060
|
-
return ev.type === "message_update" || ev.type === "message_end" || ev.type === "tool_execution_start" || ev.type === "tool_execution_update" || ev.type === "tool_execution_end" || ev.type === "agent_end";
|
|
1061
|
-
}
|
|
1062
|
-
//#endregion
|
|
1063
|
-
//#region src/acp/translate/pi-messages.ts
|
|
1064
|
-
function isTextBlock(block) {
|
|
1065
|
-
if (typeof block !== "object" || block === null) return false;
|
|
1066
|
-
return "type" in block && block.type === "text" && "text" in block && typeof block.text === "string";
|
|
1067
|
-
}
|
|
1068
|
-
function extractUserMessageText(content) {
|
|
1069
|
-
if (typeof content === "string") return content;
|
|
1070
|
-
if (!Array.isArray(content)) return "";
|
|
1071
|
-
return content.filter(isTextBlock).map((b) => b.text).join("");
|
|
1072
|
-
}
|
|
1073
|
-
//#endregion
|
|
1074
|
-
//#region src/acp/translate/prompt.ts
|
|
1075
|
-
function acpPromptToPiMessage(blocks) {
|
|
1076
|
-
let message = "";
|
|
1077
|
-
const images = [];
|
|
1078
|
-
for (const block of blocks) switch (block.type) {
|
|
1079
|
-
case "text":
|
|
1080
|
-
message += block.text;
|
|
1081
|
-
break;
|
|
1082
|
-
case "resource_link":
|
|
1083
|
-
message += `\n[Context] ${block.uri}`;
|
|
1084
|
-
break;
|
|
1085
|
-
case "image":
|
|
1086
|
-
images.push({
|
|
1087
|
-
type: "image",
|
|
1088
|
-
mimeType: block.mimeType,
|
|
1089
|
-
data: block.data
|
|
1090
|
-
});
|
|
1091
|
-
break;
|
|
1092
|
-
case "resource": {
|
|
1093
|
-
const resource = block.resource;
|
|
1094
|
-
const uri = resource.uri;
|
|
1095
|
-
const mime = resource.mimeType ?? null;
|
|
1096
|
-
if ("text" in resource) message += `\n[Embedded Context] ${uri} (${mime ?? "text/plain"})\n${resource.text}`;
|
|
1097
|
-
else if ("blob" in resource) {
|
|
1098
|
-
const bytes = Buffer.byteLength(resource.blob, "base64");
|
|
1099
|
-
message += `\n[Embedded Context] ${uri} (${mime ?? "application/octet-stream"}, ${bytes} bytes)`;
|
|
1100
|
-
} else message += `\n[Embedded Context] ${uri}`;
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
case "audio": {
|
|
1104
|
-
const bytes = Buffer.byteLength(block.data, "base64");
|
|
1105
|
-
message += `\n[Audio] (${block.mimeType}, ${bytes} bytes) not supported`;
|
|
1106
|
-
break;
|
|
1107
|
-
}
|
|
1108
|
-
default: break;
|
|
1109
|
-
}
|
|
1110
|
-
return {
|
|
1111
|
-
message,
|
|
1112
|
-
images
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
//#endregion
|
|
1116
|
-
//#region package.json
|
|
1117
|
-
var name = "@victor-software-house/pi-acp";
|
|
1118
|
-
var version = "0.5.0";
|
|
1119
|
-
//#endregion
|
|
1120
|
-
//#region src/acp/agent.ts
|
|
1121
|
-
/** Builtin ACP slash commands handled directly by the adapter. */
|
|
1122
|
-
const BUILTIN_COMMANDS = [
|
|
1123
|
-
{
|
|
1124
|
-
name: "compact",
|
|
1125
|
-
description: "Manually compact the session context",
|
|
1126
|
-
input: { hint: "optional custom instructions" }
|
|
1127
|
-
},
|
|
1128
|
-
{
|
|
1129
|
-
name: "autocompact",
|
|
1130
|
-
description: "Toggle automatic context compaction",
|
|
1131
|
-
input: { hint: "on|off|toggle" }
|
|
1132
|
-
},
|
|
1133
|
-
{
|
|
1134
|
-
name: "export",
|
|
1135
|
-
description: "Export session to an HTML file in the session cwd"
|
|
1136
|
-
},
|
|
1137
|
-
{
|
|
1138
|
-
name: "session",
|
|
1139
|
-
description: "Show session stats (messages, tokens, cost, session file)"
|
|
1140
|
-
},
|
|
1141
|
-
{
|
|
1142
|
-
name: "name",
|
|
1143
|
-
description: "Set session display name",
|
|
1144
|
-
input: { hint: "<name>" }
|
|
1145
|
-
},
|
|
1146
|
-
{
|
|
1147
|
-
name: "steering",
|
|
1148
|
-
description: "Get/set pi steering message delivery mode",
|
|
1149
|
-
input: { hint: "(no args to show) all | one-at-a-time" }
|
|
1150
|
-
},
|
|
1151
|
-
{
|
|
1152
|
-
name: "follow-up",
|
|
1153
|
-
description: "Get/set pi follow-up message delivery mode",
|
|
1154
|
-
input: { hint: "(no args to show) all | one-at-a-time" }
|
|
1155
|
-
},
|
|
1156
|
-
{
|
|
1157
|
-
name: "changelog",
|
|
1158
|
-
description: "Show pi changelog"
|
|
1159
|
-
}
|
|
1160
|
-
];
|
|
1161
|
-
/**
|
|
1162
|
-
* Deduplicate commands by name. First occurrence wins.
|
|
1163
|
-
*/
|
|
1164
|
-
function deduplicateCommands(commands) {
|
|
1165
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1166
|
-
const out = [];
|
|
1167
|
-
for (const c of commands) {
|
|
1168
|
-
if (seen.has(c.name)) continue;
|
|
1169
|
-
seen.add(c.name);
|
|
1170
|
-
out.push(c);
|
|
1171
|
-
}
|
|
1172
|
-
return out;
|
|
1173
|
-
}
|
|
1174
|
-
function parseArgs(input) {
|
|
1175
|
-
const args = [];
|
|
1176
|
-
let current = "";
|
|
1177
|
-
let quote = null;
|
|
1178
|
-
for (const ch of input) if (quote !== null) if (ch === quote) quote = null;
|
|
1179
|
-
else current += ch;
|
|
1180
|
-
else if (ch === "\"" || ch === "'") quote = ch;
|
|
1181
|
-
else if (ch === " " || ch === " ") {
|
|
1182
|
-
if (current !== "") {
|
|
1183
|
-
args.push(current);
|
|
1184
|
-
current = "";
|
|
1185
|
-
}
|
|
1186
|
-
} else current += ch;
|
|
1187
|
-
if (current !== "") args.push(current);
|
|
1188
|
-
return args;
|
|
1189
|
-
}
|
|
1190
|
-
const SESSION_TITLE_MAX = 100;
|
|
1191
|
-
function truncateSessionTitle(text) {
|
|
1192
|
-
const trimmed = text.trim();
|
|
1193
|
-
if (trimmed === "") return null;
|
|
1194
|
-
const oneLine = trimmed.replace(/\n/g, " ");
|
|
1195
|
-
if (oneLine.length <= SESSION_TITLE_MAX) return oneLine;
|
|
1196
|
-
return `${oneLine.slice(0, SESSION_TITLE_MAX - 1)}…`;
|
|
1197
|
-
}
|
|
1198
|
-
var PiAcpAgent = class {
|
|
1199
|
-
conn;
|
|
1200
|
-
sessions = new SessionManager$1();
|
|
1201
|
-
/** Cache of sessionId → file path, populated by listSessions and newSession. */
|
|
1202
|
-
sessionPaths = /* @__PURE__ */ new Map();
|
|
1203
|
-
/** Parsed client capability flags from initialize(). */
|
|
1204
|
-
clientCapabilities = {
|
|
1205
|
-
terminalOutput: false,
|
|
1206
|
-
terminalAuth: false,
|
|
1207
|
-
gatewayAuth: false
|
|
1208
|
-
};
|
|
1209
|
-
dispose() {
|
|
1210
|
-
this.sessions.disposeAll();
|
|
1211
|
-
}
|
|
1212
|
-
constructor(conn, _config) {
|
|
1213
|
-
this.conn = conn;
|
|
1214
|
-
}
|
|
1215
|
-
async initialize(params) {
|
|
1216
|
-
const supportedVersion = 1;
|
|
1217
|
-
const requested = params.protocolVersion;
|
|
1218
|
-
this.clientCapabilities = parseClientCapabilities(params.clientCapabilities);
|
|
1219
|
-
return {
|
|
1220
|
-
protocolVersion: requested === supportedVersion ? requested : supportedVersion,
|
|
1221
|
-
agentInfo: {
|
|
1222
|
-
name,
|
|
1223
|
-
title: "pi ACP adapter",
|
|
1224
|
-
version
|
|
1225
|
-
},
|
|
1226
|
-
authMethods: buildAuthMethods({ supportsTerminalAuthMeta: this.clientCapabilities.terminalAuth }),
|
|
1227
|
-
agentCapabilities: {
|
|
1228
|
-
loadSession: true,
|
|
1229
|
-
mcpCapabilities: {
|
|
1230
|
-
http: false,
|
|
1231
|
-
sse: false
|
|
1232
|
-
},
|
|
1233
|
-
promptCapabilities: {
|
|
1234
|
-
image: true,
|
|
1235
|
-
audio: false,
|
|
1236
|
-
embeddedContext: true
|
|
1237
|
-
},
|
|
1238
|
-
sessionCapabilities: {
|
|
1239
|
-
list: {},
|
|
1240
|
-
close: {},
|
|
1241
|
-
resume: {},
|
|
1242
|
-
fork: {}
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
async newSession(params) {
|
|
1248
|
-
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1249
|
-
let result;
|
|
1250
|
-
try {
|
|
1251
|
-
result = await createAgentSession({ cwd: params.cwd });
|
|
1252
|
-
} catch (e) {
|
|
1253
|
-
const authErr = detectAuthError(e);
|
|
1254
|
-
if (authErr !== null) throw authErr;
|
|
1255
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1256
|
-
throw RequestError.internalError({}, `Failed to create pi session: ${msg}`);
|
|
1257
|
-
}
|
|
1258
|
-
const piSession = result.session;
|
|
1259
|
-
if (piSession.modelRegistry.getAvailable().length === 0) {
|
|
1260
|
-
piSession.dispose();
|
|
1261
|
-
throw RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
|
|
1262
|
-
}
|
|
1263
|
-
const sessionId = piSession.sessionManager.getSessionId();
|
|
1264
|
-
const sessionFile = piSession.sessionManager.getSessionFile();
|
|
1265
|
-
if (sessionFile !== void 0) this.sessionPaths.set(sessionId, sessionFile);
|
|
1266
|
-
const session = new PiAcpSession({
|
|
1267
|
-
sessionId,
|
|
1268
|
-
cwd: params.cwd,
|
|
1269
|
-
mcpServers: params.mcpServers,
|
|
1270
|
-
piSession,
|
|
1271
|
-
conn: this.conn,
|
|
1272
|
-
supportsTerminalOutput: this.clientCapabilities.terminalOutput
|
|
1273
|
-
});
|
|
1274
|
-
this.sessions.register(session);
|
|
1275
|
-
const modes = buildThinkingModes(piSession);
|
|
1276
|
-
const models = buildModelState(piSession);
|
|
1277
|
-
const configOptions = buildConfigOptions(modes, models);
|
|
1278
|
-
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
1279
|
-
setTimeout(() => {
|
|
1280
|
-
(async () => {
|
|
1281
|
-
try {
|
|
1282
|
-
const commands = buildCommandList(piSession, enableSkillCommands);
|
|
1283
|
-
await this.conn.sessionUpdate({
|
|
1284
|
-
sessionId: session.sessionId,
|
|
1285
|
-
update: {
|
|
1286
|
-
sessionUpdate: "available_commands_update",
|
|
1287
|
-
availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
|
|
1288
|
-
}
|
|
1289
|
-
});
|
|
1290
|
-
} catch {}
|
|
1291
|
-
})();
|
|
1292
|
-
}, 0);
|
|
1293
|
-
return {
|
|
1294
|
-
sessionId: session.sessionId,
|
|
1295
|
-
configOptions,
|
|
1296
|
-
modes,
|
|
1297
|
-
models
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
async authenticate(_params) {
|
|
1301
|
-
return {};
|
|
1302
|
-
}
|
|
1303
|
-
async prompt(params) {
|
|
1304
|
-
const session = this.sessions.get(params.sessionId);
|
|
1305
|
-
const { message, images } = acpPromptToPiMessage(params.prompt);
|
|
1306
|
-
if (images.length === 0 && message.trimStart().startsWith("/")) {
|
|
1307
|
-
const trimmed = message.trim();
|
|
1308
|
-
const space = trimmed.indexOf(" ");
|
|
1309
|
-
const cmd = space === -1 ? trimmed.slice(1) : trimmed.slice(1, space);
|
|
1310
|
-
const args = parseArgs(space === -1 ? "" : trimmed.slice(space + 1));
|
|
1311
|
-
const handled = await this.handleBuiltinCommand(session, cmd, args);
|
|
1312
|
-
if (handled) return handled;
|
|
1313
|
-
}
|
|
1314
|
-
const result = await session.prompt(message, images);
|
|
1315
|
-
const stopReason = result === "error" ? "end_turn" : result;
|
|
1316
|
-
const usage = session.getUsage();
|
|
1317
|
-
const cost = session.getCost();
|
|
1318
|
-
return {
|
|
1319
|
-
stopReason,
|
|
1320
|
-
usage: {
|
|
1321
|
-
inputTokens: usage.inputTokens,
|
|
1322
|
-
outputTokens: usage.outputTokens,
|
|
1323
|
-
cachedReadTokens: usage.cachedReadTokens,
|
|
1324
|
-
cachedWriteTokens: usage.cachedWriteTokens,
|
|
1325
|
-
totalTokens: usage.inputTokens + usage.outputTokens
|
|
1326
|
-
},
|
|
1327
|
-
_meta: cost > 0 ? { cost: {
|
|
1328
|
-
amount: cost,
|
|
1329
|
-
currency: "USD"
|
|
1330
|
-
} } : {}
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
async cancel(params) {
|
|
1334
|
-
await this.sessions.get(params.sessionId).cancel();
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Resolve a session ID to a file path.
|
|
1338
|
-
* Checks the local cache first (populated by listSessions/newSession),
|
|
1339
|
-
* falls back to a full listAll() scan on cache miss.
|
|
1340
|
-
*/
|
|
1341
|
-
async resolveSessionFile(sessionId) {
|
|
1342
|
-
const cached = this.sessionPaths.get(sessionId);
|
|
1343
|
-
if (cached !== void 0) return cached;
|
|
1344
|
-
const all = await SessionManager.listAll();
|
|
1345
|
-
for (const s of all) this.sessionPaths.set(s.id, s.path);
|
|
1346
|
-
return this.sessionPaths.get(sessionId) ?? null;
|
|
1347
|
-
}
|
|
1348
|
-
/**
|
|
1349
|
-
* Replay persisted session messages as ACP session updates.
|
|
1350
|
-
*
|
|
1351
|
-
* Iterates through the message history, emitting structured updates for each
|
|
1352
|
-
* content block type: text, thinking, tool calls, and tool results. A map of
|
|
1353
|
-
* tool call IDs to their invocation data (from assistant messages) is built
|
|
1354
|
-
* to enrich subsequent tool result updates with rawInput and locations.
|
|
1355
|
-
*/
|
|
1356
|
-
async replaySessionHistory(session, messages) {
|
|
1357
|
-
const toolCallMap = /* @__PURE__ */ new Map();
|
|
1358
|
-
for (const m of messages) {
|
|
1359
|
-
if (!("role" in m)) continue;
|
|
1360
|
-
if (m.role === "user") {
|
|
1361
|
-
const text = extractUserMessageText(m.content);
|
|
1362
|
-
if (text) await this.conn.sessionUpdate({
|
|
1363
|
-
sessionId: session.sessionId,
|
|
1364
|
-
update: {
|
|
1365
|
-
sessionUpdate: "user_message_chunk",
|
|
1366
|
-
content: {
|
|
1367
|
-
type: "text",
|
|
1368
|
-
text
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
});
|
|
1372
|
-
continue;
|
|
1373
|
-
}
|
|
1374
|
-
if (m.role === "assistant") {
|
|
1375
|
-
const am = m;
|
|
1376
|
-
for (const block of am.content) if (block.type === "text" && block.text) await this.conn.sessionUpdate({
|
|
1377
|
-
sessionId: session.sessionId,
|
|
1378
|
-
update: {
|
|
1379
|
-
sessionUpdate: "agent_message_chunk",
|
|
1380
|
-
content: {
|
|
1381
|
-
type: "text",
|
|
1382
|
-
text: block.text
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
});
|
|
1386
|
-
else if (block.type === "thinking" && block.thinking) await this.conn.sessionUpdate({
|
|
1387
|
-
sessionId: session.sessionId,
|
|
1388
|
-
update: {
|
|
1389
|
-
sessionUpdate: "agent_thought_chunk",
|
|
1390
|
-
content: {
|
|
1391
|
-
type: "text",
|
|
1392
|
-
text: block.thinking
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
});
|
|
1396
|
-
else if (block.type === "toolCall") {
|
|
1397
|
-
const args = toToolArgs(block.arguments);
|
|
1398
|
-
toolCallMap.set(block.id, {
|
|
1399
|
-
name: block.name,
|
|
1400
|
-
args
|
|
1401
|
-
});
|
|
1402
|
-
const locations = resolveToolPath(args, session.cwd);
|
|
1403
|
-
await this.conn.sessionUpdate({
|
|
1404
|
-
sessionId: session.sessionId,
|
|
1405
|
-
update: {
|
|
1406
|
-
sessionUpdate: "tool_call",
|
|
1407
|
-
toolCallId: block.id,
|
|
1408
|
-
title: buildToolTitle(block.name, args),
|
|
1409
|
-
kind: toToolKind(block.name),
|
|
1410
|
-
status: "completed",
|
|
1411
|
-
rawInput: args,
|
|
1412
|
-
...locations ? { locations } : {},
|
|
1413
|
-
_meta: { piAcp: { toolName: block.name } }
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
continue;
|
|
1418
|
-
}
|
|
1419
|
-
if (m.role === "toolResult") {
|
|
1420
|
-
const tr = m;
|
|
1421
|
-
const toolName = tr.toolName;
|
|
1422
|
-
const toolCallId = tr.toolCallId;
|
|
1423
|
-
const isError = tr.isError;
|
|
1424
|
-
const invocation = toolCallMap.get(toolCallId);
|
|
1425
|
-
const args = invocation?.args;
|
|
1426
|
-
const locations = args !== void 0 ? resolveToolPath(args, session.cwd) : void 0;
|
|
1427
|
-
if (invocation === void 0) await this.conn.sessionUpdate({
|
|
1428
|
-
sessionId: session.sessionId,
|
|
1429
|
-
update: {
|
|
1430
|
-
sessionUpdate: "tool_call",
|
|
1431
|
-
toolCallId,
|
|
1432
|
-
title: buildToolTitle(toolName, {}),
|
|
1433
|
-
kind: toToolKind(toolName),
|
|
1434
|
-
status: "completed",
|
|
1435
|
-
rawInput: null,
|
|
1436
|
-
rawOutput: m,
|
|
1437
|
-
_meta: { piAcp: { toolName } }
|
|
1438
|
-
}
|
|
1439
|
-
});
|
|
1440
|
-
const content = formatToolContent(toolName, m, isError);
|
|
1441
|
-
await this.conn.sessionUpdate({
|
|
1442
|
-
sessionId: session.sessionId,
|
|
1443
|
-
update: {
|
|
1444
|
-
sessionUpdate: "tool_call_update",
|
|
1445
|
-
toolCallId,
|
|
1446
|
-
status: isError ? "failed" : "completed",
|
|
1447
|
-
content: content.length > 0 ? content : null,
|
|
1448
|
-
rawOutput: m,
|
|
1449
|
-
...locations ? { locations } : {},
|
|
1450
|
-
_meta: { piAcp: { toolName } }
|
|
1451
|
-
}
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
async listSessions(params) {
|
|
1457
|
-
const cwd = params.cwd;
|
|
1458
|
-
const raw = cwd !== void 0 && cwd !== null ? await SessionManager.list(cwd) : await SessionManager.listAll();
|
|
1459
|
-
for (const s of raw) this.sessionPaths.set(s.id, s.path);
|
|
1460
|
-
const sessions = raw.map((s) => ({
|
|
1461
|
-
id: s.id,
|
|
1462
|
-
cwd: s.cwd,
|
|
1463
|
-
name: s.name,
|
|
1464
|
-
firstMessage: s.firstMessage,
|
|
1465
|
-
modified: s.modified,
|
|
1466
|
-
messageCount: s.messageCount
|
|
1467
|
-
}));
|
|
1468
|
-
if (params.cursor !== void 0 && params.cursor !== null) {
|
|
1469
|
-
const parsed = Number.parseInt(params.cursor, 10);
|
|
1470
|
-
if (!Number.isFinite(parsed) || parsed < 0) throw RequestError.invalidParams(`Invalid cursor: ${params.cursor}`);
|
|
1471
|
-
}
|
|
1472
|
-
const start = params.cursor !== void 0 && params.cursor !== null ? Number.parseInt(params.cursor, 10) : 0;
|
|
1473
|
-
const PAGE_SIZE = 50;
|
|
1474
|
-
return {
|
|
1475
|
-
sessions: sessions.slice(start, start + PAGE_SIZE).map((s) => ({
|
|
1476
|
-
sessionId: s.id,
|
|
1477
|
-
cwd: s.cwd,
|
|
1478
|
-
title: (s.name !== void 0 && s.name !== "" ? s.name : null) ?? truncateSessionTitle(s.firstMessage) ?? null,
|
|
1479
|
-
updatedAt: s.modified.toISOString()
|
|
1480
|
-
})),
|
|
1481
|
-
nextCursor: start + PAGE_SIZE < sessions.length ? String(start + PAGE_SIZE) : null,
|
|
1482
|
-
_meta: {}
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
async loadSession(params) {
|
|
1486
|
-
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1487
|
-
this.sessions.close(params.sessionId);
|
|
1488
|
-
const sessionFile = await this.resolveSessionFile(params.sessionId);
|
|
1489
|
-
if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1490
|
-
let result;
|
|
1491
|
-
try {
|
|
1492
|
-
const sm = SessionManager.open(sessionFile);
|
|
1493
|
-
result = await createAgentSession({
|
|
1494
|
-
cwd: params.cwd,
|
|
1495
|
-
sessionManager: sm
|
|
1496
|
-
});
|
|
1497
|
-
} catch (e) {
|
|
1498
|
-
const authErr = detectAuthError(e);
|
|
1499
|
-
if (authErr !== null) throw authErr;
|
|
1500
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1501
|
-
throw RequestError.internalError({}, `Failed to load pi session: ${msg}`);
|
|
1502
|
-
}
|
|
1503
|
-
const piSession = result.session;
|
|
1504
|
-
const session = new PiAcpSession({
|
|
1505
|
-
sessionId: params.sessionId,
|
|
1506
|
-
cwd: params.cwd,
|
|
1507
|
-
mcpServers: params.mcpServers,
|
|
1508
|
-
piSession,
|
|
1509
|
-
conn: this.conn,
|
|
1510
|
-
supportsTerminalOutput: this.clientCapabilities.terminalOutput
|
|
1511
|
-
});
|
|
1512
|
-
this.sessions.register(session);
|
|
1513
|
-
await this.replaySessionHistory(session, piSession.messages);
|
|
1514
|
-
const modes = buildThinkingModes(piSession);
|
|
1515
|
-
const models = buildModelState(piSession);
|
|
1516
|
-
const configOptions = buildConfigOptions(modes, models);
|
|
1517
|
-
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
1518
|
-
setTimeout(() => {
|
|
1519
|
-
(async () => {
|
|
1520
|
-
try {
|
|
1521
|
-
const commands = buildCommandList(piSession, enableSkillCommands);
|
|
1522
|
-
await this.conn.sessionUpdate({
|
|
1523
|
-
sessionId: session.sessionId,
|
|
1524
|
-
update: {
|
|
1525
|
-
sessionUpdate: "available_commands_update",
|
|
1526
|
-
availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
|
|
1527
|
-
}
|
|
1528
|
-
});
|
|
1529
|
-
} catch {}
|
|
1530
|
-
})();
|
|
1531
|
-
}, 0);
|
|
1532
|
-
return {
|
|
1533
|
-
configOptions,
|
|
1534
|
-
modes,
|
|
1535
|
-
models
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
|
-
async closeSession(params) {
|
|
1539
|
-
if (this.sessions.maybeGet(params.sessionId) === void 0) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1540
|
-
this.sessions.close(params.sessionId);
|
|
1541
|
-
return {};
|
|
1542
|
-
}
|
|
1543
|
-
async resumeSession(params) {
|
|
1544
|
-
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1545
|
-
const existing = this.sessions.maybeGet(params.sessionId);
|
|
1546
|
-
if (existing !== void 0) {
|
|
1547
|
-
const modes = buildThinkingModes(existing.piSession);
|
|
1548
|
-
const models = buildModelState(existing.piSession);
|
|
1549
|
-
return {
|
|
1550
|
-
configOptions: buildConfigOptions(modes, models),
|
|
1551
|
-
modes,
|
|
1552
|
-
models
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
|
-
const sessionFile = await this.resolveSessionFile(params.sessionId);
|
|
1556
|
-
if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1557
|
-
let result;
|
|
1558
|
-
try {
|
|
1559
|
-
const sm = SessionManager.open(sessionFile);
|
|
1560
|
-
result = await createAgentSession({
|
|
1561
|
-
cwd: params.cwd,
|
|
1562
|
-
sessionManager: sm
|
|
1563
|
-
});
|
|
1564
|
-
} catch (e) {
|
|
1565
|
-
const authErr = detectAuthError(e);
|
|
1566
|
-
if (authErr !== null) throw authErr;
|
|
1567
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1568
|
-
throw RequestError.internalError({}, `Failed to resume pi session: ${msg}`);
|
|
1569
|
-
}
|
|
1570
|
-
const piSession = result.session;
|
|
1571
|
-
const session = new PiAcpSession({
|
|
1572
|
-
sessionId: params.sessionId,
|
|
1573
|
-
cwd: params.cwd,
|
|
1574
|
-
mcpServers: params.mcpServers ?? [],
|
|
1575
|
-
piSession,
|
|
1576
|
-
conn: this.conn,
|
|
1577
|
-
supportsTerminalOutput: this.clientCapabilities.terminalOutput
|
|
1578
|
-
});
|
|
1579
|
-
this.sessions.register(session);
|
|
1580
|
-
this.sessionPaths.set(params.sessionId, sessionFile);
|
|
1581
|
-
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
1582
|
-
setTimeout(() => {
|
|
1583
|
-
(async () => {
|
|
1584
|
-
try {
|
|
1585
|
-
const commands = buildCommandList(piSession, enableSkillCommands);
|
|
1586
|
-
await this.conn.sessionUpdate({
|
|
1587
|
-
sessionId: session.sessionId,
|
|
1588
|
-
update: {
|
|
1589
|
-
sessionUpdate: "available_commands_update",
|
|
1590
|
-
availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
|
|
1591
|
-
}
|
|
1592
|
-
});
|
|
1593
|
-
} catch {}
|
|
1594
|
-
})();
|
|
1595
|
-
}, 0);
|
|
1596
|
-
const modes = buildThinkingModes(piSession);
|
|
1597
|
-
const models = buildModelState(piSession);
|
|
1598
|
-
return {
|
|
1599
|
-
configOptions: buildConfigOptions(modes, models),
|
|
1600
|
-
modes,
|
|
1601
|
-
models
|
|
1602
|
-
};
|
|
1603
|
-
}
|
|
1604
|
-
async unstable_forkSession(params) {
|
|
1605
|
-
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1606
|
-
const sourceFile = await this.resolveSessionFile(params.sessionId);
|
|
1607
|
-
if (sourceFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1608
|
-
let result;
|
|
1609
|
-
try {
|
|
1610
|
-
const sm = SessionManager.forkFrom(sourceFile, params.cwd);
|
|
1611
|
-
result = await createAgentSession({
|
|
1612
|
-
cwd: params.cwd,
|
|
1613
|
-
sessionManager: sm
|
|
1614
|
-
});
|
|
1615
|
-
} catch (e) {
|
|
1616
|
-
const authErr = detectAuthError(e);
|
|
1617
|
-
if (authErr !== null) throw authErr;
|
|
1618
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1619
|
-
throw RequestError.internalError({}, `Failed to fork pi session: ${msg}`);
|
|
1620
|
-
}
|
|
1621
|
-
const piSession = result.session;
|
|
1622
|
-
const newSessionId = piSession.sessionManager.getSessionId();
|
|
1623
|
-
const newSessionFile = piSession.sessionManager.getSessionFile();
|
|
1624
|
-
if (newSessionFile !== void 0) this.sessionPaths.set(newSessionId, newSessionFile);
|
|
1625
|
-
const session = new PiAcpSession({
|
|
1626
|
-
sessionId: newSessionId,
|
|
1627
|
-
cwd: params.cwd,
|
|
1628
|
-
mcpServers: params.mcpServers ?? [],
|
|
1629
|
-
piSession,
|
|
1630
|
-
conn: this.conn,
|
|
1631
|
-
supportsTerminalOutput: this.clientCapabilities.terminalOutput
|
|
1632
|
-
});
|
|
1633
|
-
this.sessions.register(session);
|
|
1634
|
-
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
1635
|
-
setTimeout(() => {
|
|
1636
|
-
(async () => {
|
|
1637
|
-
try {
|
|
1638
|
-
const commands = buildCommandList(piSession, enableSkillCommands);
|
|
1639
|
-
await this.conn.sessionUpdate({
|
|
1640
|
-
sessionId: session.sessionId,
|
|
1641
|
-
update: {
|
|
1642
|
-
sessionUpdate: "available_commands_update",
|
|
1643
|
-
availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
|
|
1644
|
-
}
|
|
1645
|
-
});
|
|
1646
|
-
} catch {}
|
|
1647
|
-
})();
|
|
1648
|
-
}, 0);
|
|
1649
|
-
const modes = buildThinkingModes(piSession);
|
|
1650
|
-
const models = buildModelState(piSession);
|
|
1651
|
-
return {
|
|
1652
|
-
sessionId: newSessionId,
|
|
1653
|
-
configOptions: buildConfigOptions(modes, models),
|
|
1654
|
-
modes,
|
|
1655
|
-
models
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
async setSessionMode(params) {
|
|
1659
|
-
const session = this.sessions.get(params.sessionId);
|
|
1660
|
-
const mode = String(params.modeId);
|
|
1661
|
-
if (!isThinkingLevel(mode)) throw RequestError.invalidParams(`Unknown modeId: ${mode}`);
|
|
1662
|
-
session.piSession.setThinkingLevel(mode);
|
|
1663
|
-
this.conn.sessionUpdate({
|
|
1664
|
-
sessionId: session.sessionId,
|
|
1665
|
-
update: {
|
|
1666
|
-
sessionUpdate: "current_mode_update",
|
|
1667
|
-
currentModeId: mode
|
|
1668
|
-
}
|
|
1669
|
-
});
|
|
1670
|
-
this.emitConfigOptionUpdate(session);
|
|
1671
|
-
return {};
|
|
1672
|
-
}
|
|
1673
|
-
async unstable_setSessionModel(params) {
|
|
1674
|
-
const session = this.sessions.get(params.sessionId);
|
|
1675
|
-
const available = session.piSession.modelRegistry.getAvailable();
|
|
1676
|
-
const resolved = resolveModelPreference(available, params.modelId);
|
|
1677
|
-
if (resolved === null) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
|
|
1678
|
-
const model = available.find((m) => m.provider === resolved.provider && m.id === resolved.id);
|
|
1679
|
-
if (!model) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
|
|
1680
|
-
await session.piSession.setModel(model);
|
|
1681
|
-
this.emitConfigOptionUpdate(session);
|
|
1682
|
-
}
|
|
1683
|
-
async setSessionConfigOption(params) {
|
|
1684
|
-
const session = this.sessions.get(params.sessionId);
|
|
1685
|
-
const configId = String(params.configId);
|
|
1686
|
-
const value = String(params.value);
|
|
1687
|
-
if (configId === "model") {
|
|
1688
|
-
const available = session.piSession.modelRegistry.getAvailable();
|
|
1689
|
-
const resolved = resolveModelPreference(available, value);
|
|
1690
|
-
if (resolved === null) throw RequestError.invalidParams(`Unknown model: ${value}`);
|
|
1691
|
-
const model = available.find((m) => m.provider === resolved.provider && m.id === resolved.id);
|
|
1692
|
-
if (!model) throw RequestError.invalidParams(`Unknown model: ${value}`);
|
|
1693
|
-
await session.piSession.setModel(model);
|
|
1694
|
-
} else if (configId === "thought_level") {
|
|
1695
|
-
if (!isThinkingLevel(value)) throw RequestError.invalidParams(`Unknown thinking level: ${value}`);
|
|
1696
|
-
session.piSession.setThinkingLevel(value);
|
|
1697
|
-
} else throw RequestError.invalidParams(`Unknown config option: ${configId}`);
|
|
1698
|
-
return { configOptions: buildConfigOptions(buildThinkingModes(session.piSession), buildModelState(session.piSession)) };
|
|
1699
|
-
}
|
|
1700
|
-
emitConfigOptionUpdate(session) {
|
|
1701
|
-
const configOptions = buildConfigOptions(buildThinkingModes(session.piSession), buildModelState(session.piSession));
|
|
1702
|
-
this.conn.sessionUpdate({
|
|
1703
|
-
sessionId: session.sessionId,
|
|
1704
|
-
update: {
|
|
1705
|
-
sessionUpdate: "config_option_update",
|
|
1706
|
-
configOptions
|
|
1707
|
-
}
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1710
|
-
async handleBuiltinCommand(session, cmd, args) {
|
|
1711
|
-
const piSession = session.piSession;
|
|
1712
|
-
if (cmd === "compact") {
|
|
1713
|
-
const customInstructions = args.join(" ").trim() || void 0;
|
|
1714
|
-
const res = await piSession.compact(customInstructions);
|
|
1715
|
-
const text = [`Compaction completed.${customInstructions !== void 0 && customInstructions !== "" ? " (custom instructions applied)" : ""}`, typeof res?.tokensBefore === "number" ? `Tokens before: ${res.tokensBefore}` : null].filter(Boolean).join("\n") + (res?.summary ? `\n\n${res.summary}` : "");
|
|
1716
|
-
await this.conn.sessionUpdate({
|
|
1717
|
-
sessionId: session.sessionId,
|
|
1718
|
-
update: {
|
|
1719
|
-
sessionUpdate: "agent_message_chunk",
|
|
1720
|
-
content: {
|
|
1721
|
-
type: "text",
|
|
1722
|
-
text
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
});
|
|
1726
|
-
return { stopReason: "end_turn" };
|
|
1727
|
-
}
|
|
1728
|
-
if (cmd === "session") {
|
|
1729
|
-
const stats = piSession.getSessionStats();
|
|
1730
|
-
const lines = [];
|
|
1731
|
-
if (stats.sessionId !== void 0 && stats.sessionId !== "") lines.push(`Session: ${stats.sessionId}`);
|
|
1732
|
-
if (stats.sessionFile !== void 0 && stats.sessionFile !== "") lines.push(`Session file: ${stats.sessionFile}`);
|
|
1733
|
-
lines.push(`Messages: ${stats.totalMessages}`);
|
|
1734
|
-
lines.push(`Cost: ${stats.cost}`);
|
|
1735
|
-
const t = stats.tokens;
|
|
1736
|
-
const parts = [];
|
|
1737
|
-
if (t.input) parts.push(`in ${t.input}`);
|
|
1738
|
-
if (t.output) parts.push(`out ${t.output}`);
|
|
1739
|
-
if (t.cacheRead) parts.push(`cache read ${t.cacheRead}`);
|
|
1740
|
-
if (t.cacheWrite) parts.push(`cache write ${t.cacheWrite}`);
|
|
1741
|
-
if (t.total) parts.push(`total ${t.total}`);
|
|
1742
|
-
if (parts.length > 0) lines.push(`Tokens: ${parts.join(", ")}`);
|
|
1743
|
-
const text = lines.join("\n");
|
|
1744
|
-
await this.conn.sessionUpdate({
|
|
1745
|
-
sessionId: session.sessionId,
|
|
1746
|
-
update: {
|
|
1747
|
-
sessionUpdate: "agent_message_chunk",
|
|
1748
|
-
content: {
|
|
1749
|
-
type: "text",
|
|
1750
|
-
text
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
});
|
|
1754
|
-
return { stopReason: "end_turn" };
|
|
1755
|
-
}
|
|
1756
|
-
if (cmd === "name") {
|
|
1757
|
-
const name = args.join(" ").trim();
|
|
1758
|
-
if (!name) {
|
|
1759
|
-
await this.conn.sessionUpdate({
|
|
1760
|
-
sessionId: session.sessionId,
|
|
1761
|
-
update: {
|
|
1762
|
-
sessionUpdate: "agent_message_chunk",
|
|
1763
|
-
content: {
|
|
1764
|
-
type: "text",
|
|
1765
|
-
text: "Usage: /name <name>"
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
});
|
|
1769
|
-
return { stopReason: "end_turn" };
|
|
1770
|
-
}
|
|
1771
|
-
piSession.setSessionName(name);
|
|
1772
|
-
await this.conn.sessionUpdate({
|
|
1773
|
-
sessionId: session.sessionId,
|
|
1774
|
-
update: {
|
|
1775
|
-
sessionUpdate: "session_info_update",
|
|
1776
|
-
title: name,
|
|
1777
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1778
|
-
}
|
|
1779
|
-
});
|
|
1780
|
-
await this.conn.sessionUpdate({
|
|
1781
|
-
sessionId: session.sessionId,
|
|
1782
|
-
update: {
|
|
1783
|
-
sessionUpdate: "agent_message_chunk",
|
|
1784
|
-
content: {
|
|
1785
|
-
type: "text",
|
|
1786
|
-
text: `Session name set: ${name}`
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
});
|
|
1790
|
-
return { stopReason: "end_turn" };
|
|
1791
|
-
}
|
|
1792
|
-
if (cmd === "steering") {
|
|
1793
|
-
const modeRaw = String(args[0] ?? "").toLowerCase();
|
|
1794
|
-
if (!modeRaw) {
|
|
1795
|
-
await this.conn.sessionUpdate({
|
|
1796
|
-
sessionId: session.sessionId,
|
|
1797
|
-
update: {
|
|
1798
|
-
sessionUpdate: "agent_message_chunk",
|
|
1799
|
-
content: {
|
|
1800
|
-
type: "text",
|
|
1801
|
-
text: `Steering mode: ${piSession.steeringMode}`
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
});
|
|
1805
|
-
return { stopReason: "end_turn" };
|
|
1806
|
-
}
|
|
1807
|
-
if (modeRaw !== "all" && modeRaw !== "one-at-a-time") {
|
|
1808
|
-
await this.conn.sessionUpdate({
|
|
1809
|
-
sessionId: session.sessionId,
|
|
1810
|
-
update: {
|
|
1811
|
-
sessionUpdate: "agent_message_chunk",
|
|
1812
|
-
content: {
|
|
1813
|
-
type: "text",
|
|
1814
|
-
text: "Usage: /steering all | /steering one-at-a-time"
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
});
|
|
1818
|
-
return { stopReason: "end_turn" };
|
|
1819
|
-
}
|
|
1820
|
-
piSession.setSteeringMode(modeRaw);
|
|
1821
|
-
await this.conn.sessionUpdate({
|
|
1822
|
-
sessionId: session.sessionId,
|
|
1823
|
-
update: {
|
|
1824
|
-
sessionUpdate: "agent_message_chunk",
|
|
1825
|
-
content: {
|
|
1826
|
-
type: "text",
|
|
1827
|
-
text: `Steering mode set to: ${modeRaw}`
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
});
|
|
1831
|
-
return { stopReason: "end_turn" };
|
|
1832
|
-
}
|
|
1833
|
-
if (cmd === "follow-up") {
|
|
1834
|
-
const modeRaw = String(args[0] ?? "").toLowerCase();
|
|
1835
|
-
if (!modeRaw) {
|
|
1836
|
-
await this.conn.sessionUpdate({
|
|
1837
|
-
sessionId: session.sessionId,
|
|
1838
|
-
update: {
|
|
1839
|
-
sessionUpdate: "agent_message_chunk",
|
|
1840
|
-
content: {
|
|
1841
|
-
type: "text",
|
|
1842
|
-
text: `Follow-up mode: ${piSession.followUpMode}`
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
});
|
|
1846
|
-
return { stopReason: "end_turn" };
|
|
1847
|
-
}
|
|
1848
|
-
if (modeRaw !== "all" && modeRaw !== "one-at-a-time") {
|
|
1849
|
-
await this.conn.sessionUpdate({
|
|
1850
|
-
sessionId: session.sessionId,
|
|
1851
|
-
update: {
|
|
1852
|
-
sessionUpdate: "agent_message_chunk",
|
|
1853
|
-
content: {
|
|
1854
|
-
type: "text",
|
|
1855
|
-
text: "Usage: /follow-up all | /follow-up one-at-a-time"
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
});
|
|
1859
|
-
return { stopReason: "end_turn" };
|
|
1860
|
-
}
|
|
1861
|
-
piSession.setFollowUpMode(modeRaw);
|
|
1862
|
-
await this.conn.sessionUpdate({
|
|
1863
|
-
sessionId: session.sessionId,
|
|
1864
|
-
update: {
|
|
1865
|
-
sessionUpdate: "agent_message_chunk",
|
|
1866
|
-
content: {
|
|
1867
|
-
type: "text",
|
|
1868
|
-
text: `Follow-up mode set to: ${modeRaw}`
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
});
|
|
1872
|
-
return { stopReason: "end_turn" };
|
|
1873
|
-
}
|
|
1874
|
-
if (cmd === "autocompact") {
|
|
1875
|
-
const mode = (args[0] ?? "toggle").toLowerCase();
|
|
1876
|
-
let enabled = null;
|
|
1877
|
-
if (mode === "on" || mode === "true" || mode === "enable") enabled = true;
|
|
1878
|
-
else if (mode === "off" || mode === "false" || mode === "disable") enabled = false;
|
|
1879
|
-
if (enabled === null) enabled = !piSession.autoCompactionEnabled;
|
|
1880
|
-
piSession.setAutoCompactionEnabled(enabled);
|
|
1881
|
-
await this.conn.sessionUpdate({
|
|
1882
|
-
sessionId: session.sessionId,
|
|
1883
|
-
update: {
|
|
1884
|
-
sessionUpdate: "agent_message_chunk",
|
|
1885
|
-
content: {
|
|
1886
|
-
type: "text",
|
|
1887
|
-
text: `Auto-compaction ${enabled ? "enabled" : "disabled"}.`
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
});
|
|
1891
|
-
return { stopReason: "end_turn" };
|
|
1892
|
-
}
|
|
1893
|
-
if (cmd === "changelog") {
|
|
1894
|
-
const changelogPath = findChangelog();
|
|
1895
|
-
if (changelogPath === null) {
|
|
1896
|
-
await this.conn.sessionUpdate({
|
|
1897
|
-
sessionId: session.sessionId,
|
|
1898
|
-
update: {
|
|
1899
|
-
sessionUpdate: "agent_message_chunk",
|
|
1900
|
-
content: {
|
|
1901
|
-
type: "text",
|
|
1902
|
-
text: "Changelog not found."
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
});
|
|
1906
|
-
return { stopReason: "end_turn" };
|
|
1907
|
-
}
|
|
1908
|
-
let text = "";
|
|
1909
|
-
try {
|
|
1910
|
-
text = readFileSync(changelogPath, "utf-8");
|
|
1911
|
-
} catch (e) {
|
|
1912
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1913
|
-
await this.conn.sessionUpdate({
|
|
1914
|
-
sessionId: session.sessionId,
|
|
1915
|
-
update: {
|
|
1916
|
-
sessionUpdate: "agent_message_chunk",
|
|
1917
|
-
content: {
|
|
1918
|
-
type: "text",
|
|
1919
|
-
text: `Failed to read changelog: ${msg}`
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
});
|
|
1923
|
-
return { stopReason: "end_turn" };
|
|
1924
|
-
}
|
|
1925
|
-
const maxChars = 2e4;
|
|
1926
|
-
if (text.length > maxChars) text = `${text.slice(0, maxChars)}\n\n...(truncated)...`;
|
|
1927
|
-
await this.conn.sessionUpdate({
|
|
1928
|
-
sessionId: session.sessionId,
|
|
1929
|
-
update: {
|
|
1930
|
-
sessionUpdate: "agent_message_chunk",
|
|
1931
|
-
content: {
|
|
1932
|
-
type: "text",
|
|
1933
|
-
text
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
});
|
|
1937
|
-
return { stopReason: "end_turn" };
|
|
1938
|
-
}
|
|
1939
|
-
if (cmd === "export") {
|
|
1940
|
-
if (piSession.messages.length === 0) {
|
|
1941
|
-
await this.conn.sessionUpdate({
|
|
1942
|
-
sessionId: session.sessionId,
|
|
1943
|
-
update: {
|
|
1944
|
-
sessionUpdate: "agent_message_chunk",
|
|
1945
|
-
content: {
|
|
1946
|
-
type: "text",
|
|
1947
|
-
text: "Nothing to export yet. Send a prompt first."
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
});
|
|
1951
|
-
return { stopReason: "end_turn" };
|
|
1952
|
-
}
|
|
1953
|
-
try {
|
|
1954
|
-
const safeSessionId = session.sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1955
|
-
const outputPath = join(session.cwd, `pi-session-${safeSessionId}.html`);
|
|
1956
|
-
const resultPath = await piSession.exportToHtml(outputPath);
|
|
1957
|
-
await this.conn.sessionUpdate({
|
|
1958
|
-
sessionId: session.sessionId,
|
|
1959
|
-
update: {
|
|
1960
|
-
sessionUpdate: "agent_message_chunk",
|
|
1961
|
-
content: {
|
|
1962
|
-
type: "text",
|
|
1963
|
-
text: "Session exported: "
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
});
|
|
1967
|
-
await this.conn.sessionUpdate({
|
|
1968
|
-
sessionId: session.sessionId,
|
|
1969
|
-
update: {
|
|
1970
|
-
sessionUpdate: "agent_message_chunk",
|
|
1971
|
-
content: {
|
|
1972
|
-
type: "resource_link",
|
|
1973
|
-
name: `pi-session-${safeSessionId}.html`,
|
|
1974
|
-
uri: `file://${resultPath}`,
|
|
1975
|
-
mimeType: "text/html",
|
|
1976
|
-
title: "Session exported"
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
});
|
|
1980
|
-
} catch (e) {
|
|
1981
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1982
|
-
await this.conn.sessionUpdate({
|
|
1983
|
-
sessionId: session.sessionId,
|
|
1984
|
-
update: {
|
|
1985
|
-
sessionUpdate: "agent_message_chunk",
|
|
1986
|
-
content: {
|
|
1987
|
-
type: "text",
|
|
1988
|
-
text: `Export failed: ${msg}`
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
});
|
|
1992
|
-
}
|
|
1993
|
-
return { stopReason: "end_turn" };
|
|
1994
|
-
}
|
|
1995
|
-
return null;
|
|
1996
|
-
}
|
|
1997
|
-
};
|
|
1998
|
-
function isThinkingLevel(x) {
|
|
1999
|
-
return x === "off" || x === "minimal" || x === "low" || x === "medium" || x === "high" || x === "xhigh";
|
|
2000
|
-
}
|
|
2001
|
-
function buildThinkingModes(piSession) {
|
|
2002
|
-
const levels = piSession.getAvailableThinkingLevels();
|
|
2003
|
-
return {
|
|
2004
|
-
currentModeId: piSession.thinkingLevel,
|
|
2005
|
-
availableModes: levels.map((id) => ({
|
|
2006
|
-
id,
|
|
2007
|
-
name: `Thinking: ${id}`,
|
|
2008
|
-
description: null
|
|
2009
|
-
}))
|
|
2010
|
-
};
|
|
2011
|
-
}
|
|
2012
|
-
function buildModelState(piSession) {
|
|
2013
|
-
const available = piSession.modelRegistry.getAvailable();
|
|
2014
|
-
const current = piSession.model;
|
|
2015
|
-
const availableModels = available.map((m) => ({
|
|
2016
|
-
modelId: `${m.provider}/${m.id}`,
|
|
2017
|
-
name: `${m.provider}/${m.name ?? m.id}`,
|
|
2018
|
-
description: null
|
|
2019
|
-
}));
|
|
2020
|
-
let currentModelId = "default";
|
|
2021
|
-
if (current !== void 0) currentModelId = `${current.provider}/${current.id}`;
|
|
2022
|
-
else if (availableModels.length > 0 && availableModels[0] !== void 0) currentModelId = availableModels[0].modelId;
|
|
2023
|
-
return {
|
|
2024
|
-
availableModels,
|
|
2025
|
-
currentModelId
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
function buildConfigOptions(modes, models) {
|
|
2029
|
-
return [{
|
|
2030
|
-
id: "model",
|
|
2031
|
-
name: "Model",
|
|
2032
|
-
description: "AI model to use",
|
|
2033
|
-
category: "model",
|
|
2034
|
-
type: "select",
|
|
2035
|
-
currentValue: models.currentModelId,
|
|
2036
|
-
options: models.availableModels.map((m) => ({
|
|
2037
|
-
value: m.modelId,
|
|
2038
|
-
name: m.name,
|
|
2039
|
-
description: m.description ?? null
|
|
2040
|
-
}))
|
|
2041
|
-
}, {
|
|
2042
|
-
id: "thought_level",
|
|
2043
|
-
name: "Thinking Level",
|
|
2044
|
-
description: "Reasoning depth for models that support it",
|
|
2045
|
-
category: "thought_level",
|
|
2046
|
-
type: "select",
|
|
2047
|
-
currentValue: modes.currentModeId,
|
|
2048
|
-
options: modes.availableModes.map((m) => ({
|
|
2049
|
-
value: m.id,
|
|
2050
|
-
name: m.name,
|
|
2051
|
-
description: m.description ?? null
|
|
2052
|
-
}))
|
|
2053
|
-
}];
|
|
2054
|
-
}
|
|
2055
|
-
function buildCommandList(piSession, enableSkillCommands) {
|
|
2056
|
-
const commands = [];
|
|
2057
|
-
for (const template of piSession.promptTemplates) commands.push({
|
|
2058
|
-
name: template.name,
|
|
2059
|
-
description: template.description ?? `(prompt)`
|
|
2060
|
-
});
|
|
2061
|
-
if (enableSkillCommands) {
|
|
2062
|
-
const skills = piSession.resourceLoader.getSkills();
|
|
2063
|
-
for (const skill of skills.skills) commands.push({
|
|
2064
|
-
name: `skill:${skill.name}`,
|
|
2065
|
-
description: skill.description ?? `(skill)`
|
|
2066
|
-
});
|
|
2067
|
-
}
|
|
2068
|
-
for (const cmd of piSession.extensionRunner.getRegisteredCommands()) commands.push({
|
|
2069
|
-
name: cmd.name,
|
|
2070
|
-
description: cmd.description ?? "(extension)"
|
|
2071
|
-
});
|
|
2072
|
-
return commands;
|
|
2073
|
-
}
|
|
2074
|
-
function findChangelog() {
|
|
2075
|
-
try {
|
|
2076
|
-
const which = spawnSync(process.platform === "win32" ? "where" : "which", ["pi"], { encoding: "utf-8" });
|
|
2077
|
-
const piPath = String(which.stdout ?? "").split(/\r?\n/)[0]?.trim();
|
|
2078
|
-
if (piPath !== void 0 && piPath !== "") {
|
|
2079
|
-
const p = join(dirname(dirname(realpathSync(piPath))), "CHANGELOG.md");
|
|
2080
|
-
if (existsSync(p)) return p;
|
|
2081
|
-
}
|
|
2082
|
-
} catch {}
|
|
2083
|
-
try {
|
|
2084
|
-
const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
|
|
2085
|
-
const root = String(npmRoot.stdout ?? "").trim();
|
|
2086
|
-
if (root) {
|
|
2087
|
-
const p = join(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
|
|
2088
|
-
if (existsSync(p)) return p;
|
|
2089
|
-
}
|
|
2090
|
-
} catch {}
|
|
2091
|
-
return null;
|
|
2092
|
-
}
|
|
2093
|
-
//#endregion
|
|
2094
|
-
//#region src/index.ts
|
|
2095
16
|
{
|
|
2096
17
|
const toStderr = (...args) => {
|
|
2097
18
|
process.stderr.write(`${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}\n`);
|
|
@@ -2101,10 +22,22 @@ function findChangelog() {
|
|
|
2101
22
|
console.warn = toStderr;
|
|
2102
23
|
console.debug = toStderr;
|
|
2103
24
|
}
|
|
2104
|
-
|
|
25
|
+
const argv = process.argv.slice(2);
|
|
26
|
+
if (argv.includes("--terminal-login")) await runTerminalLogin();
|
|
27
|
+
else if (argv.includes("--daemon")) {
|
|
28
|
+
const { runDaemon } = await import("./daemon-irIzm1zJ.mjs");
|
|
29
|
+
await runDaemon();
|
|
30
|
+
} else if (argv.includes("--no-daemon") || process.env["PI_ACP_NO_DAEMON"] === "1") {
|
|
31
|
+
const { runInProcess } = await import("./in-process-DcAV6Sgx.mjs");
|
|
32
|
+
runInProcess();
|
|
33
|
+
} else {
|
|
34
|
+
const { runClient } = await import("./client-CTg5Oiz5.mjs");
|
|
35
|
+
await runClient();
|
|
36
|
+
}
|
|
37
|
+
async function runTerminalLogin() {
|
|
2105
38
|
const { spawnSync } = await import("node:child_process");
|
|
2106
39
|
const isWindows = platform() === "win32";
|
|
2107
|
-
const cmd = process.env
|
|
40
|
+
const cmd = process.env["PI_ACP_PI_COMMAND"] ?? (isWindows ? "pi.cmd" : "pi");
|
|
2108
41
|
const res = spawnSync(cmd, [], {
|
|
2109
42
|
stdio: "inherit",
|
|
2110
43
|
env: process.env
|
|
@@ -2116,39 +49,6 @@ if (process.argv.includes("--terminal-login")) {
|
|
|
2116
49
|
}
|
|
2117
50
|
process.exit(typeof res.status === "number" ? res.status : 1);
|
|
2118
51
|
}
|
|
2119
|
-
const agent = new AgentSideConnection((conn) => new PiAcpAgent(conn), ndJsonStream(new WritableStream({ write(chunk) {
|
|
2120
|
-
return new Promise((resolve) => {
|
|
2121
|
-
if (process.stdout.destroyed || !process.stdout.writable) {
|
|
2122
|
-
resolve();
|
|
2123
|
-
return;
|
|
2124
|
-
}
|
|
2125
|
-
try {
|
|
2126
|
-
process.stdout.write(chunk, () => resolve());
|
|
2127
|
-
} catch {
|
|
2128
|
-
resolve();
|
|
2129
|
-
}
|
|
2130
|
-
});
|
|
2131
|
-
} }), new ReadableStream({ start(controller) {
|
|
2132
|
-
process.stdin.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
|
|
2133
|
-
process.stdin.on("end", () => controller.close());
|
|
2134
|
-
process.stdin.on("error", (err) => controller.error(err));
|
|
2135
|
-
} })));
|
|
2136
|
-
let shuttingDown = false;
|
|
2137
|
-
function shutdown() {
|
|
2138
|
-
if (shuttingDown) return;
|
|
2139
|
-
shuttingDown = true;
|
|
2140
|
-
try {
|
|
2141
|
-
if ("agent" in agent) {
|
|
2142
|
-
const inner = agent.agent;
|
|
2143
|
-
if (typeof inner === "object" && inner !== null && "dispose" in inner && typeof inner.dispose === "function") inner.dispose();
|
|
2144
|
-
}
|
|
2145
|
-
} catch {}
|
|
2146
|
-
process.exit(0);
|
|
2147
|
-
}
|
|
2148
|
-
agent.closed.then(shutdown);
|
|
2149
|
-
process.on("SIGINT", shutdown);
|
|
2150
|
-
process.on("SIGTERM", shutdown);
|
|
2151
|
-
process.stdout.on("error", () => process.exit(0));
|
|
2152
52
|
//#endregion
|
|
2153
53
|
export {};
|
|
2154
54
|
|