pi-acp 0.0.14 → 0.0.16
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 +46 -11
- package/dist/index.js +810 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,21 +5,103 @@ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
|
5
5
|
|
|
6
6
|
// src/acp/agent.ts
|
|
7
7
|
import {
|
|
8
|
-
RequestError as
|
|
8
|
+
RequestError as RequestError3
|
|
9
9
|
} from "@agentclientprotocol/sdk";
|
|
10
10
|
|
|
11
|
+
// src/acp/auth.ts
|
|
12
|
+
var PI_SETUP_METHOD_ID = "pi_terminal_login";
|
|
13
|
+
function getAuthMethods(opts) {
|
|
14
|
+
const supportsTerminalAuthMeta = opts?.supportsTerminalAuthMeta ?? true;
|
|
15
|
+
const method = {
|
|
16
|
+
id: PI_SETUP_METHOD_ID,
|
|
17
|
+
name: "Launch pi in the terminal",
|
|
18
|
+
description: "Start pi in an interactive terminal to configure API keys or login",
|
|
19
|
+
// Registry-required fields
|
|
20
|
+
type: "terminal",
|
|
21
|
+
args: ["--terminal-login"],
|
|
22
|
+
env: {}
|
|
23
|
+
};
|
|
24
|
+
if (supportsTerminalAuthMeta) {
|
|
25
|
+
const launch = terminalAuthLaunchSpec();
|
|
26
|
+
method._meta = {
|
|
27
|
+
...method._meta ?? {},
|
|
28
|
+
"terminal-auth": {
|
|
29
|
+
...launch,
|
|
30
|
+
label: "Launch pi"
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return [method];
|
|
35
|
+
}
|
|
36
|
+
function terminalAuthLaunchSpec() {
|
|
37
|
+
const argv0 = process.argv[0] || "node";
|
|
38
|
+
const argv1 = process.argv[1];
|
|
39
|
+
if (argv1 && argv0) {
|
|
40
|
+
const isNode = argv0.includes("node");
|
|
41
|
+
const isJs = argv1.endsWith(".js");
|
|
42
|
+
if (isNode && isJs) {
|
|
43
|
+
return { command: argv0, args: [argv1, "--terminal-login"] };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { command: "pi-acp", args: ["--terminal-login"] };
|
|
47
|
+
}
|
|
48
|
+
|
|
11
49
|
// src/acp/session.ts
|
|
50
|
+
import { RequestError as RequestError2 } from "@agentclientprotocol/sdk";
|
|
51
|
+
|
|
52
|
+
// src/acp/auth-required.ts
|
|
12
53
|
import { RequestError } from "@agentclientprotocol/sdk";
|
|
54
|
+
function maybeAuthRequiredError(err) {
|
|
55
|
+
const msg = String(err?.message ?? err ?? "");
|
|
56
|
+
const s = msg.toLowerCase();
|
|
57
|
+
const patterns = [
|
|
58
|
+
"api key",
|
|
59
|
+
"apikey",
|
|
60
|
+
"missing key",
|
|
61
|
+
"no key",
|
|
62
|
+
"not configured",
|
|
63
|
+
"unauthorized",
|
|
64
|
+
"authentication",
|
|
65
|
+
"permission denied",
|
|
66
|
+
"forbidden",
|
|
67
|
+
"401",
|
|
68
|
+
"403"
|
|
69
|
+
];
|
|
70
|
+
const hit = patterns.some((p) => s.includes(p));
|
|
71
|
+
if (!hit) return null;
|
|
72
|
+
return RequestError.authRequired(
|
|
73
|
+
{
|
|
74
|
+
authMethods: getAuthMethods()
|
|
75
|
+
},
|
|
76
|
+
"Configure an API key or log in with an OAuth provider."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/acp/session.ts
|
|
13
81
|
import { readFileSync as readFileSync3 } from "fs";
|
|
14
82
|
import { isAbsolute, resolve as resolvePath } from "path";
|
|
15
83
|
|
|
16
84
|
// src/pi-rpc/process.ts
|
|
17
85
|
import { spawn } from "child_process";
|
|
18
86
|
import * as readline from "readline";
|
|
87
|
+
var PiRpcSpawnError = class extends Error {
|
|
88
|
+
/** Underlying spawn error code, e.g. ENOENT, EACCES */
|
|
89
|
+
code;
|
|
90
|
+
constructor(message, opts) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = "PiRpcSpawnError";
|
|
93
|
+
this.code = opts?.code;
|
|
94
|
+
this.cause = opts?.cause;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
function stripAnsi(s) {
|
|
98
|
+
return s.replace(/[\u001B\u009B][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
99
|
+
}
|
|
19
100
|
var PiRpcProcess = class _PiRpcProcess {
|
|
20
101
|
child;
|
|
21
102
|
pending = /* @__PURE__ */ new Map();
|
|
22
103
|
eventHandlers = [];
|
|
104
|
+
preludeLines = [];
|
|
23
105
|
constructor(child) {
|
|
24
106
|
this.child = child;
|
|
25
107
|
const rl = readline.createInterface({ input: child.stdout });
|
|
@@ -29,6 +111,8 @@ var PiRpcProcess = class _PiRpcProcess {
|
|
|
29
111
|
try {
|
|
30
112
|
msg = JSON.parse(line);
|
|
31
113
|
} catch {
|
|
114
|
+
const cleaned = stripAnsi(String(line)).trimEnd();
|
|
115
|
+
if (cleaned) this.preludeLines.push(cleaned);
|
|
32
116
|
return;
|
|
33
117
|
}
|
|
34
118
|
if (msg?.type === "response") {
|
|
@@ -49,6 +133,10 @@ var PiRpcProcess = class _PiRpcProcess {
|
|
|
49
133
|
for (const [, p] of this.pending) p.reject(err);
|
|
50
134
|
this.pending.clear();
|
|
51
135
|
});
|
|
136
|
+
child.on("error", (err) => {
|
|
137
|
+
for (const [, p] of this.pending) p.reject(err);
|
|
138
|
+
this.pending.clear();
|
|
139
|
+
});
|
|
52
140
|
}
|
|
53
141
|
static async spawn(params) {
|
|
54
142
|
const cmd = params.piCommand ?? "pi";
|
|
@@ -59,6 +147,36 @@ var PiRpcProcess = class _PiRpcProcess {
|
|
|
59
147
|
stdio: "pipe",
|
|
60
148
|
env: process.env
|
|
61
149
|
});
|
|
150
|
+
try {
|
|
151
|
+
await new Promise((resolve2, reject) => {
|
|
152
|
+
const onSpawn = () => {
|
|
153
|
+
cleanup();
|
|
154
|
+
resolve2();
|
|
155
|
+
};
|
|
156
|
+
const onError = (err) => {
|
|
157
|
+
cleanup();
|
|
158
|
+
reject(err);
|
|
159
|
+
};
|
|
160
|
+
const cleanup = () => {
|
|
161
|
+
child.off("spawn", onSpawn);
|
|
162
|
+
child.off("error", onError);
|
|
163
|
+
};
|
|
164
|
+
child.once("spawn", onSpawn);
|
|
165
|
+
child.once("error", onError);
|
|
166
|
+
});
|
|
167
|
+
} catch (e) {
|
|
168
|
+
const code = typeof e?.code === "string" ? e.code : void 0;
|
|
169
|
+
if (code === "ENOENT") {
|
|
170
|
+
throw new PiRpcSpawnError(
|
|
171
|
+
`Could not start pi: executable not found (command: ${cmd}). Pi needs to be installed before it can run in ACP clients. Install it via \`npm install -g @mariozechner/pi-coding-agent\` or ensure \`pi\` is on your PATH. Then try again.`,
|
|
172
|
+
{ code, cause: e }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (code === "EACCES") {
|
|
176
|
+
throw new PiRpcSpawnError(`Could not start pi: permission denied (command: ${cmd}).`, { code, cause: e });
|
|
177
|
+
}
|
|
178
|
+
throw new PiRpcSpawnError(`Could not start pi (command: ${cmd}).`, { code, cause: e });
|
|
179
|
+
}
|
|
62
180
|
child.stderr.on("data", () => {
|
|
63
181
|
});
|
|
64
182
|
const proc = new _PiRpcProcess(child);
|
|
@@ -80,6 +198,21 @@ var PiRpcProcess = class _PiRpcProcess {
|
|
|
80
198
|
this.eventHandlers = this.eventHandlers.filter((h) => h !== handler);
|
|
81
199
|
};
|
|
82
200
|
}
|
|
201
|
+
dispose(signal = "SIGTERM") {
|
|
202
|
+
if (this.child.killed) return;
|
|
203
|
+
try {
|
|
204
|
+
this.child.kill(signal);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Human-readable stdout lines emitted before RPC NDJSON begins (e.g. Context/Skills/Extensions info).
|
|
210
|
+
* Themes are typically noisy/less useful for ACP, so callers can filter as needed.
|
|
211
|
+
*/
|
|
212
|
+
consumePreludeLines() {
|
|
213
|
+
const lines = this.preludeLines.splice(0, this.preludeLines.length);
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
83
216
|
async prompt(message, attachments = []) {
|
|
84
217
|
const res = await this.request({ type: "prompt", message, attachments });
|
|
85
218
|
if (!res.success) throw new Error(`pi prompt failed: ${res.error ?? JSON.stringify(res.data)}`);
|
|
@@ -150,12 +283,17 @@ var PiRpcProcess = class _PiRpcProcess {
|
|
|
150
283
|
const line = JSON.stringify(withId) + "\n";
|
|
151
284
|
return new Promise((resolve2, reject) => {
|
|
152
285
|
this.pending.set(id, { resolve: resolve2, reject });
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
286
|
+
try {
|
|
287
|
+
this.child.stdin.write(line, (err) => {
|
|
288
|
+
if (err) {
|
|
289
|
+
this.pending.delete(id);
|
|
290
|
+
reject(err);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
} catch (e) {
|
|
294
|
+
this.pending.delete(id);
|
|
295
|
+
reject(e);
|
|
296
|
+
}
|
|
159
297
|
});
|
|
160
298
|
}
|
|
161
299
|
};
|
|
@@ -379,10 +517,24 @@ var SessionManager = class {
|
|
|
379
517
|
return this.sessions.get(sessionId);
|
|
380
518
|
}
|
|
381
519
|
async create(params) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
520
|
+
let proc;
|
|
521
|
+
try {
|
|
522
|
+
proc = await PiRpcProcess.spawn({
|
|
523
|
+
cwd: params.cwd,
|
|
524
|
+
piCommand: params.piCommand
|
|
525
|
+
});
|
|
526
|
+
} catch (e) {
|
|
527
|
+
if (e instanceof PiRpcSpawnError) {
|
|
528
|
+
throw RequestError2.internalError({ code: e.code }, e.message);
|
|
529
|
+
}
|
|
530
|
+
throw e;
|
|
531
|
+
}
|
|
532
|
+
let state = null;
|
|
533
|
+
try {
|
|
534
|
+
state = await proc.getState();
|
|
535
|
+
} catch {
|
|
536
|
+
state = null;
|
|
537
|
+
}
|
|
386
538
|
const sessionId = typeof state?.sessionId === "string" ? state.sessionId : crypto.randomUUID();
|
|
387
539
|
const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
|
|
388
540
|
if (sessionFile) {
|
|
@@ -401,7 +553,7 @@ var SessionManager = class {
|
|
|
401
553
|
}
|
|
402
554
|
get(sessionId) {
|
|
403
555
|
const s = this.sessions.get(sessionId);
|
|
404
|
-
if (!s) throw
|
|
556
|
+
if (!s) throw RequestError2.invalidParams(`Unknown sessionId: ${sessionId}`);
|
|
405
557
|
return s;
|
|
406
558
|
}
|
|
407
559
|
/**
|
|
@@ -427,6 +579,8 @@ var PiAcpSession = class {
|
|
|
427
579
|
sessionId;
|
|
428
580
|
cwd;
|
|
429
581
|
mcpServers;
|
|
582
|
+
startupInfo = null;
|
|
583
|
+
startupInfoSent = false;
|
|
430
584
|
proc;
|
|
431
585
|
conn;
|
|
432
586
|
fileCommands;
|
|
@@ -459,7 +613,30 @@ var PiAcpSession = class {
|
|
|
459
613
|
this.fileCommands = opts.fileCommands ?? [];
|
|
460
614
|
this.proc.onEvent((ev) => this.handlePiEvent(ev));
|
|
461
615
|
}
|
|
616
|
+
setStartupInfo(text) {
|
|
617
|
+
this.startupInfo = text;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Best-effort attempt to send startup info outside of a prompt turn.
|
|
621
|
+
* Some clients (e.g. Zed) may only render agent messages once the UI is ready;
|
|
622
|
+
* callers can invoke this shortly after session/new returns.
|
|
623
|
+
*/
|
|
624
|
+
sendStartupInfoIfPending() {
|
|
625
|
+
if (this.startupInfoSent || !this.startupInfo) return;
|
|
626
|
+
this.startupInfoSent = true;
|
|
627
|
+
this.emit({
|
|
628
|
+
sessionUpdate: "agent_message_chunk",
|
|
629
|
+
content: { type: "text", text: this.startupInfo }
|
|
630
|
+
});
|
|
631
|
+
}
|
|
462
632
|
async prompt(message, attachments = []) {
|
|
633
|
+
if (!this.startupInfoSent && this.startupInfo) {
|
|
634
|
+
this.startupInfoSent = true;
|
|
635
|
+
this.emit({
|
|
636
|
+
sessionUpdate: "agent_message_chunk",
|
|
637
|
+
content: { type: "text", text: this.startupInfo }
|
|
638
|
+
});
|
|
639
|
+
}
|
|
463
640
|
const expandedMessage = expandSlashCommand(message, this.fileCommands);
|
|
464
641
|
const turnPromise = new Promise((resolve2, reject) => {
|
|
465
642
|
const queued = { message: expandedMessage, attachments, resolve: resolve2, reject };
|
|
@@ -523,8 +700,13 @@ var PiAcpSession = class {
|
|
|
523
700
|
});
|
|
524
701
|
this.proc.prompt(t.message, t.attachments).catch((err) => {
|
|
525
702
|
void this.flushEmits().finally(() => {
|
|
526
|
-
const
|
|
527
|
-
|
|
703
|
+
const authErr = maybeAuthRequiredError(err);
|
|
704
|
+
if (authErr) {
|
|
705
|
+
this.pendingTurn?.reject(authErr);
|
|
706
|
+
} else {
|
|
707
|
+
const reason = this.cancelRequested ? "cancelled" : "error";
|
|
708
|
+
this.pendingTurn?.resolve(reason);
|
|
709
|
+
}
|
|
528
710
|
this.pendingTurn = null;
|
|
529
711
|
this.inAgentLoop = false;
|
|
530
712
|
this.emit({
|
|
@@ -728,6 +910,245 @@ function toToolKind(toolName) {
|
|
|
728
910
|
}
|
|
729
911
|
}
|
|
730
912
|
|
|
913
|
+
// src/acp/pi-sessions.ts
|
|
914
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync, openSync, readSync, closeSync } from "fs";
|
|
915
|
+
import { homedir as homedir3 } from "os";
|
|
916
|
+
import { join as join3 } from "path";
|
|
917
|
+
var DEFAULT_TAIL_BYTES = 256 * 1024;
|
|
918
|
+
var DEFAULT_HEAD_BYTES = 64 * 1024;
|
|
919
|
+
function getPiAgentDir() {
|
|
920
|
+
return process.env.PI_CODING_AGENT_DIR ?? join3(homedir3(), ".pi", "agent");
|
|
921
|
+
}
|
|
922
|
+
function getPiSessionsDir() {
|
|
923
|
+
return join3(getPiAgentDir(), "sessions");
|
|
924
|
+
}
|
|
925
|
+
function walkJsonlFiles(dir, out) {
|
|
926
|
+
let entries;
|
|
927
|
+
try {
|
|
928
|
+
entries = readdirSync2(dir, { withFileTypes: true, encoding: "utf8" });
|
|
929
|
+
} catch {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
for (const e of entries) {
|
|
933
|
+
const name = typeof e.name === "string" ? e.name : String(e.name);
|
|
934
|
+
const p = join3(dir, name);
|
|
935
|
+
if (e.isDirectory()) walkJsonlFiles(p, out);
|
|
936
|
+
else if (e.isFile() && name.endsWith(".jsonl")) out.push(p);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function readFirstLine(path) {
|
|
940
|
+
const fd = openSync(path, "r");
|
|
941
|
+
try {
|
|
942
|
+
const buf = Buffer.alloc(DEFAULT_HEAD_BYTES);
|
|
943
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
944
|
+
if (n <= 0) return null;
|
|
945
|
+
const s = buf.subarray(0, n).toString("utf-8");
|
|
946
|
+
const idx = s.indexOf("\n");
|
|
947
|
+
return idx === -1 ? s.trim() : s.slice(0, idx).trim();
|
|
948
|
+
} catch {
|
|
949
|
+
return null;
|
|
950
|
+
} finally {
|
|
951
|
+
try {
|
|
952
|
+
closeSync(fd);
|
|
953
|
+
} catch {
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function readTail(path, tailBytes = DEFAULT_TAIL_BYTES) {
|
|
958
|
+
const st = statSync(path);
|
|
959
|
+
const start = Math.max(0, st.size - tailBytes);
|
|
960
|
+
const len = st.size - start;
|
|
961
|
+
const fd = openSync(path, "r");
|
|
962
|
+
try {
|
|
963
|
+
const buf = Buffer.alloc(len);
|
|
964
|
+
const n = readSync(fd, buf, 0, buf.length, start);
|
|
965
|
+
return buf.subarray(0, n).toString("utf-8");
|
|
966
|
+
} finally {
|
|
967
|
+
try {
|
|
968
|
+
closeSync(fd);
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function parseSessionHeader(firstLine) {
|
|
974
|
+
try {
|
|
975
|
+
const obj = JSON.parse(firstLine);
|
|
976
|
+
if (obj?.type !== "session") return null;
|
|
977
|
+
const sessionId = typeof obj?.id === "string" ? obj.id : null;
|
|
978
|
+
const cwd = typeof obj?.cwd === "string" ? obj.cwd : null;
|
|
979
|
+
if (!sessionId || !cwd) return null;
|
|
980
|
+
return { sessionId, cwd };
|
|
981
|
+
} catch {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function pickTitleFromTail(tail) {
|
|
986
|
+
const lines = tail.split(/\r?\n/);
|
|
987
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
988
|
+
const line = lines[i].trim();
|
|
989
|
+
if (!line) continue;
|
|
990
|
+
try {
|
|
991
|
+
const obj = JSON.parse(line);
|
|
992
|
+
if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
|
|
993
|
+
return obj.name.trim();
|
|
994
|
+
}
|
|
995
|
+
} catch {
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
function scanSessionInfoNameFromFile(path) {
|
|
1001
|
+
const fd = openSync(path, "r");
|
|
1002
|
+
try {
|
|
1003
|
+
const buf = Buffer.alloc(256 * 1024);
|
|
1004
|
+
let leftover = "";
|
|
1005
|
+
let offset = 0;
|
|
1006
|
+
let lastName = null;
|
|
1007
|
+
while (true) {
|
|
1008
|
+
const n = readSync(fd, buf, 0, buf.length, offset);
|
|
1009
|
+
if (n <= 0) break;
|
|
1010
|
+
offset += n;
|
|
1011
|
+
const chunk = leftover + buf.subarray(0, n).toString("utf8");
|
|
1012
|
+
const lines = chunk.split(/\r?\n/);
|
|
1013
|
+
leftover = lines.pop() ?? "";
|
|
1014
|
+
for (const line0 of lines) {
|
|
1015
|
+
const line = line0.trim();
|
|
1016
|
+
if (!line) continue;
|
|
1017
|
+
try {
|
|
1018
|
+
const obj = JSON.parse(line);
|
|
1019
|
+
if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
|
|
1020
|
+
lastName = obj.name.trim();
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const tailLine = leftover.trim();
|
|
1027
|
+
if (tailLine) {
|
|
1028
|
+
try {
|
|
1029
|
+
const obj = JSON.parse(tailLine);
|
|
1030
|
+
if (obj?.type === "session_info" && typeof obj?.name === "string" && obj.name.trim()) {
|
|
1031
|
+
lastName = obj.name.trim();
|
|
1032
|
+
}
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return lastName;
|
|
1037
|
+
} catch {
|
|
1038
|
+
return null;
|
|
1039
|
+
} finally {
|
|
1040
|
+
try {
|
|
1041
|
+
closeSync(fd);
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function pickUpdatedAtFromTail(tail) {
|
|
1047
|
+
const lines = tail.split(/\r?\n/);
|
|
1048
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1049
|
+
const line = lines[i].trim();
|
|
1050
|
+
if (!line) continue;
|
|
1051
|
+
try {
|
|
1052
|
+
const obj = JSON.parse(line);
|
|
1053
|
+
if (obj?.type !== "message") continue;
|
|
1054
|
+
const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
|
|
1055
|
+
if (!ts) continue;
|
|
1056
|
+
const d = new Date(ts);
|
|
1057
|
+
if (Number.isFinite(d.getTime())) return d.toISOString();
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1062
|
+
const line = lines[i].trim();
|
|
1063
|
+
if (!line) continue;
|
|
1064
|
+
try {
|
|
1065
|
+
const obj = JSON.parse(line);
|
|
1066
|
+
const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
|
|
1067
|
+
if (!ts) continue;
|
|
1068
|
+
const d = new Date(ts);
|
|
1069
|
+
if (Number.isFinite(d.getTime())) return d.toISOString();
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
function pickFallbackTitleFromHead(path) {
|
|
1076
|
+
try {
|
|
1077
|
+
const raw = readFileSync4(path, { encoding: "utf8" });
|
|
1078
|
+
const lines = raw.split(/\r?\n/);
|
|
1079
|
+
for (const line0 of lines) {
|
|
1080
|
+
const line = line0.trim();
|
|
1081
|
+
if (!line) continue;
|
|
1082
|
+
try {
|
|
1083
|
+
const obj = JSON.parse(line);
|
|
1084
|
+
if (obj?.type === "message" && obj?.message?.role === "user") {
|
|
1085
|
+
const content = obj?.message?.content;
|
|
1086
|
+
if (typeof content === "string") return content.slice(0, 80);
|
|
1087
|
+
if (Array.isArray(content)) {
|
|
1088
|
+
const t = content.find((c) => c?.type === "text" && typeof c?.text === "string");
|
|
1089
|
+
if (t?.text) return String(t.text).slice(0, 80);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
}
|
|
1094
|
+
if (lines.length > 2e3) break;
|
|
1095
|
+
}
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
function listPiSessions() {
|
|
1101
|
+
const sessionsDir = getPiSessionsDir();
|
|
1102
|
+
const files = [];
|
|
1103
|
+
walkJsonlFiles(sessionsDir, files);
|
|
1104
|
+
const items = [];
|
|
1105
|
+
for (const file of files) {
|
|
1106
|
+
const first = readFirstLine(file);
|
|
1107
|
+
if (!first) continue;
|
|
1108
|
+
const header = parseSessionHeader(first);
|
|
1109
|
+
if (!header) continue;
|
|
1110
|
+
let updatedAt = null;
|
|
1111
|
+
let title = null;
|
|
1112
|
+
try {
|
|
1113
|
+
const tail = readTail(file);
|
|
1114
|
+
title = pickTitleFromTail(tail);
|
|
1115
|
+
updatedAt = pickUpdatedAtFromTail(tail);
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
if (!title) {
|
|
1119
|
+
title = scanSessionInfoNameFromFile(file);
|
|
1120
|
+
}
|
|
1121
|
+
if (!updatedAt) {
|
|
1122
|
+
try {
|
|
1123
|
+
updatedAt = statSync(file).mtime.toISOString();
|
|
1124
|
+
} catch {
|
|
1125
|
+
updatedAt = null;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (!title) {
|
|
1129
|
+
title = pickFallbackTitleFromHead(file);
|
|
1130
|
+
}
|
|
1131
|
+
items.push({
|
|
1132
|
+
sessionId: header.sessionId,
|
|
1133
|
+
cwd: header.cwd,
|
|
1134
|
+
title,
|
|
1135
|
+
updatedAt,
|
|
1136
|
+
sessionFile: file
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
items.sort((a, b) => {
|
|
1140
|
+
const aa = a.updatedAt ?? "";
|
|
1141
|
+
const bb = b.updatedAt ?? "";
|
|
1142
|
+
return bb.localeCompare(aa);
|
|
1143
|
+
});
|
|
1144
|
+
return items;
|
|
1145
|
+
}
|
|
1146
|
+
function findPiSessionFile(sessionId) {
|
|
1147
|
+
const all = listPiSessions();
|
|
1148
|
+
const found = all.find((s) => s.sessionId === sessionId);
|
|
1149
|
+
return found?.sessionFile ?? null;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
731
1152
|
// src/acp/translate/pi-messages.ts
|
|
732
1153
|
function normalizePiMessageText(content) {
|
|
733
1154
|
if (typeof content === "string") return content;
|
|
@@ -803,10 +1224,89 @@ ${r.text}`;
|
|
|
803
1224
|
|
|
804
1225
|
// src/acp/agent.ts
|
|
805
1226
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
806
|
-
import { existsSync as
|
|
807
|
-
import { join as
|
|
1227
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
1228
|
+
import { join as join5, dirname as dirname2, basename } from "path";
|
|
808
1229
|
import { spawnSync } from "child_process";
|
|
1230
|
+
|
|
1231
|
+
// src/pi-auth/status.ts
|
|
1232
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
1233
|
+
import { homedir as homedir4 } from "os";
|
|
1234
|
+
import { join as join4 } from "path";
|
|
1235
|
+
function safeReadJson(path) {
|
|
1236
|
+
try {
|
|
1237
|
+
if (!existsSync2(path)) return null;
|
|
1238
|
+
const raw = readFileSync5(path, "utf-8");
|
|
1239
|
+
if (!raw.trim()) return null;
|
|
1240
|
+
return JSON.parse(raw);
|
|
1241
|
+
} catch {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
function getPiAgentDir2() {
|
|
1246
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
1247
|
+
if (envDir) {
|
|
1248
|
+
if (envDir === "~") return homedir4();
|
|
1249
|
+
if (envDir.startsWith("~/")) return homedir4() + envDir.slice(1);
|
|
1250
|
+
return envDir;
|
|
1251
|
+
}
|
|
1252
|
+
return join4(homedir4(), ".pi", "agent");
|
|
1253
|
+
}
|
|
1254
|
+
function hasAnyPiAuthConfigured() {
|
|
1255
|
+
const agentDir = getPiAgentDir2();
|
|
1256
|
+
const authPath = join4(agentDir, "auth.json");
|
|
1257
|
+
const auth = safeReadJson(authPath);
|
|
1258
|
+
if (auth && typeof auth === "object" && Object.keys(auth).length > 0) return true;
|
|
1259
|
+
const modelsPath = join4(agentDir, "models.json");
|
|
1260
|
+
const models = safeReadJson(modelsPath);
|
|
1261
|
+
const providers = models?.providers;
|
|
1262
|
+
if (providers && typeof providers === "object") {
|
|
1263
|
+
for (const p of Object.values(providers)) {
|
|
1264
|
+
if (p && typeof p === "object" && typeof p.apiKey === "string" && p.apiKey.trim()) {
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const envVars = [
|
|
1270
|
+
"OPENAI_API_KEY",
|
|
1271
|
+
"AZURE_OPENAI_API_KEY",
|
|
1272
|
+
"GEMINI_API_KEY",
|
|
1273
|
+
"GROQ_API_KEY",
|
|
1274
|
+
"CEREBRAS_API_KEY",
|
|
1275
|
+
"XAI_API_KEY",
|
|
1276
|
+
"OPENROUTER_API_KEY",
|
|
1277
|
+
"AI_GATEWAY_API_KEY",
|
|
1278
|
+
"ZAI_API_KEY",
|
|
1279
|
+
"MISTRAL_API_KEY",
|
|
1280
|
+
"MINIMAX_API_KEY",
|
|
1281
|
+
"MINIMAX_CN_API_KEY",
|
|
1282
|
+
"HF_TOKEN",
|
|
1283
|
+
"OPENCODE_API_KEY",
|
|
1284
|
+
"KIMI_API_KEY",
|
|
1285
|
+
// Copilot/github
|
|
1286
|
+
"COPILOT_GITHUB_TOKEN",
|
|
1287
|
+
"GH_TOKEN",
|
|
1288
|
+
"GITHUB_TOKEN",
|
|
1289
|
+
// Anthropic oauth
|
|
1290
|
+
"ANTHROPIC_OAUTH_TOKEN",
|
|
1291
|
+
"ANTHROPIC_API_KEY"
|
|
1292
|
+
];
|
|
1293
|
+
for (const k of envVars) {
|
|
1294
|
+
const v = process.env[k];
|
|
1295
|
+
if (typeof v === "string" && v.trim()) return true;
|
|
1296
|
+
}
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/acp/agent.ts
|
|
809
1301
|
import { fileURLToPath } from "url";
|
|
1302
|
+
function booleanEnv(name, defaultValue) {
|
|
1303
|
+
const raw = process.env[name];
|
|
1304
|
+
if (raw == null) return defaultValue;
|
|
1305
|
+
const v = String(raw).trim().toLowerCase();
|
|
1306
|
+
if (v === "true") return true;
|
|
1307
|
+
if (v === "false") return false;
|
|
1308
|
+
return defaultValue;
|
|
1309
|
+
}
|
|
810
1310
|
function builtinAvailableCommands() {
|
|
811
1311
|
return [
|
|
812
1312
|
{
|
|
@@ -858,8 +1358,11 @@ var PiAcpAgent = class {
|
|
|
858
1358
|
conn;
|
|
859
1359
|
sessions = new SessionManager();
|
|
860
1360
|
store = new SessionStore();
|
|
861
|
-
|
|
1361
|
+
// Remember recent session cwd and use it as the default filter.
|
|
1362
|
+
lastSessionCwd = null;
|
|
1363
|
+
constructor(conn, _config) {
|
|
862
1364
|
this.conn = conn;
|
|
1365
|
+
void _config;
|
|
863
1366
|
}
|
|
864
1367
|
async initialize(params) {
|
|
865
1368
|
const supportedVersion = 1;
|
|
@@ -871,7 +1374,11 @@ var PiAcpAgent = class {
|
|
|
871
1374
|
title: "pi ACP adapter",
|
|
872
1375
|
version: pkg.version ?? "0.0.0"
|
|
873
1376
|
},
|
|
874
|
-
|
|
1377
|
+
// Zed currently uses ClientCapabilities._meta["terminal-auth"] to decide whether to show
|
|
1378
|
+
// the "Authenticate" banner/button. If not supported, we still return the method for the registry.
|
|
1379
|
+
authMethods: getAuthMethods({
|
|
1380
|
+
supportsTerminalAuthMeta: params?.clientCapabilities?._meta?.["terminal-auth"] === true
|
|
1381
|
+
}),
|
|
875
1382
|
agentCapabilities: {
|
|
876
1383
|
loadSession: true,
|
|
877
1384
|
mcpCapabilities: { http: false, sse: false },
|
|
@@ -880,29 +1387,65 @@ var PiAcpAgent = class {
|
|
|
880
1387
|
audio: false,
|
|
881
1388
|
embeddedContext: false
|
|
882
1389
|
},
|
|
883
|
-
sessionCapabilities: {
|
|
1390
|
+
sessionCapabilities: {
|
|
1391
|
+
// **UNSTABLE** ACP capability used by Zed's codex-acp adapter.
|
|
1392
|
+
// Enables a native session picker in clients that support it.
|
|
1393
|
+
list: {}
|
|
1394
|
+
}
|
|
884
1395
|
}
|
|
885
1396
|
};
|
|
886
1397
|
}
|
|
887
1398
|
async newSession(params) {
|
|
888
1399
|
if (!isAbsolute2(params.cwd)) {
|
|
889
|
-
throw
|
|
1400
|
+
throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1401
|
+
}
|
|
1402
|
+
this.lastSessionCwd = params.cwd;
|
|
1403
|
+
if (!hasAnyPiAuthConfigured()) {
|
|
1404
|
+
throw RequestError3.authRequired(
|
|
1405
|
+
{ authMethods: getAuthMethods() },
|
|
1406
|
+
"Configure an API key or log in with an OAuth provider."
|
|
1407
|
+
);
|
|
890
1408
|
}
|
|
891
1409
|
const fileCommands = loadSlashCommands(params.cwd);
|
|
892
1410
|
const session = await this.sessions.create({
|
|
893
1411
|
cwd: params.cwd,
|
|
894
1412
|
mcpServers: params.mcpServers,
|
|
895
1413
|
conn: this.conn,
|
|
896
|
-
fileCommands
|
|
1414
|
+
fileCommands,
|
|
1415
|
+
piCommand: process.env.PI_ACP_PI_COMMAND
|
|
897
1416
|
});
|
|
1417
|
+
let rawModelsCount = 0;
|
|
1418
|
+
try {
|
|
1419
|
+
const data = await session.proc.getAvailableModels();
|
|
1420
|
+
rawModelsCount = Array.isArray(data?.models) ? data.models.length : 0;
|
|
1421
|
+
} catch {
|
|
1422
|
+
}
|
|
1423
|
+
if (rawModelsCount === 0) {
|
|
1424
|
+
try {
|
|
1425
|
+
session.proc.dispose?.();
|
|
1426
|
+
} catch {
|
|
1427
|
+
}
|
|
1428
|
+
throw RequestError3.authRequired(
|
|
1429
|
+
{ authMethods: getAuthMethods() },
|
|
1430
|
+
"Configure an API key or log in with an OAuth provider."
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
898
1433
|
const models = await getModelState(session.proc);
|
|
899
1434
|
const thinking = await getThinkingState(session.proc);
|
|
1435
|
+
const showStartupInfo = booleanEnv("PI_ACP_STARTUP_INFO", true);
|
|
1436
|
+
const preludeText = showStartupInfo ? buildStartupInfo({ cwd: params.cwd, fileCommands }) : "";
|
|
1437
|
+
if (preludeText) session.setStartupInfo(preludeText);
|
|
900
1438
|
const response = {
|
|
901
1439
|
sessionId: session.sessionId,
|
|
902
1440
|
models,
|
|
903
1441
|
modes: thinking,
|
|
904
|
-
_meta: {
|
|
1442
|
+
_meta: {
|
|
1443
|
+
piAcp: {
|
|
1444
|
+
startupInfo: preludeText || null
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
905
1447
|
};
|
|
1448
|
+
if (showStartupInfo) setTimeout(() => session.sendStartupInfoIfPending(), 0);
|
|
906
1449
|
setTimeout(() => {
|
|
907
1450
|
void this.conn.sessionUpdate({
|
|
908
1451
|
sessionId: session.sessionId,
|
|
@@ -1065,8 +1608,8 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1065
1608
|
if (piPath) {
|
|
1066
1609
|
const resolved = realpathSync(piPath);
|
|
1067
1610
|
const pkgRoot = dirname2(dirname2(resolved));
|
|
1068
|
-
const p =
|
|
1069
|
-
if (
|
|
1611
|
+
const p = join5(pkgRoot, "CHANGELOG.md");
|
|
1612
|
+
if (existsSync3(p)) return p;
|
|
1070
1613
|
}
|
|
1071
1614
|
} catch {
|
|
1072
1615
|
}
|
|
@@ -1074,8 +1617,8 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1074
1617
|
const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
|
|
1075
1618
|
const root = String(npmRoot.stdout ?? "").trim();
|
|
1076
1619
|
if (root) {
|
|
1077
|
-
const p =
|
|
1078
|
-
if (
|
|
1620
|
+
const p = join5(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
|
|
1621
|
+
if (existsSync3(p)) return p;
|
|
1079
1622
|
}
|
|
1080
1623
|
} catch {
|
|
1081
1624
|
}
|
|
@@ -1094,7 +1637,7 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1094
1637
|
}
|
|
1095
1638
|
let text = "";
|
|
1096
1639
|
try {
|
|
1097
|
-
text =
|
|
1640
|
+
text = readFileSync6(changelogPath, "utf-8");
|
|
1098
1641
|
} catch (e) {
|
|
1099
1642
|
await this.conn.sessionUpdate({
|
|
1100
1643
|
sessionId: session.sessionId,
|
|
@@ -1120,7 +1663,7 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1120
1663
|
const state = await session.proc.getState();
|
|
1121
1664
|
const sessionFile = typeof state?.sessionFile === "string" ? state.sessionFile : null;
|
|
1122
1665
|
const messageCount = typeof state?.messageCount === "number" ? state.messageCount : 0;
|
|
1123
|
-
if (!sessionFile || messageCount === 0 || !
|
|
1666
|
+
if (!sessionFile || messageCount === 0 || !existsSync3(sessionFile)) {
|
|
1124
1667
|
await this.conn.sessionUpdate({
|
|
1125
1668
|
sessionId: session.sessionId,
|
|
1126
1669
|
update: {
|
|
@@ -1134,7 +1677,7 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1134
1677
|
return { stopReason: "end_turn" };
|
|
1135
1678
|
}
|
|
1136
1679
|
try {
|
|
1137
|
-
const raw =
|
|
1680
|
+
const raw = readFileSync6(sessionFile, "utf-8");
|
|
1138
1681
|
if (raw.trim().length === 0) {
|
|
1139
1682
|
await this.conn.sessionUpdate({
|
|
1140
1683
|
sessionId: session.sessionId,
|
|
@@ -1162,7 +1705,7 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1162
1705
|
return { stopReason: "end_turn" };
|
|
1163
1706
|
}
|
|
1164
1707
|
const safeSessionId = session.sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1165
|
-
const outputPath =
|
|
1708
|
+
const outputPath = join5(session.cwd, `pi-session-${safeSessionId}.html`);
|
|
1166
1709
|
let resultPath = "";
|
|
1167
1710
|
try {
|
|
1168
1711
|
const result2 = await session.proc.exportHtml(outputPath);
|
|
@@ -1251,18 +1794,46 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1251
1794
|
const session = this.sessions.get(params.sessionId);
|
|
1252
1795
|
await session.cancel();
|
|
1253
1796
|
}
|
|
1797
|
+
async unstable_listSessions(params) {
|
|
1798
|
+
const all = listPiSessions();
|
|
1799
|
+
const effectiveCwd = params.cwd ?? this.lastSessionCwd;
|
|
1800
|
+
const filtered = effectiveCwd ? all.filter((s) => s.cwd === effectiveCwd) : all;
|
|
1801
|
+
const offset = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
|
|
1802
|
+
const start = Number.isFinite(offset) && offset > 0 ? offset : 0;
|
|
1803
|
+
const PAGE_SIZE = 50;
|
|
1804
|
+
const page = filtered.slice(start, start + PAGE_SIZE);
|
|
1805
|
+
const sessions = page.map((s) => ({
|
|
1806
|
+
sessionId: s.sessionId,
|
|
1807
|
+
cwd: s.cwd,
|
|
1808
|
+
title: s.title,
|
|
1809
|
+
updatedAt: s.updatedAt
|
|
1810
|
+
}));
|
|
1811
|
+
const nextCursor = start + PAGE_SIZE < filtered.length ? String(start + PAGE_SIZE) : null;
|
|
1812
|
+
return { sessions, nextCursor, _meta: {} };
|
|
1813
|
+
}
|
|
1254
1814
|
async loadSession(params) {
|
|
1255
1815
|
if (!isAbsolute2(params.cwd)) {
|
|
1256
|
-
throw
|
|
1816
|
+
throw RequestError3.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1257
1817
|
}
|
|
1818
|
+
this.lastSessionCwd = params.cwd;
|
|
1258
1819
|
const stored = this.store.get(params.sessionId);
|
|
1259
|
-
|
|
1260
|
-
|
|
1820
|
+
const sessionFile = stored?.sessionFile ?? findPiSessionFile(params.sessionId);
|
|
1821
|
+
if (!sessionFile) {
|
|
1822
|
+
throw RequestError3.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1823
|
+
}
|
|
1824
|
+
let proc;
|
|
1825
|
+
try {
|
|
1826
|
+
proc = await PiRpcProcess.spawn({
|
|
1827
|
+
cwd: params.cwd,
|
|
1828
|
+
sessionPath: sessionFile,
|
|
1829
|
+
piCommand: process.env.PI_ACP_PI_COMMAND
|
|
1830
|
+
});
|
|
1831
|
+
} catch (e) {
|
|
1832
|
+
if (e?.name === "PiRpcSpawnError") {
|
|
1833
|
+
throw RequestError3.internalError({ code: e?.code }, String(e?.message ?? e));
|
|
1834
|
+
}
|
|
1835
|
+
throw e;
|
|
1261
1836
|
}
|
|
1262
|
-
const proc = await PiRpcProcess.spawn({
|
|
1263
|
-
cwd: params.cwd,
|
|
1264
|
-
sessionPath: stored.sessionFile
|
|
1265
|
-
});
|
|
1266
1837
|
const fileCommands = loadSlashCommands(params.cwd);
|
|
1267
1838
|
const session = this.sessions.getOrCreate(params.sessionId, {
|
|
1268
1839
|
cwd: params.cwd,
|
|
@@ -1274,7 +1845,7 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1274
1845
|
this.store.upsert({
|
|
1275
1846
|
sessionId: params.sessionId,
|
|
1276
1847
|
cwd: params.cwd,
|
|
1277
|
-
sessionFile
|
|
1848
|
+
sessionFile
|
|
1278
1849
|
});
|
|
1279
1850
|
const data = await proc.getMessages();
|
|
1280
1851
|
const messages = Array.isArray(data?.messages) ? data.messages : [];
|
|
@@ -1304,13 +1875,45 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1304
1875
|
});
|
|
1305
1876
|
}
|
|
1306
1877
|
}
|
|
1878
|
+
if (role === "toolResult") {
|
|
1879
|
+
const toolName = String(m?.toolName ?? "tool");
|
|
1880
|
+
const toolCallId = String(m?.toolCallId ?? crypto.randomUUID());
|
|
1881
|
+
const isError = Boolean(m?.isError);
|
|
1882
|
+
await this.conn.sessionUpdate({
|
|
1883
|
+
sessionId: session.sessionId,
|
|
1884
|
+
update: {
|
|
1885
|
+
sessionUpdate: "tool_call",
|
|
1886
|
+
toolCallId,
|
|
1887
|
+
title: toolName,
|
|
1888
|
+
kind: toolName === "read" ? "read" : toolName === "write" || toolName === "edit" ? "edit" : "other",
|
|
1889
|
+
status: "completed",
|
|
1890
|
+
rawInput: null,
|
|
1891
|
+
rawOutput: m
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
const text = toolResultToText(m);
|
|
1895
|
+
await this.conn.sessionUpdate({
|
|
1896
|
+
sessionId: session.sessionId,
|
|
1897
|
+
update: {
|
|
1898
|
+
sessionUpdate: "tool_call_update",
|
|
1899
|
+
toolCallId,
|
|
1900
|
+
status: isError ? "failed" : "completed",
|
|
1901
|
+
content: text ? [{ type: "content", content: { type: "text", text } }] : null,
|
|
1902
|
+
rawOutput: m
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1307
1906
|
}
|
|
1308
1907
|
const models = await getModelState(proc);
|
|
1309
1908
|
const thinking = await getThinkingState(proc);
|
|
1310
1909
|
const response = {
|
|
1311
1910
|
models,
|
|
1312
1911
|
modes: thinking,
|
|
1313
|
-
_meta: {
|
|
1912
|
+
_meta: {
|
|
1913
|
+
piAcp: {
|
|
1914
|
+
startupInfo: null
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1314
1917
|
};
|
|
1315
1918
|
setTimeout(() => {
|
|
1316
1919
|
void this.conn.sessionUpdate({
|
|
@@ -1344,7 +1947,7 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1344
1947
|
}
|
|
1345
1948
|
}
|
|
1346
1949
|
if (!provider || !modelId) {
|
|
1347
|
-
throw
|
|
1950
|
+
throw RequestError3.invalidParams(`Unknown modelId: ${params.modelId}`);
|
|
1348
1951
|
}
|
|
1349
1952
|
await session.proc.setModel(provider, modelId);
|
|
1350
1953
|
}
|
|
@@ -1352,7 +1955,7 @@ ${JSON.stringify(stats, null, 2)}`;
|
|
|
1352
1955
|
const session = this.sessions.get(params.sessionId);
|
|
1353
1956
|
const mode = String(params.modeId);
|
|
1354
1957
|
if (!isThinkingLevel(mode)) {
|
|
1355
|
-
throw
|
|
1958
|
+
throw RequestError3.invalidParams(`Unknown modeId: ${mode}`);
|
|
1356
1959
|
}
|
|
1357
1960
|
await session.proc.setThinkingLevel(mode);
|
|
1358
1961
|
void this.conn.sessionUpdate({
|
|
@@ -1422,13 +2025,147 @@ async function getModelState(proc) {
|
|
|
1422
2025
|
currentModelId
|
|
1423
2026
|
};
|
|
1424
2027
|
}
|
|
2028
|
+
function isSemver(v) {
|
|
2029
|
+
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(v);
|
|
2030
|
+
}
|
|
2031
|
+
function compareSemver(a, b) {
|
|
2032
|
+
const pa = a.split(/[.-]/).slice(0, 3).map((n) => Number(n));
|
|
2033
|
+
const pb = b.split(/[.-]/).slice(0, 3).map((n) => Number(n));
|
|
2034
|
+
for (let i = 0; i < 3; i++) {
|
|
2035
|
+
const da = pa[i] ?? 0;
|
|
2036
|
+
const db = pb[i] ?? 0;
|
|
2037
|
+
if (da > db) return 1;
|
|
2038
|
+
if (da < db) return -1;
|
|
2039
|
+
}
|
|
2040
|
+
return 0;
|
|
2041
|
+
}
|
|
2042
|
+
function buildStartupInfo(opts) {
|
|
2043
|
+
void opts.fileCommands;
|
|
2044
|
+
const md = [];
|
|
2045
|
+
let updateNotice = null;
|
|
2046
|
+
try {
|
|
2047
|
+
const piVersion = spawnSync("pi", ["--version"], { encoding: "utf-8" });
|
|
2048
|
+
const installed = String(piVersion.stdout ?? "").trim().replace(/^v/i, "");
|
|
2049
|
+
if (installed) {
|
|
2050
|
+
md.push(`pi v${installed}`);
|
|
2051
|
+
md.push("---");
|
|
2052
|
+
md.push("");
|
|
2053
|
+
try {
|
|
2054
|
+
const latestRes = spawnSync("npm", ["view", "@mariozechner/pi-coding-agent", "version"], {
|
|
2055
|
+
encoding: "utf-8",
|
|
2056
|
+
timeout: 800
|
|
2057
|
+
});
|
|
2058
|
+
const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
|
|
2059
|
+
if (latest && isSemver(latest) && isSemver(installed) && compareSemver(latest, installed) > 0) {
|
|
2060
|
+
updateNotice = `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
|
|
2061
|
+
}
|
|
2062
|
+
} catch {
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
} catch {
|
|
2066
|
+
}
|
|
2067
|
+
const addSection = (title, items) => {
|
|
2068
|
+
const cleaned = items.map((s) => s.trim()).filter(Boolean);
|
|
2069
|
+
if (!cleaned.length) return;
|
|
2070
|
+
md.push(`## ${title}`);
|
|
2071
|
+
for (const item of cleaned) md.push(`- ${item}`);
|
|
2072
|
+
md.push("");
|
|
2073
|
+
};
|
|
2074
|
+
const contextItems = [];
|
|
2075
|
+
const contextPath = join5(opts.cwd, "AGENTS.md");
|
|
2076
|
+
if (existsSync3(contextPath)) contextItems.push(contextPath);
|
|
2077
|
+
addSection("Context", contextItems);
|
|
2078
|
+
const skillsItems = [];
|
|
2079
|
+
const pushSkillFromRoot = (root) => {
|
|
2080
|
+
try {
|
|
2081
|
+
for (const e of readdirSync3(root)) {
|
|
2082
|
+
const p = join5(root, e);
|
|
2083
|
+
try {
|
|
2084
|
+
const st = statSync2(p);
|
|
2085
|
+
if (st.isFile() && e.toLowerCase().endsWith(".md")) {
|
|
2086
|
+
skillsItems.push(p);
|
|
2087
|
+
}
|
|
2088
|
+
} catch {
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
const stack = [root];
|
|
2092
|
+
while (stack.length) {
|
|
2093
|
+
const dir = stack.pop();
|
|
2094
|
+
let entries = [];
|
|
2095
|
+
try {
|
|
2096
|
+
entries = readdirSync3(dir);
|
|
2097
|
+
} catch {
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
for (const name of entries) {
|
|
2101
|
+
if (name === "node_modules" || name === ".git") continue;
|
|
2102
|
+
const p = join5(dir, name);
|
|
2103
|
+
let st;
|
|
2104
|
+
try {
|
|
2105
|
+
st = statSync2(p);
|
|
2106
|
+
} catch {
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
if (st.isDirectory()) {
|
|
2110
|
+
stack.push(p);
|
|
2111
|
+
} else if (st.isFile() && name === "SKILL.md") {
|
|
2112
|
+
skillsItems.push(p);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
} catch {
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
const globalSkillsDir = join5(process.env.HOME ?? "", ".pi", "agent", "skills");
|
|
2120
|
+
pushSkillFromRoot(globalSkillsDir);
|
|
2121
|
+
const projectSkillsDir = join5(opts.cwd, ".pi", "skills");
|
|
2122
|
+
pushSkillFromRoot(projectSkillsDir);
|
|
2123
|
+
addSection("Skills", skillsItems);
|
|
2124
|
+
const promptsItems = [];
|
|
2125
|
+
const promptsDir = join5(process.env.HOME ?? "", ".pi", "agent", "prompts");
|
|
2126
|
+
try {
|
|
2127
|
+
const prompts = readdirSync3(promptsDir).filter((f) => f.endsWith(".md"));
|
|
2128
|
+
for (const f of prompts) promptsItems.push(`/${basename(f, ".md")}`);
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
addSection("Prompts", promptsItems);
|
|
2132
|
+
const extItems = [];
|
|
2133
|
+
const extDir = join5(process.env.HOME ?? "", ".pi", "agent", "extensions");
|
|
2134
|
+
try {
|
|
2135
|
+
const exts = readdirSync3(extDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
|
|
2136
|
+
for (const f of exts) extItems.push(join5(extDir, f));
|
|
2137
|
+
} catch {
|
|
2138
|
+
}
|
|
2139
|
+
try {
|
|
2140
|
+
const settingsPath = join5(process.env.HOME ?? "", ".pi", "agent", "settings.json");
|
|
2141
|
+
const settings = JSON.parse(readFileSync6(settingsPath, "utf-8"));
|
|
2142
|
+
const pkgs = Array.isArray(settings?.packages) ? settings.packages : [];
|
|
2143
|
+
for (const pkg2 of pkgs) {
|
|
2144
|
+
const s = String(pkg2);
|
|
2145
|
+
if (s.startsWith("npm:")) {
|
|
2146
|
+
extItems.push(`${s}
|
|
2147
|
+
- index.ts`);
|
|
2148
|
+
} else {
|
|
2149
|
+
extItems.push(s);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
} catch {
|
|
2153
|
+
}
|
|
2154
|
+
addSection("Extensions", extItems);
|
|
2155
|
+
if (updateNotice) {
|
|
2156
|
+
md.push("---");
|
|
2157
|
+
md.push(updateNotice);
|
|
2158
|
+
md.push("");
|
|
2159
|
+
}
|
|
2160
|
+
return md.join("\n").trim() + "\n";
|
|
2161
|
+
}
|
|
1425
2162
|
function readNearestPackageJson(metaUrl) {
|
|
1426
2163
|
try {
|
|
1427
2164
|
let dir = dirname2(fileURLToPath(metaUrl));
|
|
1428
2165
|
for (let i = 0; i < 6; i++) {
|
|
1429
|
-
const p =
|
|
1430
|
-
if (
|
|
1431
|
-
const json = JSON.parse(
|
|
2166
|
+
const p = join5(dir, "package.json");
|
|
2167
|
+
if (existsSync3(p)) {
|
|
2168
|
+
const json = JSON.parse(readFileSync6(p, "utf-8"));
|
|
1432
2169
|
return { name: json?.name, version: json?.version };
|
|
1433
2170
|
}
|
|
1434
2171
|
dir = dirname2(dir);
|
|
@@ -1439,13 +2176,31 @@ function readNearestPackageJson(metaUrl) {
|
|
|
1439
2176
|
}
|
|
1440
2177
|
|
|
1441
2178
|
// src/index.ts
|
|
2179
|
+
if (process.argv.includes("--terminal-login")) {
|
|
2180
|
+
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
2181
|
+
const cmd = process.env.PI_ACP_PI_COMMAND ?? "pi";
|
|
2182
|
+
const res = spawnSync2(cmd, [], { stdio: "inherit", env: process.env });
|
|
2183
|
+
if (res.error && res.error.code === "ENOENT") {
|
|
2184
|
+
process.stderr.write(
|
|
2185
|
+
`pi-acp: could not start pi (command not found: ${cmd}). Install it via \`npm install -g @mariozechner/pi-coding-agent\` or ensure \`pi\` is on your PATH.
|
|
2186
|
+
`
|
|
2187
|
+
);
|
|
2188
|
+
process.exit(1);
|
|
2189
|
+
}
|
|
2190
|
+
process.exit(typeof res.status === "number" ? res.status : 1);
|
|
2191
|
+
}
|
|
1442
2192
|
var input = new WritableStream({
|
|
1443
2193
|
write(chunk) {
|
|
1444
|
-
return new Promise((resolve2
|
|
1445
|
-
process.stdout.
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
2194
|
+
return new Promise((resolve2) => {
|
|
2195
|
+
if (process.stdout.destroyed || !process.stdout.writable) return resolve2();
|
|
2196
|
+
try {
|
|
2197
|
+
process.stdout.write(chunk, (err) => {
|
|
2198
|
+
void err;
|
|
2199
|
+
resolve2();
|
|
2200
|
+
});
|
|
2201
|
+
} catch {
|
|
2202
|
+
resolve2();
|
|
2203
|
+
}
|
|
1449
2204
|
});
|
|
1450
2205
|
}
|
|
1451
2206
|
});
|
|
@@ -1461,4 +2216,10 @@ new AgentSideConnection((conn) => new PiAcpAgent(conn), stream);
|
|
|
1461
2216
|
process.stdin.resume();
|
|
1462
2217
|
process.on("SIGINT", () => process.exit(0));
|
|
1463
2218
|
process.on("SIGTERM", () => process.exit(0));
|
|
2219
|
+
process.stdout.on("error", () => {
|
|
2220
|
+
try {
|
|
2221
|
+
process.exit(0);
|
|
2222
|
+
} catch {
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
1464
2225
|
//# sourceMappingURL=index.js.map
|