@victor-software-house/pi-acp 0.1.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/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/index.mjs +1579 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +73 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1579 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { AgentSideConnection, RequestError, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { SessionManager, VERSION, createAgentSession } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import * as z from "zod";
|
|
10
|
+
//#region src/pi-auth/status.ts
|
|
11
|
+
/**
|
|
12
|
+
* Detect whether the user has any pi authentication configured.
|
|
13
|
+
*
|
|
14
|
+
* Checks three sources:
|
|
15
|
+
* 1. auth.json (API keys, OAuth credentials)
|
|
16
|
+
* 2. models.json custom provider apiKey entries
|
|
17
|
+
* 3. Known provider environment variables
|
|
18
|
+
*/
|
|
19
|
+
const modelsConfigSchema = z.object({ providers: z.record(z.string().trim(), z.object({ apiKey: z.string().trim().optional() })).optional() });
|
|
20
|
+
function agentDir() {
|
|
21
|
+
const env = process.env.PI_CODING_AGENT_DIR;
|
|
22
|
+
if (env === void 0) return join(homedir(), ".pi", "agent");
|
|
23
|
+
if (env === "~") return homedir();
|
|
24
|
+
if (env.startsWith("~/")) return homedir() + env.slice(1);
|
|
25
|
+
return env;
|
|
26
|
+
}
|
|
27
|
+
function readJsonFile(path) {
|
|
28
|
+
try {
|
|
29
|
+
if (!existsSync(path)) return null;
|
|
30
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
31
|
+
if (!raw) return null;
|
|
32
|
+
return JSON.parse(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function hasAuthJson() {
|
|
38
|
+
const data = readJsonFile(join(agentDir(), "auth.json"));
|
|
39
|
+
return typeof data === "object" && data !== null && Object.keys(data).length > 0;
|
|
40
|
+
}
|
|
41
|
+
function hasCustomProviderKey() {
|
|
42
|
+
const raw = readJsonFile(join(agentDir(), "models.json"));
|
|
43
|
+
const result = modelsConfigSchema.safeParse(raw);
|
|
44
|
+
if (!result.success || !result.data.providers) return false;
|
|
45
|
+
return Object.values(result.data.providers).some((provider) => typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0);
|
|
46
|
+
}
|
|
47
|
+
/** Environment variables that indicate a configured provider API key. */
|
|
48
|
+
const PROVIDER_ENV_VARS = [
|
|
49
|
+
"ANTHROPIC_API_KEY",
|
|
50
|
+
"ANTHROPIC_OAUTH_TOKEN",
|
|
51
|
+
"OPENAI_API_KEY",
|
|
52
|
+
"AZURE_OPENAI_API_KEY",
|
|
53
|
+
"GEMINI_API_KEY",
|
|
54
|
+
"GROQ_API_KEY",
|
|
55
|
+
"CEREBRAS_API_KEY",
|
|
56
|
+
"XAI_API_KEY",
|
|
57
|
+
"OPENROUTER_API_KEY",
|
|
58
|
+
"AI_GATEWAY_API_KEY",
|
|
59
|
+
"ZAI_API_KEY",
|
|
60
|
+
"MISTRAL_API_KEY",
|
|
61
|
+
"MINIMAX_API_KEY",
|
|
62
|
+
"MINIMAX_CN_API_KEY",
|
|
63
|
+
"HF_TOKEN",
|
|
64
|
+
"OPENCODE_API_KEY",
|
|
65
|
+
"KIMI_API_KEY",
|
|
66
|
+
"COPILOT_GITHUB_TOKEN",
|
|
67
|
+
"GH_TOKEN",
|
|
68
|
+
"GITHUB_TOKEN"
|
|
69
|
+
];
|
|
70
|
+
function hasProviderEnvVar() {
|
|
71
|
+
return PROVIDER_ENV_VARS.some((key) => {
|
|
72
|
+
const val = process.env[key];
|
|
73
|
+
return typeof val === "string" && val.trim().length > 0;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function hasPiAuthConfigured() {
|
|
77
|
+
return hasAuthJson() || hasCustomProviderKey() || hasProviderEnvVar();
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/acp/auth.ts
|
|
81
|
+
const AUTH_METHOD_ID = "pi_terminal_login";
|
|
82
|
+
function buildAuthMethods(opts) {
|
|
83
|
+
const supportsTerminalAuthMeta = opts?.supportsTerminalAuthMeta ?? true;
|
|
84
|
+
const method = {
|
|
85
|
+
id: AUTH_METHOD_ID,
|
|
86
|
+
name: "Launch pi in the terminal",
|
|
87
|
+
description: "Start pi in an interactive terminal to configure API keys or login",
|
|
88
|
+
type: "terminal",
|
|
89
|
+
args: ["--terminal-login"],
|
|
90
|
+
env: {}
|
|
91
|
+
};
|
|
92
|
+
if (supportsTerminalAuthMeta) {
|
|
93
|
+
const launch = resolveTerminalLaunchCommand();
|
|
94
|
+
method._meta = { "terminal-auth": {
|
|
95
|
+
...launch,
|
|
96
|
+
label: "Launch pi"
|
|
97
|
+
} };
|
|
98
|
+
}
|
|
99
|
+
return [method];
|
|
100
|
+
}
|
|
101
|
+
function resolveTerminalLaunchCommand() {
|
|
102
|
+
const argv0 = process.argv[0] ?? "node";
|
|
103
|
+
const argv1 = process.argv[1];
|
|
104
|
+
if (argv1 !== void 0 && argv0.includes("node") && argv1.endsWith(".js")) return {
|
|
105
|
+
command: argv0,
|
|
106
|
+
args: [argv1, "--terminal-login"]
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
command: "pi-acp",
|
|
110
|
+
args: ["--terminal-login"]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/acp/pi-settings.ts
|
|
115
|
+
/**
|
|
116
|
+
* Read pi settings from global and project config files.
|
|
117
|
+
*
|
|
118
|
+
* Settings are merged: project overrides global.
|
|
119
|
+
* Paths follow pi-mono conventions:
|
|
120
|
+
* Global: ~/.pi/agent/settings.json
|
|
121
|
+
* Project: <cwd>/.pi/settings.json
|
|
122
|
+
*/
|
|
123
|
+
const piSettingsSchema = z.object({
|
|
124
|
+
enableSkillCommands: z.boolean().optional(),
|
|
125
|
+
quietStartup: z.boolean().optional(),
|
|
126
|
+
quietStart: z.boolean().optional(),
|
|
127
|
+
skills: z.object({ enableSkillCommands: z.boolean().optional() }).optional()
|
|
128
|
+
});
|
|
129
|
+
function isRecord(x) {
|
|
130
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
131
|
+
}
|
|
132
|
+
function merge(base, override) {
|
|
133
|
+
const result = { ...base };
|
|
134
|
+
for (const [key, val] of Object.entries(override)) {
|
|
135
|
+
const existing = result[key];
|
|
136
|
+
if (isRecord(existing) && isRecord(val)) result[key] = merge(existing, val);
|
|
137
|
+
else result[key] = val;
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function readJson(path) {
|
|
142
|
+
try {
|
|
143
|
+
if (!existsSync(path)) return {};
|
|
144
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
145
|
+
return isRecord(data) ? data : {};
|
|
146
|
+
} catch {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function piAgentDir() {
|
|
151
|
+
return process.env.PI_CODING_AGENT_DIR !== void 0 ? resolve(process.env.PI_CODING_AGENT_DIR) : join(homedir(), ".pi", "agent");
|
|
152
|
+
}
|
|
153
|
+
function resolvedSettings(cwd) {
|
|
154
|
+
const globalPath = join(piAgentDir(), "settings.json");
|
|
155
|
+
const projectPath = resolve(cwd, ".pi", "settings.json");
|
|
156
|
+
const merged = merge(readJson(globalPath), readJson(projectPath));
|
|
157
|
+
const result = piSettingsSchema.safeParse(merged);
|
|
158
|
+
return result.success ? result.data : {};
|
|
159
|
+
}
|
|
160
|
+
function skillCommandsEnabled(cwd) {
|
|
161
|
+
const settings = resolvedSettings(cwd);
|
|
162
|
+
if (typeof settings.enableSkillCommands === "boolean") return settings.enableSkillCommands;
|
|
163
|
+
if (typeof settings.skills?.enableSkillCommands === "boolean") return settings.skills.enableSkillCommands;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
function quietStartupEnabled(cwd) {
|
|
167
|
+
const settings = resolvedSettings(cwd);
|
|
168
|
+
if (typeof settings.quietStartup === "boolean") return settings.quietStartup;
|
|
169
|
+
if (typeof settings.quietStart === "boolean") return settings.quietStart;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/acp/translate/pi-tools.ts
|
|
174
|
+
/**
|
|
175
|
+
* Extract displayable text from a pi tool result.
|
|
176
|
+
*
|
|
177
|
+
* Pi tool results have varying shapes depending on the tool. This function
|
|
178
|
+
* tries content blocks first, then falls back to details fields (diff, stdout/stderr),
|
|
179
|
+
* and finally JSON serialization as a last resort.
|
|
180
|
+
*/
|
|
181
|
+
const textBlockSchema = z.object({
|
|
182
|
+
type: z.literal("text"),
|
|
183
|
+
text: z.string()
|
|
184
|
+
});
|
|
185
|
+
const toolDetailsSchema = z.object({
|
|
186
|
+
diff: z.string().optional(),
|
|
187
|
+
stdout: z.string().optional(),
|
|
188
|
+
stderr: z.string().optional(),
|
|
189
|
+
output: z.string().optional(),
|
|
190
|
+
exitCode: z.number().optional(),
|
|
191
|
+
code: z.number().optional()
|
|
192
|
+
});
|
|
193
|
+
const toolResultSchema = z.object({
|
|
194
|
+
content: z.array(z.unknown()).optional(),
|
|
195
|
+
details: toolDetailsSchema.optional(),
|
|
196
|
+
stdout: z.string().optional(),
|
|
197
|
+
stderr: z.string().optional(),
|
|
198
|
+
output: z.string().optional(),
|
|
199
|
+
exitCode: z.number().optional(),
|
|
200
|
+
code: z.number().optional()
|
|
201
|
+
});
|
|
202
|
+
function toolResultToText(result) {
|
|
203
|
+
if (result === null || result === void 0 || typeof result !== "object") return "";
|
|
204
|
+
const parsed = toolResultSchema.safeParse(result);
|
|
205
|
+
if (!parsed.success) try {
|
|
206
|
+
return JSON.stringify(result, null, 2);
|
|
207
|
+
} catch {
|
|
208
|
+
return String(result);
|
|
209
|
+
}
|
|
210
|
+
const r = parsed.data;
|
|
211
|
+
if (r.content !== void 0) {
|
|
212
|
+
const texts = r.content.map((block) => textBlockSchema.safeParse(block)).filter((res) => res.success).map((res) => res.data.text);
|
|
213
|
+
if (texts.length > 0) return texts.join("");
|
|
214
|
+
}
|
|
215
|
+
const d = r.details;
|
|
216
|
+
const diff = d?.diff;
|
|
217
|
+
if (diff !== void 0 && diff.trim() !== "") return diff;
|
|
218
|
+
const stdout = d?.stdout ?? r.stdout ?? d?.output ?? r.output;
|
|
219
|
+
const stderr = d?.stderr ?? r.stderr;
|
|
220
|
+
const exitCode = d?.exitCode ?? r.exitCode ?? d?.code ?? r.code;
|
|
221
|
+
const hasStdout = stdout !== void 0 && stdout.trim() !== "";
|
|
222
|
+
const hasStderr = stderr !== void 0 && stderr.trim() !== "";
|
|
223
|
+
if (hasStdout || hasStderr) {
|
|
224
|
+
const parts = [];
|
|
225
|
+
if (hasStdout) parts.push(stdout);
|
|
226
|
+
if (hasStderr) parts.push(`stderr:\n${stderr}`);
|
|
227
|
+
if (exitCode !== void 0) parts.push(`exit code: ${exitCode}`);
|
|
228
|
+
return parts.join("\n\n").trimEnd();
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
return JSON.stringify(result, null, 2);
|
|
232
|
+
} catch {
|
|
233
|
+
return String(result);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/acp/session.ts
|
|
238
|
+
function findUniqueLineNumber(text, needle) {
|
|
239
|
+
if (!needle) return void 0;
|
|
240
|
+
const first = text.indexOf(needle);
|
|
241
|
+
if (first < 0) return void 0;
|
|
242
|
+
if (text.indexOf(needle, first + needle.length) >= 0) return void 0;
|
|
243
|
+
let line = 1;
|
|
244
|
+
for (let i = 0; i < first; i++) if (text.charCodeAt(i) === 10) line++;
|
|
245
|
+
return line;
|
|
246
|
+
}
|
|
247
|
+
function resolveToolPath(args, cwd, line) {
|
|
248
|
+
const p = args.path;
|
|
249
|
+
if (p === void 0) return void 0;
|
|
250
|
+
return [{
|
|
251
|
+
path: isAbsolute(p) ? p : resolve(cwd, p),
|
|
252
|
+
...typeof line === "number" ? { line } : {}
|
|
253
|
+
}];
|
|
254
|
+
}
|
|
255
|
+
function toToolKind(toolName) {
|
|
256
|
+
switch (toolName) {
|
|
257
|
+
case "read": return "read";
|
|
258
|
+
case "write":
|
|
259
|
+
case "edit": return "edit";
|
|
260
|
+
case "bash": return "execute";
|
|
261
|
+
default: return "other";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Map pi assistant stopReason to ACP StopReason.
|
|
266
|
+
* pi: "stop" | "length" | "toolUse" | "error" | "aborted"
|
|
267
|
+
* ACP: "end_turn" | "cancelled" | "max_tokens" | "error"
|
|
268
|
+
*/
|
|
269
|
+
function mapPiStopReason(piReason) {
|
|
270
|
+
switch (piReason) {
|
|
271
|
+
case "stop":
|
|
272
|
+
case "toolUse": return "end_turn";
|
|
273
|
+
case "length": return "max_tokens";
|
|
274
|
+
case "aborted": return "cancelled";
|
|
275
|
+
case "error": return "error";
|
|
276
|
+
default: return "end_turn";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function extractToolCallFromPartial(ame) {
|
|
280
|
+
if (!("partial" in ame)) return void 0;
|
|
281
|
+
const block = ame.partial.content["contentIndex" in ame ? ame.contentIndex : 0];
|
|
282
|
+
if (block && "type" in block && block.type === "toolCall") return block;
|
|
283
|
+
}
|
|
284
|
+
function parseToolInput(tc) {
|
|
285
|
+
return tc.arguments;
|
|
286
|
+
}
|
|
287
|
+
const toolArgsSchema = z.object({
|
|
288
|
+
path: z.string().optional(),
|
|
289
|
+
oldText: z.string().optional()
|
|
290
|
+
}).loose();
|
|
291
|
+
function toToolArgs(raw) {
|
|
292
|
+
const result = toolArgsSchema.safeParse(raw);
|
|
293
|
+
return result.success ? result.data : {};
|
|
294
|
+
}
|
|
295
|
+
var SessionManager$1 = class {
|
|
296
|
+
sessions = /* @__PURE__ */ new Map();
|
|
297
|
+
disposeAll() {
|
|
298
|
+
for (const id of this.sessions.keys()) this.close(id);
|
|
299
|
+
}
|
|
300
|
+
maybeGet(sessionId) {
|
|
301
|
+
return this.sessions.get(sessionId);
|
|
302
|
+
}
|
|
303
|
+
close(sessionId) {
|
|
304
|
+
const s = this.sessions.get(sessionId);
|
|
305
|
+
if (!s) return;
|
|
306
|
+
try {
|
|
307
|
+
s.dispose();
|
|
308
|
+
} catch {}
|
|
309
|
+
this.sessions.delete(sessionId);
|
|
310
|
+
}
|
|
311
|
+
closeAllExcept(keepSessionId) {
|
|
312
|
+
for (const id of this.sessions.keys()) if (id !== keepSessionId) this.close(id);
|
|
313
|
+
}
|
|
314
|
+
register(session) {
|
|
315
|
+
this.sessions.set(session.sessionId, session);
|
|
316
|
+
}
|
|
317
|
+
get(sessionId) {
|
|
318
|
+
const s = this.sessions.get(sessionId);
|
|
319
|
+
if (!s) throw RequestError.invalidParams(`Unknown sessionId: ${sessionId}`);
|
|
320
|
+
return s;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
var PiAcpSession = class {
|
|
324
|
+
sessionId;
|
|
325
|
+
cwd;
|
|
326
|
+
mcpServers;
|
|
327
|
+
piSession;
|
|
328
|
+
startupInfo = null;
|
|
329
|
+
startupInfoSent = false;
|
|
330
|
+
conn;
|
|
331
|
+
cancelRequested = false;
|
|
332
|
+
pendingTurn = null;
|
|
333
|
+
currentToolCalls = /* @__PURE__ */ new Map();
|
|
334
|
+
editSnapshots = /* @__PURE__ */ new Map();
|
|
335
|
+
lastAssistantStopReason = null;
|
|
336
|
+
lastEmit = Promise.resolve();
|
|
337
|
+
unsubscribe;
|
|
338
|
+
constructor(opts) {
|
|
339
|
+
this.sessionId = opts.sessionId;
|
|
340
|
+
this.cwd = opts.cwd;
|
|
341
|
+
this.mcpServers = opts.mcpServers;
|
|
342
|
+
this.piSession = opts.piSession;
|
|
343
|
+
this.conn = opts.conn;
|
|
344
|
+
this.unsubscribe = this.piSession.subscribe((ev) => this.handlePiEvent(ev));
|
|
345
|
+
}
|
|
346
|
+
dispose() {
|
|
347
|
+
this.unsubscribe?.();
|
|
348
|
+
this.piSession.dispose();
|
|
349
|
+
}
|
|
350
|
+
setStartupInfo(text) {
|
|
351
|
+
this.startupInfo = text;
|
|
352
|
+
}
|
|
353
|
+
sendStartupInfoIfPending() {
|
|
354
|
+
if (this.startupInfoSent || this.startupInfo === null) return;
|
|
355
|
+
this.startupInfoSent = true;
|
|
356
|
+
this.emit({
|
|
357
|
+
sessionUpdate: "agent_message_chunk",
|
|
358
|
+
content: {
|
|
359
|
+
type: "text",
|
|
360
|
+
text: this.startupInfo
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
async prompt(message, images = []) {
|
|
365
|
+
const turnPromise = new Promise((resolve, reject) => {
|
|
366
|
+
this.cancelRequested = false;
|
|
367
|
+
this.pendingTurn = {
|
|
368
|
+
resolve,
|
|
369
|
+
reject
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
const imageContents = Array.isArray(images) ? images.filter((img) => typeof img === "object" && img !== null && "type" in img && img.type === "image") : [];
|
|
373
|
+
this.piSession.prompt(message, { images: imageContents }).catch(() => {
|
|
374
|
+
this.flushEmits().finally(() => {
|
|
375
|
+
const reason = this.cancelRequested ? "cancelled" : "error";
|
|
376
|
+
this.pendingTurn?.resolve(reason);
|
|
377
|
+
this.pendingTurn = null;
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
return turnPromise;
|
|
381
|
+
}
|
|
382
|
+
async cancel() {
|
|
383
|
+
this.cancelRequested = true;
|
|
384
|
+
await this.piSession.abort();
|
|
385
|
+
}
|
|
386
|
+
wasCancelRequested() {
|
|
387
|
+
return this.cancelRequested;
|
|
388
|
+
}
|
|
389
|
+
emit(update) {
|
|
390
|
+
this.lastEmit = this.lastEmit.then(() => this.conn.sessionUpdate({
|
|
391
|
+
sessionId: this.sessionId,
|
|
392
|
+
update
|
|
393
|
+
})).catch(() => {});
|
|
394
|
+
}
|
|
395
|
+
async flushEmits() {
|
|
396
|
+
await this.lastEmit;
|
|
397
|
+
}
|
|
398
|
+
handlePiEvent(ev) {
|
|
399
|
+
if (!isAgentEvent(ev)) return;
|
|
400
|
+
switch (ev.type) {
|
|
401
|
+
case "message_update":
|
|
402
|
+
this.handleMessageUpdate(ev.assistantMessageEvent);
|
|
403
|
+
break;
|
|
404
|
+
case "message_end":
|
|
405
|
+
this.handleMessageEnd(ev.message);
|
|
406
|
+
break;
|
|
407
|
+
case "tool_execution_start":
|
|
408
|
+
this.handleToolStart(ev.toolCallId, ev.toolName, toToolArgs(ev.args));
|
|
409
|
+
break;
|
|
410
|
+
case "tool_execution_update":
|
|
411
|
+
this.handleToolUpdate(ev.toolCallId, ev.partialResult);
|
|
412
|
+
break;
|
|
413
|
+
case "tool_execution_end":
|
|
414
|
+
this.handleToolEnd(ev.toolCallId, ev.result, ev.isError);
|
|
415
|
+
break;
|
|
416
|
+
case "agent_end":
|
|
417
|
+
this.handleAgentEnd();
|
|
418
|
+
break;
|
|
419
|
+
default: break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
handleMessageUpdate(ame) {
|
|
423
|
+
if (ame.type === "text_delta") {
|
|
424
|
+
this.emit({
|
|
425
|
+
sessionUpdate: "agent_message_chunk",
|
|
426
|
+
content: {
|
|
427
|
+
type: "text",
|
|
428
|
+
text: ame.delta
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (ame.type === "thinking_delta") {
|
|
434
|
+
this.emit({
|
|
435
|
+
sessionUpdate: "agent_thought_chunk",
|
|
436
|
+
content: {
|
|
437
|
+
type: "text",
|
|
438
|
+
text: ame.delta
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (ame.type === "toolcall_start" || ame.type === "toolcall_delta" || ame.type === "toolcall_end") {
|
|
444
|
+
const toolCall = ame.type === "toolcall_end" ? ame.toolCall : extractToolCallFromPartial(ame);
|
|
445
|
+
if (!toolCall) return;
|
|
446
|
+
const rawInput = parseToolInput(toolCall);
|
|
447
|
+
const locations = resolveToolPath(rawInput, this.cwd);
|
|
448
|
+
const existingStatus = this.currentToolCalls.get(toolCall.id);
|
|
449
|
+
const status = existingStatus ?? "pending";
|
|
450
|
+
if (!existingStatus) {
|
|
451
|
+
this.currentToolCalls.set(toolCall.id, "pending");
|
|
452
|
+
this.emit({
|
|
453
|
+
sessionUpdate: "tool_call",
|
|
454
|
+
toolCallId: toolCall.id,
|
|
455
|
+
title: toolCall.name,
|
|
456
|
+
kind: toToolKind(toolCall.name),
|
|
457
|
+
status,
|
|
458
|
+
...locations ? { locations } : {},
|
|
459
|
+
rawInput
|
|
460
|
+
});
|
|
461
|
+
} else this.emit({
|
|
462
|
+
sessionUpdate: "tool_call_update",
|
|
463
|
+
toolCallId: toolCall.id,
|
|
464
|
+
status,
|
|
465
|
+
...locations ? { locations } : {},
|
|
466
|
+
rawInput
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
handleMessageEnd(msg) {
|
|
471
|
+
if ("role" in msg && msg.role === "assistant") this.lastAssistantStopReason = msg.stopReason;
|
|
472
|
+
}
|
|
473
|
+
handleToolStart(toolCallId, toolName, args) {
|
|
474
|
+
let line;
|
|
475
|
+
if (toolName === "edit" && args.path !== void 0) try {
|
|
476
|
+
const abs = isAbsolute(args.path) ? args.path : resolve(this.cwd, args.path);
|
|
477
|
+
const oldText = readFileSync(abs, "utf8");
|
|
478
|
+
this.editSnapshots.set(toolCallId, {
|
|
479
|
+
path: abs,
|
|
480
|
+
oldText
|
|
481
|
+
});
|
|
482
|
+
line = findUniqueLineNumber(oldText, args.oldText ?? "");
|
|
483
|
+
} catch {}
|
|
484
|
+
const locations = resolveToolPath(args, this.cwd, line);
|
|
485
|
+
if (!this.currentToolCalls.has(toolCallId)) {
|
|
486
|
+
this.currentToolCalls.set(toolCallId, "in_progress");
|
|
487
|
+
this.emit({
|
|
488
|
+
sessionUpdate: "tool_call",
|
|
489
|
+
toolCallId,
|
|
490
|
+
title: toolName,
|
|
491
|
+
kind: toToolKind(toolName),
|
|
492
|
+
status: "in_progress",
|
|
493
|
+
...locations ? { locations } : {},
|
|
494
|
+
rawInput: args
|
|
495
|
+
});
|
|
496
|
+
} else {
|
|
497
|
+
this.currentToolCalls.set(toolCallId, "in_progress");
|
|
498
|
+
this.emit({
|
|
499
|
+
sessionUpdate: "tool_call_update",
|
|
500
|
+
toolCallId,
|
|
501
|
+
status: "in_progress",
|
|
502
|
+
...locations ? { locations } : {},
|
|
503
|
+
rawInput: args
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
handleToolUpdate(toolCallId, partialResult) {
|
|
508
|
+
const text = toolResultToText(partialResult);
|
|
509
|
+
this.emit({
|
|
510
|
+
sessionUpdate: "tool_call_update",
|
|
511
|
+
toolCallId,
|
|
512
|
+
status: "in_progress",
|
|
513
|
+
content: text ? [{
|
|
514
|
+
type: "content",
|
|
515
|
+
content: {
|
|
516
|
+
type: "text",
|
|
517
|
+
text
|
|
518
|
+
}
|
|
519
|
+
}] : null,
|
|
520
|
+
rawOutput: partialResult
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
handleToolEnd(toolCallId, result, isError) {
|
|
524
|
+
const text = toolResultToText(result);
|
|
525
|
+
const snapshot = this.editSnapshots.get(toolCallId);
|
|
526
|
+
let content = null;
|
|
527
|
+
if (!isError && snapshot) try {
|
|
528
|
+
const newText = readFileSync(snapshot.path, "utf8");
|
|
529
|
+
if (newText !== snapshot.oldText) content = [{
|
|
530
|
+
type: "diff",
|
|
531
|
+
path: snapshot.path,
|
|
532
|
+
oldText: snapshot.oldText,
|
|
533
|
+
newText
|
|
534
|
+
}, ...text ? [{
|
|
535
|
+
type: "content",
|
|
536
|
+
content: {
|
|
537
|
+
type: "text",
|
|
538
|
+
text
|
|
539
|
+
}
|
|
540
|
+
}] : []];
|
|
541
|
+
} catch {}
|
|
542
|
+
if (!content && text) content = [{
|
|
543
|
+
type: "content",
|
|
544
|
+
content: {
|
|
545
|
+
type: "text",
|
|
546
|
+
text
|
|
547
|
+
}
|
|
548
|
+
}];
|
|
549
|
+
this.emit({
|
|
550
|
+
sessionUpdate: "tool_call_update",
|
|
551
|
+
toolCallId,
|
|
552
|
+
status: isError ? "failed" : "completed",
|
|
553
|
+
content,
|
|
554
|
+
rawOutput: result
|
|
555
|
+
});
|
|
556
|
+
this.currentToolCalls.delete(toolCallId);
|
|
557
|
+
this.editSnapshots.delete(toolCallId);
|
|
558
|
+
}
|
|
559
|
+
handleAgentEnd() {
|
|
560
|
+
this.flushEmits().finally(() => {
|
|
561
|
+
const reason = this.cancelRequested ? "cancelled" : mapPiStopReason(this.lastAssistantStopReason);
|
|
562
|
+
this.lastAssistantStopReason = null;
|
|
563
|
+
this.pendingTurn?.resolve(reason);
|
|
564
|
+
this.pendingTurn = null;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* Type guard to narrow AgentSessionEvent to the AgentEvent subset
|
|
570
|
+
* (the variants we handle). Session-specific events like auto_compaction
|
|
571
|
+
* are ignored.
|
|
572
|
+
*/
|
|
573
|
+
function isAgentEvent(ev) {
|
|
574
|
+
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";
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/acp/translate/pi-messages.ts
|
|
578
|
+
function isTextBlock(block) {
|
|
579
|
+
if (typeof block !== "object" || block === null) return false;
|
|
580
|
+
return "type" in block && block.type === "text" && "text" in block && typeof block.text === "string";
|
|
581
|
+
}
|
|
582
|
+
function extractUserMessageText(content) {
|
|
583
|
+
if (typeof content === "string") return content;
|
|
584
|
+
if (!Array.isArray(content)) return "";
|
|
585
|
+
return content.filter(isTextBlock).map((b) => b.text).join("");
|
|
586
|
+
}
|
|
587
|
+
function extractAssistantText(content) {
|
|
588
|
+
if (!Array.isArray(content)) return "";
|
|
589
|
+
return content.filter(isTextBlock).map((b) => b.text).join("");
|
|
590
|
+
}
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/acp/translate/prompt.ts
|
|
593
|
+
function acpPromptToPiMessage(blocks) {
|
|
594
|
+
let message = "";
|
|
595
|
+
const images = [];
|
|
596
|
+
for (const block of blocks) switch (block.type) {
|
|
597
|
+
case "text":
|
|
598
|
+
message += block.text;
|
|
599
|
+
break;
|
|
600
|
+
case "resource_link":
|
|
601
|
+
message += `\n[Context] ${block.uri}`;
|
|
602
|
+
break;
|
|
603
|
+
case "image":
|
|
604
|
+
images.push({
|
|
605
|
+
type: "image",
|
|
606
|
+
mimeType: block.mimeType,
|
|
607
|
+
data: block.data
|
|
608
|
+
});
|
|
609
|
+
break;
|
|
610
|
+
case "resource": {
|
|
611
|
+
const resource = block.resource;
|
|
612
|
+
const uri = resource.uri;
|
|
613
|
+
const mime = resource.mimeType ?? null;
|
|
614
|
+
if ("text" in resource) message += `\n[Embedded Context] ${uri} (${mime ?? "text/plain"})\n${resource.text}`;
|
|
615
|
+
else if ("blob" in resource) {
|
|
616
|
+
const bytes = Buffer.byteLength(resource.blob, "base64");
|
|
617
|
+
message += `\n[Embedded Context] ${uri} (${mime ?? "application/octet-stream"}, ${bytes} bytes)`;
|
|
618
|
+
} else message += `\n[Embedded Context] ${uri}`;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
case "audio": {
|
|
622
|
+
const bytes = Buffer.byteLength(block.data, "base64");
|
|
623
|
+
message += `\n[Audio] (${block.mimeType}, ${bytes} bytes) not supported`;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
default: break;
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
message,
|
|
630
|
+
images
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/acp/agent.ts
|
|
635
|
+
function builtinAvailableCommands() {
|
|
636
|
+
return [
|
|
637
|
+
{
|
|
638
|
+
name: "compact",
|
|
639
|
+
description: "Manually compact the session context",
|
|
640
|
+
input: { hint: "optional custom instructions" }
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "autocompact",
|
|
644
|
+
description: "Toggle automatic context compaction",
|
|
645
|
+
input: { hint: "on|off|toggle" }
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: "export",
|
|
649
|
+
description: "Export session to an HTML file in the session cwd"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: "session",
|
|
653
|
+
description: "Show session stats (messages, tokens, cost, session file)"
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: "name",
|
|
657
|
+
description: "Set session display name",
|
|
658
|
+
input: { hint: "<name>" }
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: "steering",
|
|
662
|
+
description: "Get/set pi steering message delivery mode",
|
|
663
|
+
input: { hint: "(no args to show) all | one-at-a-time" }
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "follow-up",
|
|
667
|
+
description: "Get/set pi follow-up message delivery mode",
|
|
668
|
+
input: { hint: "(no args to show) all | one-at-a-time" }
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "changelog",
|
|
672
|
+
description: "Show pi changelog"
|
|
673
|
+
}
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
function mergeCommands(a, b) {
|
|
677
|
+
const out = [];
|
|
678
|
+
const seen = /* @__PURE__ */ new Set();
|
|
679
|
+
for (const c of [...a, ...b]) {
|
|
680
|
+
if (seen.has(c.name)) continue;
|
|
681
|
+
seen.add(c.name);
|
|
682
|
+
out.push(c);
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
function parseArgs(input) {
|
|
687
|
+
const args = [];
|
|
688
|
+
let current = "";
|
|
689
|
+
let quote = null;
|
|
690
|
+
for (const ch of input) if (quote !== null) if (ch === quote) quote = null;
|
|
691
|
+
else current += ch;
|
|
692
|
+
else if (ch === "\"" || ch === "'") quote = ch;
|
|
693
|
+
else if (ch === " " || ch === " ") {
|
|
694
|
+
if (current !== "") {
|
|
695
|
+
args.push(current);
|
|
696
|
+
current = "";
|
|
697
|
+
}
|
|
698
|
+
} else current += ch;
|
|
699
|
+
if (current !== "") args.push(current);
|
|
700
|
+
return args;
|
|
701
|
+
}
|
|
702
|
+
const pkg = readNearestPackageJson(import.meta.url);
|
|
703
|
+
var PiAcpAgent = class {
|
|
704
|
+
conn;
|
|
705
|
+
sessions = new SessionManager$1();
|
|
706
|
+
dispose() {
|
|
707
|
+
this.sessions.disposeAll();
|
|
708
|
+
}
|
|
709
|
+
constructor(conn, _config) {
|
|
710
|
+
this.conn = conn;
|
|
711
|
+
}
|
|
712
|
+
async initialize(params) {
|
|
713
|
+
const supportedVersion = 1;
|
|
714
|
+
const requested = params.protocolVersion;
|
|
715
|
+
return {
|
|
716
|
+
protocolVersion: requested === supportedVersion ? requested : supportedVersion,
|
|
717
|
+
agentInfo: {
|
|
718
|
+
name: pkg.name,
|
|
719
|
+
title: "pi ACP adapter",
|
|
720
|
+
version: pkg.version
|
|
721
|
+
},
|
|
722
|
+
authMethods: buildAuthMethods({ supportsTerminalAuthMeta: params.clientCapabilities?._meta?.["terminal-auth"] === true }),
|
|
723
|
+
agentCapabilities: {
|
|
724
|
+
loadSession: true,
|
|
725
|
+
mcpCapabilities: {
|
|
726
|
+
http: false,
|
|
727
|
+
sse: false
|
|
728
|
+
},
|
|
729
|
+
promptCapabilities: {
|
|
730
|
+
image: true,
|
|
731
|
+
audio: false,
|
|
732
|
+
embeddedContext: false
|
|
733
|
+
},
|
|
734
|
+
sessionCapabilities: { list: {} }
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
async newSession(params) {
|
|
739
|
+
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
740
|
+
if (!hasPiAuthConfigured()) throw RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
|
|
741
|
+
let result;
|
|
742
|
+
try {
|
|
743
|
+
result = await createAgentSession({ cwd: params.cwd });
|
|
744
|
+
} catch (e) {
|
|
745
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
746
|
+
throw RequestError.internalError({}, `Failed to create pi session: ${msg}`);
|
|
747
|
+
}
|
|
748
|
+
const piSession = result.session;
|
|
749
|
+
if (piSession.modelRegistry.getAvailable().length === 0) {
|
|
750
|
+
piSession.dispose();
|
|
751
|
+
throw RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
|
|
752
|
+
}
|
|
753
|
+
const session = new PiAcpSession({
|
|
754
|
+
sessionId: piSession.sessionManager.getSessionId(),
|
|
755
|
+
cwd: params.cwd,
|
|
756
|
+
mcpServers: params.mcpServers,
|
|
757
|
+
piSession,
|
|
758
|
+
conn: this.conn
|
|
759
|
+
});
|
|
760
|
+
this.sessions.register(session);
|
|
761
|
+
const quietStartup = quietStartupEnabled(params.cwd);
|
|
762
|
+
const updateNotice = buildUpdateNotice();
|
|
763
|
+
const preludeText = quietStartup ? updateNotice !== null ? `${updateNotice}\n` : "" : buildStartupInfo({
|
|
764
|
+
cwd: params.cwd,
|
|
765
|
+
updateNotice
|
|
766
|
+
});
|
|
767
|
+
if (preludeText) session.setStartupInfo(preludeText);
|
|
768
|
+
this.sessions.closeAllExcept(session.sessionId);
|
|
769
|
+
const modes = buildThinkingModes(piSession);
|
|
770
|
+
const models = buildModelState(piSession);
|
|
771
|
+
const configOptions = buildConfigOptions(modes, models);
|
|
772
|
+
const response = {
|
|
773
|
+
sessionId: session.sessionId,
|
|
774
|
+
configOptions,
|
|
775
|
+
modes,
|
|
776
|
+
models,
|
|
777
|
+
_meta: { piAcp: { startupInfo: preludeText || null } }
|
|
778
|
+
};
|
|
779
|
+
if (preludeText) setTimeout(() => session.sendStartupInfoIfPending(), 0);
|
|
780
|
+
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
781
|
+
setTimeout(() => {
|
|
782
|
+
(async () => {
|
|
783
|
+
try {
|
|
784
|
+
const commands = buildCommandList(piSession, enableSkillCommands);
|
|
785
|
+
await this.conn.sessionUpdate({
|
|
786
|
+
sessionId: session.sessionId,
|
|
787
|
+
update: {
|
|
788
|
+
sessionUpdate: "available_commands_update",
|
|
789
|
+
availableCommands: mergeCommands(commands, builtinAvailableCommands())
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
} catch {}
|
|
793
|
+
})();
|
|
794
|
+
}, 0);
|
|
795
|
+
return response;
|
|
796
|
+
}
|
|
797
|
+
async authenticate(_params) {}
|
|
798
|
+
async prompt(params) {
|
|
799
|
+
const session = this.sessions.get(params.sessionId);
|
|
800
|
+
const { message, images } = acpPromptToPiMessage(params.prompt);
|
|
801
|
+
if (images.length === 0 && message.trimStart().startsWith("/")) {
|
|
802
|
+
const trimmed = message.trim();
|
|
803
|
+
const space = trimmed.indexOf(" ");
|
|
804
|
+
const cmd = space === -1 ? trimmed.slice(1) : trimmed.slice(1, space);
|
|
805
|
+
const args = parseArgs(space === -1 ? "" : trimmed.slice(space + 1));
|
|
806
|
+
const handled = await this.handleBuiltinCommand(session, cmd, args);
|
|
807
|
+
if (handled) return handled;
|
|
808
|
+
}
|
|
809
|
+
const result = await session.prompt(message, images);
|
|
810
|
+
return { stopReason: result === "error" ? "end_turn" : result };
|
|
811
|
+
}
|
|
812
|
+
async cancel(params) {
|
|
813
|
+
await this.sessions.get(params.sessionId).cancel();
|
|
814
|
+
}
|
|
815
|
+
async listSessions(params) {
|
|
816
|
+
const cwd = params.cwd;
|
|
817
|
+
const sessions = (cwd !== void 0 && cwd !== null ? await SessionManager.list(cwd) : await SessionManager.listAll()).map((s) => ({
|
|
818
|
+
id: s.id,
|
|
819
|
+
cwd: s.cwd,
|
|
820
|
+
name: s.name ?? "",
|
|
821
|
+
modified: s.modified,
|
|
822
|
+
messageCount: s.messageCount
|
|
823
|
+
}));
|
|
824
|
+
if (params.cursor !== void 0 && params.cursor !== null) {
|
|
825
|
+
const parsed = Number.parseInt(params.cursor, 10);
|
|
826
|
+
if (!Number.isFinite(parsed) || parsed < 0) throw RequestError.invalidParams(`Invalid cursor: ${params.cursor}`);
|
|
827
|
+
}
|
|
828
|
+
const start = params.cursor !== void 0 && params.cursor !== null ? Number.parseInt(params.cursor, 10) : 0;
|
|
829
|
+
const PAGE_SIZE = 50;
|
|
830
|
+
return {
|
|
831
|
+
sessions: sessions.slice(start, start + PAGE_SIZE).map((s) => ({
|
|
832
|
+
sessionId: s.id,
|
|
833
|
+
cwd: s.cwd,
|
|
834
|
+
title: s.name ?? null,
|
|
835
|
+
updatedAt: s.modified.toISOString()
|
|
836
|
+
})),
|
|
837
|
+
nextCursor: start + PAGE_SIZE < sessions.length ? String(start + PAGE_SIZE) : null,
|
|
838
|
+
_meta: {}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async loadSession(params) {
|
|
842
|
+
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
843
|
+
this.sessions.close(params.sessionId);
|
|
844
|
+
const sessionFile = findPiSessionFile(params.sessionId);
|
|
845
|
+
if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
846
|
+
let result;
|
|
847
|
+
try {
|
|
848
|
+
const sm = SessionManager.open(sessionFile);
|
|
849
|
+
result = await createAgentSession({
|
|
850
|
+
cwd: params.cwd,
|
|
851
|
+
sessionManager: sm
|
|
852
|
+
});
|
|
853
|
+
} catch (e) {
|
|
854
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
855
|
+
throw RequestError.internalError({}, `Failed to load pi session: ${msg}`);
|
|
856
|
+
}
|
|
857
|
+
const piSession = result.session;
|
|
858
|
+
const session = new PiAcpSession({
|
|
859
|
+
sessionId: params.sessionId,
|
|
860
|
+
cwd: params.cwd,
|
|
861
|
+
mcpServers: params.mcpServers,
|
|
862
|
+
piSession,
|
|
863
|
+
conn: this.conn
|
|
864
|
+
});
|
|
865
|
+
this.sessions.register(session);
|
|
866
|
+
this.sessions.closeAllExcept(session.sessionId);
|
|
867
|
+
const messages = piSession.messages;
|
|
868
|
+
for (const m of messages) {
|
|
869
|
+
if (!("role" in m)) continue;
|
|
870
|
+
if (m.role === "user") {
|
|
871
|
+
const text = extractUserMessageText(m.content);
|
|
872
|
+
if (text) await this.conn.sessionUpdate({
|
|
873
|
+
sessionId: session.sessionId,
|
|
874
|
+
update: {
|
|
875
|
+
sessionUpdate: "user_message_chunk",
|
|
876
|
+
content: {
|
|
877
|
+
type: "text",
|
|
878
|
+
text
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
if (m.role === "assistant") {
|
|
884
|
+
const text = extractAssistantText(m.content);
|
|
885
|
+
if (text) await this.conn.sessionUpdate({
|
|
886
|
+
sessionId: session.sessionId,
|
|
887
|
+
update: {
|
|
888
|
+
sessionUpdate: "agent_message_chunk",
|
|
889
|
+
content: {
|
|
890
|
+
type: "text",
|
|
891
|
+
text
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
if (m.role === "toolResult") {
|
|
897
|
+
const tr = m;
|
|
898
|
+
const toolName = tr.toolName;
|
|
899
|
+
const toolCallId = tr.toolCallId;
|
|
900
|
+
const isError = tr.isError;
|
|
901
|
+
await this.conn.sessionUpdate({
|
|
902
|
+
sessionId: session.sessionId,
|
|
903
|
+
update: {
|
|
904
|
+
sessionUpdate: "tool_call",
|
|
905
|
+
toolCallId,
|
|
906
|
+
title: toolName,
|
|
907
|
+
kind: toolName === "read" ? "read" : toolName === "write" || toolName === "edit" ? "edit" : "other",
|
|
908
|
+
status: "completed",
|
|
909
|
+
rawInput: null,
|
|
910
|
+
rawOutput: m
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
const text = toolResultToText(m);
|
|
914
|
+
await this.conn.sessionUpdate({
|
|
915
|
+
sessionId: session.sessionId,
|
|
916
|
+
update: {
|
|
917
|
+
sessionUpdate: "tool_call_update",
|
|
918
|
+
toolCallId,
|
|
919
|
+
status: isError ? "failed" : "completed",
|
|
920
|
+
content: text ? [{
|
|
921
|
+
type: "content",
|
|
922
|
+
content: {
|
|
923
|
+
type: "text",
|
|
924
|
+
text
|
|
925
|
+
}
|
|
926
|
+
}] : null,
|
|
927
|
+
rawOutput: m
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const modes = buildThinkingModes(piSession);
|
|
933
|
+
const models = buildModelState(piSession);
|
|
934
|
+
const configOptions = buildConfigOptions(modes, models);
|
|
935
|
+
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
936
|
+
setTimeout(() => {
|
|
937
|
+
(async () => {
|
|
938
|
+
try {
|
|
939
|
+
const commands = buildCommandList(piSession, enableSkillCommands);
|
|
940
|
+
await this.conn.sessionUpdate({
|
|
941
|
+
sessionId: session.sessionId,
|
|
942
|
+
update: {
|
|
943
|
+
sessionUpdate: "available_commands_update",
|
|
944
|
+
availableCommands: mergeCommands(commands, builtinAvailableCommands())
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
} catch {}
|
|
948
|
+
})();
|
|
949
|
+
}, 0);
|
|
950
|
+
return {
|
|
951
|
+
configOptions,
|
|
952
|
+
modes,
|
|
953
|
+
models,
|
|
954
|
+
_meta: { piAcp: { startupInfo: null } }
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
async setSessionMode(params) {
|
|
958
|
+
const session = this.sessions.get(params.sessionId);
|
|
959
|
+
const mode = String(params.modeId);
|
|
960
|
+
if (!isThinkingLevel(mode)) throw RequestError.invalidParams(`Unknown modeId: ${mode}`);
|
|
961
|
+
session.piSession.setThinkingLevel(mode);
|
|
962
|
+
this.conn.sessionUpdate({
|
|
963
|
+
sessionId: session.sessionId,
|
|
964
|
+
update: {
|
|
965
|
+
sessionUpdate: "current_mode_update",
|
|
966
|
+
currentModeId: mode
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
this.emitConfigOptionUpdate(session);
|
|
970
|
+
return {};
|
|
971
|
+
}
|
|
972
|
+
async unstable_setSessionModel(params) {
|
|
973
|
+
const session = this.sessions.get(params.sessionId);
|
|
974
|
+
let provider = null;
|
|
975
|
+
let modelId = null;
|
|
976
|
+
if (params.modelId.includes("/")) {
|
|
977
|
+
const [p, ...rest] = params.modelId.split("/");
|
|
978
|
+
provider = p ?? null;
|
|
979
|
+
modelId = rest.join("/");
|
|
980
|
+
} else modelId = params.modelId;
|
|
981
|
+
if (provider === null) {
|
|
982
|
+
const found = session.piSession.modelRegistry.getAvailable().find((m) => m.id === modelId);
|
|
983
|
+
if (found) {
|
|
984
|
+
provider = found.provider;
|
|
985
|
+
modelId = found.id;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (provider === null || modelId === null) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
|
|
989
|
+
const model = session.piSession.modelRegistry.getAvailable().find((m) => m.provider === provider && m.id === modelId);
|
|
990
|
+
if (!model) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
|
|
991
|
+
await session.piSession.setModel(model);
|
|
992
|
+
this.emitConfigOptionUpdate(session);
|
|
993
|
+
}
|
|
994
|
+
async setSessionConfigOption(params) {
|
|
995
|
+
const session = this.sessions.get(params.sessionId);
|
|
996
|
+
const configId = String(params.configId);
|
|
997
|
+
const value = String(params.value);
|
|
998
|
+
if (configId === "model") {
|
|
999
|
+
let provider = null;
|
|
1000
|
+
let modelId = null;
|
|
1001
|
+
if (value.includes("/")) {
|
|
1002
|
+
const [p, ...rest] = value.split("/");
|
|
1003
|
+
provider = p ?? null;
|
|
1004
|
+
modelId = rest.join("/");
|
|
1005
|
+
} else modelId = value;
|
|
1006
|
+
if (provider === null) {
|
|
1007
|
+
const found = session.piSession.modelRegistry.getAvailable().find((m) => m.id === modelId);
|
|
1008
|
+
if (found) {
|
|
1009
|
+
provider = found.provider;
|
|
1010
|
+
modelId = found.id;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (provider === null || modelId === null) throw RequestError.invalidParams(`Unknown model: ${value}`);
|
|
1014
|
+
const model = session.piSession.modelRegistry.getAvailable().find((m) => m.provider === provider && m.id === modelId);
|
|
1015
|
+
if (!model) throw RequestError.invalidParams(`Unknown model: ${value}`);
|
|
1016
|
+
await session.piSession.setModel(model);
|
|
1017
|
+
} else if (configId === "thought_level") {
|
|
1018
|
+
if (!isThinkingLevel(value)) throw RequestError.invalidParams(`Unknown thinking level: ${value}`);
|
|
1019
|
+
session.piSession.setThinkingLevel(value);
|
|
1020
|
+
} else throw RequestError.invalidParams(`Unknown config option: ${configId}`);
|
|
1021
|
+
return { configOptions: buildConfigOptions(buildThinkingModes(session.piSession), buildModelState(session.piSession)) };
|
|
1022
|
+
}
|
|
1023
|
+
emitConfigOptionUpdate(session) {
|
|
1024
|
+
const configOptions = buildConfigOptions(buildThinkingModes(session.piSession), buildModelState(session.piSession));
|
|
1025
|
+
this.conn.sessionUpdate({
|
|
1026
|
+
sessionId: session.sessionId,
|
|
1027
|
+
update: {
|
|
1028
|
+
sessionUpdate: "config_option_update",
|
|
1029
|
+
configOptions
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
async handleBuiltinCommand(session, cmd, args) {
|
|
1034
|
+
const piSession = session.piSession;
|
|
1035
|
+
if (cmd === "compact") {
|
|
1036
|
+
const customInstructions = args.join(" ").trim() || void 0;
|
|
1037
|
+
const res = await piSession.compact(customInstructions);
|
|
1038
|
+
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}` : "");
|
|
1039
|
+
await this.conn.sessionUpdate({
|
|
1040
|
+
sessionId: session.sessionId,
|
|
1041
|
+
update: {
|
|
1042
|
+
sessionUpdate: "agent_message_chunk",
|
|
1043
|
+
content: {
|
|
1044
|
+
type: "text",
|
|
1045
|
+
text
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
return { stopReason: "end_turn" };
|
|
1050
|
+
}
|
|
1051
|
+
if (cmd === "session") {
|
|
1052
|
+
const stats = piSession.getSessionStats();
|
|
1053
|
+
const lines = [];
|
|
1054
|
+
if (stats.sessionId !== void 0 && stats.sessionId !== "") lines.push(`Session: ${stats.sessionId}`);
|
|
1055
|
+
if (stats.sessionFile !== void 0 && stats.sessionFile !== "") lines.push(`Session file: ${stats.sessionFile}`);
|
|
1056
|
+
lines.push(`Messages: ${stats.totalMessages}`);
|
|
1057
|
+
lines.push(`Cost: ${stats.cost}`);
|
|
1058
|
+
const t = stats.tokens;
|
|
1059
|
+
const parts = [];
|
|
1060
|
+
if (t.input) parts.push(`in ${t.input}`);
|
|
1061
|
+
if (t.output) parts.push(`out ${t.output}`);
|
|
1062
|
+
if (t.cacheRead) parts.push(`cache read ${t.cacheRead}`);
|
|
1063
|
+
if (t.cacheWrite) parts.push(`cache write ${t.cacheWrite}`);
|
|
1064
|
+
if (t.total) parts.push(`total ${t.total}`);
|
|
1065
|
+
if (parts.length > 0) lines.push(`Tokens: ${parts.join(", ")}`);
|
|
1066
|
+
const text = lines.join("\n");
|
|
1067
|
+
await this.conn.sessionUpdate({
|
|
1068
|
+
sessionId: session.sessionId,
|
|
1069
|
+
update: {
|
|
1070
|
+
sessionUpdate: "agent_message_chunk",
|
|
1071
|
+
content: {
|
|
1072
|
+
type: "text",
|
|
1073
|
+
text
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
return { stopReason: "end_turn" };
|
|
1078
|
+
}
|
|
1079
|
+
if (cmd === "name") {
|
|
1080
|
+
const name = args.join(" ").trim();
|
|
1081
|
+
if (!name) {
|
|
1082
|
+
await this.conn.sessionUpdate({
|
|
1083
|
+
sessionId: session.sessionId,
|
|
1084
|
+
update: {
|
|
1085
|
+
sessionUpdate: "agent_message_chunk",
|
|
1086
|
+
content: {
|
|
1087
|
+
type: "text",
|
|
1088
|
+
text: "Usage: /name <name>"
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
return { stopReason: "end_turn" };
|
|
1093
|
+
}
|
|
1094
|
+
piSession.setSessionName(name);
|
|
1095
|
+
await this.conn.sessionUpdate({
|
|
1096
|
+
sessionId: session.sessionId,
|
|
1097
|
+
update: {
|
|
1098
|
+
sessionUpdate: "session_info_update",
|
|
1099
|
+
title: name,
|
|
1100
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
await this.conn.sessionUpdate({
|
|
1104
|
+
sessionId: session.sessionId,
|
|
1105
|
+
update: {
|
|
1106
|
+
sessionUpdate: "agent_message_chunk",
|
|
1107
|
+
content: {
|
|
1108
|
+
type: "text",
|
|
1109
|
+
text: `Session name set: ${name}`
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
return { stopReason: "end_turn" };
|
|
1114
|
+
}
|
|
1115
|
+
if (cmd === "steering") {
|
|
1116
|
+
const modeRaw = String(args[0] ?? "").toLowerCase();
|
|
1117
|
+
if (!modeRaw) {
|
|
1118
|
+
await this.conn.sessionUpdate({
|
|
1119
|
+
sessionId: session.sessionId,
|
|
1120
|
+
update: {
|
|
1121
|
+
sessionUpdate: "agent_message_chunk",
|
|
1122
|
+
content: {
|
|
1123
|
+
type: "text",
|
|
1124
|
+
text: `Steering mode: ${piSession.steeringMode}`
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
return { stopReason: "end_turn" };
|
|
1129
|
+
}
|
|
1130
|
+
if (modeRaw !== "all" && modeRaw !== "one-at-a-time") {
|
|
1131
|
+
await this.conn.sessionUpdate({
|
|
1132
|
+
sessionId: session.sessionId,
|
|
1133
|
+
update: {
|
|
1134
|
+
sessionUpdate: "agent_message_chunk",
|
|
1135
|
+
content: {
|
|
1136
|
+
type: "text",
|
|
1137
|
+
text: "Usage: /steering all | /steering one-at-a-time"
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
return { stopReason: "end_turn" };
|
|
1142
|
+
}
|
|
1143
|
+
piSession.setSteeringMode(modeRaw);
|
|
1144
|
+
await this.conn.sessionUpdate({
|
|
1145
|
+
sessionId: session.sessionId,
|
|
1146
|
+
update: {
|
|
1147
|
+
sessionUpdate: "agent_message_chunk",
|
|
1148
|
+
content: {
|
|
1149
|
+
type: "text",
|
|
1150
|
+
text: `Steering mode set to: ${modeRaw}`
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
return { stopReason: "end_turn" };
|
|
1155
|
+
}
|
|
1156
|
+
if (cmd === "follow-up") {
|
|
1157
|
+
const modeRaw = String(args[0] ?? "").toLowerCase();
|
|
1158
|
+
if (!modeRaw) {
|
|
1159
|
+
await this.conn.sessionUpdate({
|
|
1160
|
+
sessionId: session.sessionId,
|
|
1161
|
+
update: {
|
|
1162
|
+
sessionUpdate: "agent_message_chunk",
|
|
1163
|
+
content: {
|
|
1164
|
+
type: "text",
|
|
1165
|
+
text: `Follow-up mode: ${piSession.followUpMode}`
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
return { stopReason: "end_turn" };
|
|
1170
|
+
}
|
|
1171
|
+
if (modeRaw !== "all" && modeRaw !== "one-at-a-time") {
|
|
1172
|
+
await this.conn.sessionUpdate({
|
|
1173
|
+
sessionId: session.sessionId,
|
|
1174
|
+
update: {
|
|
1175
|
+
sessionUpdate: "agent_message_chunk",
|
|
1176
|
+
content: {
|
|
1177
|
+
type: "text",
|
|
1178
|
+
text: "Usage: /follow-up all | /follow-up one-at-a-time"
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
return { stopReason: "end_turn" };
|
|
1183
|
+
}
|
|
1184
|
+
piSession.setFollowUpMode(modeRaw);
|
|
1185
|
+
await this.conn.sessionUpdate({
|
|
1186
|
+
sessionId: session.sessionId,
|
|
1187
|
+
update: {
|
|
1188
|
+
sessionUpdate: "agent_message_chunk",
|
|
1189
|
+
content: {
|
|
1190
|
+
type: "text",
|
|
1191
|
+
text: `Follow-up mode set to: ${modeRaw}`
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
return { stopReason: "end_turn" };
|
|
1196
|
+
}
|
|
1197
|
+
if (cmd === "autocompact") {
|
|
1198
|
+
const mode = (args[0] ?? "toggle").toLowerCase();
|
|
1199
|
+
let enabled = null;
|
|
1200
|
+
if (mode === "on" || mode === "true" || mode === "enable") enabled = true;
|
|
1201
|
+
else if (mode === "off" || mode === "false" || mode === "disable") enabled = false;
|
|
1202
|
+
if (enabled === null) enabled = !piSession.autoCompactionEnabled;
|
|
1203
|
+
piSession.setAutoCompactionEnabled(enabled);
|
|
1204
|
+
await this.conn.sessionUpdate({
|
|
1205
|
+
sessionId: session.sessionId,
|
|
1206
|
+
update: {
|
|
1207
|
+
sessionUpdate: "agent_message_chunk",
|
|
1208
|
+
content: {
|
|
1209
|
+
type: "text",
|
|
1210
|
+
text: `Auto-compaction ${enabled ? "enabled" : "disabled"}.`
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
return { stopReason: "end_turn" };
|
|
1215
|
+
}
|
|
1216
|
+
if (cmd === "changelog") {
|
|
1217
|
+
const changelogPath = findChangelog();
|
|
1218
|
+
if (changelogPath === null) {
|
|
1219
|
+
await this.conn.sessionUpdate({
|
|
1220
|
+
sessionId: session.sessionId,
|
|
1221
|
+
update: {
|
|
1222
|
+
sessionUpdate: "agent_message_chunk",
|
|
1223
|
+
content: {
|
|
1224
|
+
type: "text",
|
|
1225
|
+
text: "Changelog not found."
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
return { stopReason: "end_turn" };
|
|
1230
|
+
}
|
|
1231
|
+
let text = "";
|
|
1232
|
+
try {
|
|
1233
|
+
text = readFileSync(changelogPath, "utf-8");
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1236
|
+
await this.conn.sessionUpdate({
|
|
1237
|
+
sessionId: session.sessionId,
|
|
1238
|
+
update: {
|
|
1239
|
+
sessionUpdate: "agent_message_chunk",
|
|
1240
|
+
content: {
|
|
1241
|
+
type: "text",
|
|
1242
|
+
text: `Failed to read changelog: ${msg}`
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
return { stopReason: "end_turn" };
|
|
1247
|
+
}
|
|
1248
|
+
const maxChars = 2e4;
|
|
1249
|
+
if (text.length > maxChars) text = `${text.slice(0, maxChars)}\n\n...(truncated)...`;
|
|
1250
|
+
await this.conn.sessionUpdate({
|
|
1251
|
+
sessionId: session.sessionId,
|
|
1252
|
+
update: {
|
|
1253
|
+
sessionUpdate: "agent_message_chunk",
|
|
1254
|
+
content: {
|
|
1255
|
+
type: "text",
|
|
1256
|
+
text
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
return { stopReason: "end_turn" };
|
|
1261
|
+
}
|
|
1262
|
+
if (cmd === "export") {
|
|
1263
|
+
if (piSession.messages.length === 0) {
|
|
1264
|
+
await this.conn.sessionUpdate({
|
|
1265
|
+
sessionId: session.sessionId,
|
|
1266
|
+
update: {
|
|
1267
|
+
sessionUpdate: "agent_message_chunk",
|
|
1268
|
+
content: {
|
|
1269
|
+
type: "text",
|
|
1270
|
+
text: "Nothing to export yet. Send a prompt first."
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
return { stopReason: "end_turn" };
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
const safeSessionId = session.sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1278
|
+
const outputPath = join(session.cwd, `pi-session-${safeSessionId}.html`);
|
|
1279
|
+
const resultPath = await piSession.exportToHtml(outputPath);
|
|
1280
|
+
await this.conn.sessionUpdate({
|
|
1281
|
+
sessionId: session.sessionId,
|
|
1282
|
+
update: {
|
|
1283
|
+
sessionUpdate: "agent_message_chunk",
|
|
1284
|
+
content: {
|
|
1285
|
+
type: "text",
|
|
1286
|
+
text: "Session exported: "
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
await this.conn.sessionUpdate({
|
|
1291
|
+
sessionId: session.sessionId,
|
|
1292
|
+
update: {
|
|
1293
|
+
sessionUpdate: "agent_message_chunk",
|
|
1294
|
+
content: {
|
|
1295
|
+
type: "resource_link",
|
|
1296
|
+
name: `pi-session-${safeSessionId}.html`,
|
|
1297
|
+
uri: `file://${resultPath}`,
|
|
1298
|
+
mimeType: "text/html",
|
|
1299
|
+
title: "Session exported"
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
} catch (e) {
|
|
1304
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1305
|
+
await this.conn.sessionUpdate({
|
|
1306
|
+
sessionId: session.sessionId,
|
|
1307
|
+
update: {
|
|
1308
|
+
sessionUpdate: "agent_message_chunk",
|
|
1309
|
+
content: {
|
|
1310
|
+
type: "text",
|
|
1311
|
+
text: `Export failed: ${msg}`
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
return { stopReason: "end_turn" };
|
|
1317
|
+
}
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
function isThinkingLevel(x) {
|
|
1322
|
+
return x === "off" || x === "minimal" || x === "low" || x === "medium" || x === "high" || x === "xhigh";
|
|
1323
|
+
}
|
|
1324
|
+
function buildThinkingModes(piSession) {
|
|
1325
|
+
const levels = piSession.getAvailableThinkingLevels();
|
|
1326
|
+
return {
|
|
1327
|
+
currentModeId: piSession.thinkingLevel,
|
|
1328
|
+
availableModes: levels.map((id) => ({
|
|
1329
|
+
id,
|
|
1330
|
+
name: `Thinking: ${id}`,
|
|
1331
|
+
description: null
|
|
1332
|
+
}))
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function buildModelState(piSession) {
|
|
1336
|
+
const available = piSession.modelRegistry.getAvailable();
|
|
1337
|
+
const current = piSession.model;
|
|
1338
|
+
const availableModels = available.map((m) => ({
|
|
1339
|
+
modelId: `${m.provider}/${m.id}`,
|
|
1340
|
+
name: `${m.provider}/${m.name ?? m.id}`,
|
|
1341
|
+
description: null
|
|
1342
|
+
}));
|
|
1343
|
+
let currentModelId = "default";
|
|
1344
|
+
if (current !== void 0) currentModelId = `${current.provider}/${current.id}`;
|
|
1345
|
+
else if (availableModels.length > 0 && availableModels[0] !== void 0) currentModelId = availableModels[0].modelId;
|
|
1346
|
+
return {
|
|
1347
|
+
availableModels,
|
|
1348
|
+
currentModelId
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function buildConfigOptions(modes, models) {
|
|
1352
|
+
return [{
|
|
1353
|
+
id: "model",
|
|
1354
|
+
name: "Model",
|
|
1355
|
+
description: "AI model to use",
|
|
1356
|
+
category: "model",
|
|
1357
|
+
type: "select",
|
|
1358
|
+
currentValue: models.currentModelId,
|
|
1359
|
+
options: models.availableModels.map((m) => ({
|
|
1360
|
+
value: m.modelId,
|
|
1361
|
+
name: m.name,
|
|
1362
|
+
description: m.description ?? null
|
|
1363
|
+
}))
|
|
1364
|
+
}, {
|
|
1365
|
+
id: "thought_level",
|
|
1366
|
+
name: "Thinking Level",
|
|
1367
|
+
description: "Reasoning depth for models that support it",
|
|
1368
|
+
category: "thought_level",
|
|
1369
|
+
type: "select",
|
|
1370
|
+
currentValue: modes.currentModeId,
|
|
1371
|
+
options: modes.availableModes.map((m) => ({
|
|
1372
|
+
value: m.id,
|
|
1373
|
+
name: m.name,
|
|
1374
|
+
description: m.description ?? null
|
|
1375
|
+
}))
|
|
1376
|
+
}];
|
|
1377
|
+
}
|
|
1378
|
+
function buildCommandList(piSession, enableSkillCommands) {
|
|
1379
|
+
const commands = [];
|
|
1380
|
+
for (const template of piSession.promptTemplates) commands.push({
|
|
1381
|
+
name: template.name,
|
|
1382
|
+
description: template.description ?? `(prompt)`
|
|
1383
|
+
});
|
|
1384
|
+
if (enableSkillCommands) {
|
|
1385
|
+
const skills = piSession.resourceLoader.getSkills();
|
|
1386
|
+
for (const skill of skills.skills) commands.push({
|
|
1387
|
+
name: `skill:${skill.name}`,
|
|
1388
|
+
description: skill.description ?? `(skill)`
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
const runner = piSession.extensionRunner;
|
|
1392
|
+
if (runner) for (const { command } of runner.getRegisteredCommandsWithPaths()) commands.push({
|
|
1393
|
+
name: command.name,
|
|
1394
|
+
description: command.description ?? `(extension)`
|
|
1395
|
+
});
|
|
1396
|
+
return commands;
|
|
1397
|
+
}
|
|
1398
|
+
function findPiSessionFile(sessionId) {
|
|
1399
|
+
const sessionsDir = join(piAgentDir(), "sessions");
|
|
1400
|
+
if (!existsSync(sessionsDir)) return null;
|
|
1401
|
+
const walkJsonl = (dir) => {
|
|
1402
|
+
let entries;
|
|
1403
|
+
try {
|
|
1404
|
+
entries = readdirSync(dir);
|
|
1405
|
+
} catch {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
for (const name of entries) {
|
|
1409
|
+
const p = join(dir, name);
|
|
1410
|
+
try {
|
|
1411
|
+
const st = statSync(p);
|
|
1412
|
+
if (st.isDirectory()) {
|
|
1413
|
+
const found = walkJsonl(p);
|
|
1414
|
+
if (found !== void 0) return found;
|
|
1415
|
+
} else if (st.isFile() && name.endsWith(".jsonl")) try {
|
|
1416
|
+
const firstLine = readFileSync(p, "utf8").split("\n")[0];
|
|
1417
|
+
if (firstLine === void 0) continue;
|
|
1418
|
+
const header = JSON.parse(firstLine);
|
|
1419
|
+
if (typeof header === "object" && header !== null && "type" in header && header.type === "session" && "id" in header && header.id === sessionId) return p;
|
|
1420
|
+
} catch {}
|
|
1421
|
+
} catch {}
|
|
1422
|
+
}
|
|
1423
|
+
return null;
|
|
1424
|
+
};
|
|
1425
|
+
return walkJsonl(sessionsDir);
|
|
1426
|
+
}
|
|
1427
|
+
function buildUpdateNotice() {
|
|
1428
|
+
try {
|
|
1429
|
+
const installed = VERSION;
|
|
1430
|
+
if (!installed || !isSemver(installed)) return null;
|
|
1431
|
+
const latestRes = spawnSync("npm", [
|
|
1432
|
+
"view",
|
|
1433
|
+
"@mariozechner/pi-coding-agent",
|
|
1434
|
+
"version"
|
|
1435
|
+
], {
|
|
1436
|
+
encoding: "utf-8",
|
|
1437
|
+
timeout: 800
|
|
1438
|
+
});
|
|
1439
|
+
const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
|
|
1440
|
+
if (!latest || !isSemver(latest)) return null;
|
|
1441
|
+
if (compareSemver(latest, installed) <= 0) return null;
|
|
1442
|
+
return `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
|
|
1443
|
+
} catch {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function buildStartupInfo(opts) {
|
|
1448
|
+
const md = [];
|
|
1449
|
+
if (VERSION) {
|
|
1450
|
+
md.push(`pi v${VERSION}`);
|
|
1451
|
+
md.push("---");
|
|
1452
|
+
md.push("");
|
|
1453
|
+
}
|
|
1454
|
+
const addSection = (title, items) => {
|
|
1455
|
+
const cleaned = items.map((s) => s.trim()).filter(Boolean);
|
|
1456
|
+
if (cleaned.length === 0) return;
|
|
1457
|
+
md.push(`## ${title}`);
|
|
1458
|
+
for (const item of cleaned) md.push(`- ${item}`);
|
|
1459
|
+
md.push("");
|
|
1460
|
+
};
|
|
1461
|
+
const contextItems = [];
|
|
1462
|
+
const contextPath = join(opts.cwd, "AGENTS.md");
|
|
1463
|
+
if (existsSync(contextPath)) contextItems.push(contextPath);
|
|
1464
|
+
addSection("Context", contextItems);
|
|
1465
|
+
if (opts.updateNotice !== void 0 && opts.updateNotice !== null) {
|
|
1466
|
+
md.push("---");
|
|
1467
|
+
md.push(opts.updateNotice);
|
|
1468
|
+
md.push("");
|
|
1469
|
+
}
|
|
1470
|
+
return `${md.join("\n").trim()}\n`;
|
|
1471
|
+
}
|
|
1472
|
+
function findChangelog() {
|
|
1473
|
+
try {
|
|
1474
|
+
const which = spawnSync(process.platform === "win32" ? "where" : "which", ["pi"], { encoding: "utf-8" });
|
|
1475
|
+
const piPath = String(which.stdout ?? "").split(/\r?\n/)[0]?.trim();
|
|
1476
|
+
if (piPath !== void 0 && piPath !== "") {
|
|
1477
|
+
const p = join(dirname(dirname(realpathSync(piPath))), "CHANGELOG.md");
|
|
1478
|
+
if (existsSync(p)) return p;
|
|
1479
|
+
}
|
|
1480
|
+
} catch {}
|
|
1481
|
+
try {
|
|
1482
|
+
const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
|
|
1483
|
+
const root = String(npmRoot.stdout ?? "").trim();
|
|
1484
|
+
if (root) {
|
|
1485
|
+
const p = join(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
|
|
1486
|
+
if (existsSync(p)) return p;
|
|
1487
|
+
}
|
|
1488
|
+
} catch {}
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
function isSemver(v) {
|
|
1492
|
+
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(v);
|
|
1493
|
+
}
|
|
1494
|
+
function compareSemver(a, b) {
|
|
1495
|
+
const pa = a.split(/[.-]/).slice(0, 3).map((n) => Number(n));
|
|
1496
|
+
const pb = b.split(/[.-]/).slice(0, 3).map((n) => Number(n));
|
|
1497
|
+
for (let i = 0; i < 3; i++) {
|
|
1498
|
+
const da = pa[i] ?? 0;
|
|
1499
|
+
const db = pb[i] ?? 0;
|
|
1500
|
+
if (da > db) return 1;
|
|
1501
|
+
if (da < db) return -1;
|
|
1502
|
+
}
|
|
1503
|
+
return 0;
|
|
1504
|
+
}
|
|
1505
|
+
function readNearestPackageJson(metaUrl) {
|
|
1506
|
+
const fallback = {
|
|
1507
|
+
name: "pi-acp",
|
|
1508
|
+
version: "0.0.0"
|
|
1509
|
+
};
|
|
1510
|
+
try {
|
|
1511
|
+
let dir = dirname(fileURLToPath(metaUrl));
|
|
1512
|
+
for (let i = 0; i < 6; i++) {
|
|
1513
|
+
const p = join(dir, "package.json");
|
|
1514
|
+
if (existsSync(p)) {
|
|
1515
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
1516
|
+
if (typeof raw !== "object" || raw === null) return fallback;
|
|
1517
|
+
return {
|
|
1518
|
+
name: "name" in raw && typeof raw.name === "string" ? raw.name : fallback.name,
|
|
1519
|
+
version: "version" in raw && typeof raw.version === "string" ? raw.version : fallback.version
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
dir = dirname(dir);
|
|
1523
|
+
}
|
|
1524
|
+
} catch {}
|
|
1525
|
+
return fallback;
|
|
1526
|
+
}
|
|
1527
|
+
//#endregion
|
|
1528
|
+
//#region src/index.ts
|
|
1529
|
+
if (process.argv.includes("--terminal-login")) {
|
|
1530
|
+
const { spawnSync } = await import("node:child_process");
|
|
1531
|
+
const isWindows = platform() === "win32";
|
|
1532
|
+
const cmd = process.env.PI_ACP_PI_COMMAND ?? (isWindows ? "pi.cmd" : "pi");
|
|
1533
|
+
const res = spawnSync(cmd, [], {
|
|
1534
|
+
stdio: "inherit",
|
|
1535
|
+
env: process.env
|
|
1536
|
+
});
|
|
1537
|
+
if (res.error && "code" in res.error && res.error.code === "ENOENT") {
|
|
1538
|
+
process.stderr.write(`pi-acp: could not start pi (command not found: ${cmd}). Install via \`npm install -g @mariozechner/pi-coding-agent\` or ensure \`pi\` is on your PATH.
|
|
1539
|
+
`);
|
|
1540
|
+
process.exit(1);
|
|
1541
|
+
}
|
|
1542
|
+
process.exit(typeof res.status === "number" ? res.status : 1);
|
|
1543
|
+
}
|
|
1544
|
+
const agent = new AgentSideConnection((conn) => new PiAcpAgent(conn), ndJsonStream(new WritableStream({ write(chunk) {
|
|
1545
|
+
return new Promise((resolve) => {
|
|
1546
|
+
if (process.stdout.destroyed || !process.stdout.writable) {
|
|
1547
|
+
resolve();
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
try {
|
|
1551
|
+
process.stdout.write(chunk, () => resolve());
|
|
1552
|
+
} catch {
|
|
1553
|
+
resolve();
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
} }), new ReadableStream({ start(controller) {
|
|
1557
|
+
process.stdin.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
|
|
1558
|
+
process.stdin.on("end", () => controller.close());
|
|
1559
|
+
process.stdin.on("error", (err) => controller.error(err));
|
|
1560
|
+
} })));
|
|
1561
|
+
function shutdown() {
|
|
1562
|
+
try {
|
|
1563
|
+
if ("agent" in agent) {
|
|
1564
|
+
const inner = agent.agent;
|
|
1565
|
+
if (typeof inner === "object" && inner !== null && "dispose" in inner && typeof inner.dispose === "function") inner.dispose();
|
|
1566
|
+
}
|
|
1567
|
+
} catch {}
|
|
1568
|
+
process.exit(0);
|
|
1569
|
+
}
|
|
1570
|
+
process.stdin.on("end", shutdown);
|
|
1571
|
+
process.stdin.on("close", shutdown);
|
|
1572
|
+
process.stdin.resume();
|
|
1573
|
+
process.on("SIGINT", shutdown);
|
|
1574
|
+
process.on("SIGTERM", shutdown);
|
|
1575
|
+
process.stdout.on("error", () => process.exit(0));
|
|
1576
|
+
//#endregion
|
|
1577
|
+
export {};
|
|
1578
|
+
|
|
1579
|
+
//# sourceMappingURL=index.mjs.map
|