@victor-software-house/pi-acp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/dist/index.mjs +276 -316
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -4,8 +4,7 @@ import { AgentSideConnection, RequestError, ndJsonStream } from "@agentclientpro
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
6
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import { SessionManager, VERSION, createAgentSession } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { SessionManager, createAgentSession } from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import * as z from "zod";
|
|
10
9
|
//#region src/acp/auth.ts
|
|
11
10
|
const AUTH_METHOD_ID = "pi_terminal_login";
|
|
@@ -19,13 +18,10 @@ function buildAuthMethods(opts) {
|
|
|
19
18
|
args: ["--terminal-login"],
|
|
20
19
|
env: {}
|
|
21
20
|
};
|
|
22
|
-
if (supportsTerminalAuthMeta) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
label: "Launch pi"
|
|
27
|
-
} };
|
|
28
|
-
}
|
|
21
|
+
if (supportsTerminalAuthMeta) method._meta = { "terminal-auth": {
|
|
22
|
+
...resolveTerminalLaunchCommand(),
|
|
23
|
+
label: "Launch pi"
|
|
24
|
+
} };
|
|
29
25
|
return [method];
|
|
30
26
|
}
|
|
31
27
|
function resolveTerminalLaunchCommand() {
|
|
@@ -97,6 +93,102 @@ function parseClientCapabilities(caps) {
|
|
|
97
93
|
};
|
|
98
94
|
}
|
|
99
95
|
//#endregion
|
|
96
|
+
//#region src/acp/model-alias.ts
|
|
97
|
+
/**
|
|
98
|
+
* Tokenize a string: split on non-alphanumeric, lowercase, strip "claude".
|
|
99
|
+
*/
|
|
100
|
+
function tokenize(input) {
|
|
101
|
+
return input.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t !== "" && t !== "claude");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Extract a context hint in square brackets, e.g. "opus[1m]" -> { base: "opus", hint: "1m" }.
|
|
105
|
+
*/
|
|
106
|
+
function extractContextHint(input) {
|
|
107
|
+
const match = /^(.+?)\[([^\]]+)\]$/.exec(input);
|
|
108
|
+
if (match !== null && match[1] !== void 0 && match[2] !== void 0) return {
|
|
109
|
+
base: match[1],
|
|
110
|
+
hint: match[2]
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
base: input,
|
|
114
|
+
hint: null
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Check if a string is purely numeric. */
|
|
118
|
+
function isNumeric(s) {
|
|
119
|
+
return /^\d+$/.test(s);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Score how well a model matches the given preference tokens.
|
|
123
|
+
*
|
|
124
|
+
* Returns a score >= 0 (higher is better), or -1 for no match.
|
|
125
|
+
* Requires at least one non-numeric token to match to avoid false positives
|
|
126
|
+
* from bare version numbers (e.g. "4" matching model version suffixes).
|
|
127
|
+
*/
|
|
128
|
+
function scoreModel(model, prefTokens, hint) {
|
|
129
|
+
const modelStr = `${model.provider}/${model.id}/${model.name ?? ""}`.toLowerCase();
|
|
130
|
+
const modelTokens = tokenize(modelStr);
|
|
131
|
+
let matched = 0;
|
|
132
|
+
let hasNonNumericMatch = false;
|
|
133
|
+
for (const pt of prefTokens) if (modelTokens.some((mt) => mt.includes(pt) || pt.includes(mt))) {
|
|
134
|
+
matched++;
|
|
135
|
+
if (!isNumeric(pt)) hasNonNumericMatch = true;
|
|
136
|
+
}
|
|
137
|
+
if (matched === 0) return -1;
|
|
138
|
+
if (!hasNonNumericMatch) return -1;
|
|
139
|
+
let score = matched / prefTokens.length;
|
|
140
|
+
if (hint !== null && modelStr.includes(hint.toLowerCase())) score += .5;
|
|
141
|
+
const pref = prefTokens.join("");
|
|
142
|
+
if (model.id.toLowerCase().includes(pref)) score += .25;
|
|
143
|
+
return score;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a user-friendly model preference to a concrete model.
|
|
147
|
+
*
|
|
148
|
+
* Matching strategy (in order):
|
|
149
|
+
* 1. Exact match on "provider/id"
|
|
150
|
+
* 2. Exact match on "id" alone
|
|
151
|
+
* 3. Tokenized scored match with optional context hint
|
|
152
|
+
*
|
|
153
|
+
* Returns null if no model matches.
|
|
154
|
+
*/
|
|
155
|
+
function resolveModelPreference(models, preference) {
|
|
156
|
+
const trimmed = preference.trim();
|
|
157
|
+
if (trimmed === "") return null;
|
|
158
|
+
if (trimmed.includes("/")) {
|
|
159
|
+
const [p, ...rest] = trimmed.split("/");
|
|
160
|
+
const provider = p ?? "";
|
|
161
|
+
const id = rest.join("/");
|
|
162
|
+
const exact = models.find((m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === id.toLowerCase());
|
|
163
|
+
if (exact !== void 0) return {
|
|
164
|
+
provider: exact.provider,
|
|
165
|
+
id: exact.id
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const byId = models.find((m) => m.id.toLowerCase() === trimmed.toLowerCase());
|
|
169
|
+
if (byId !== void 0) return {
|
|
170
|
+
provider: byId.provider,
|
|
171
|
+
id: byId.id
|
|
172
|
+
};
|
|
173
|
+
const { base, hint } = extractContextHint(trimmed);
|
|
174
|
+
const prefTokens = tokenize(base);
|
|
175
|
+
if (prefTokens.length === 0) return null;
|
|
176
|
+
let bestModel = null;
|
|
177
|
+
let bestScore = -1;
|
|
178
|
+
for (const model of models) {
|
|
179
|
+
const s = scoreModel(model, prefTokens, hint);
|
|
180
|
+
if (s > bestScore) {
|
|
181
|
+
bestScore = s;
|
|
182
|
+
bestModel = model;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (bestModel === null || bestScore < .5) return null;
|
|
186
|
+
return {
|
|
187
|
+
provider: bestModel.provider,
|
|
188
|
+
id: bestModel.id
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
100
192
|
//#region src/acp/pi-settings.ts
|
|
101
193
|
/**
|
|
102
194
|
* Read pi settings from global and project config files.
|
|
@@ -149,12 +241,6 @@ function skillCommandsEnabled(cwd) {
|
|
|
149
241
|
if (typeof settings.skills?.enableSkillCommands === "boolean") return settings.skills.enableSkillCommands;
|
|
150
242
|
return true;
|
|
151
243
|
}
|
|
152
|
-
function quietStartupEnabled(cwd) {
|
|
153
|
-
const settings = resolvedSettings(cwd);
|
|
154
|
-
if (typeof settings.quietStartup === "boolean") return settings.quietStartup;
|
|
155
|
-
if (typeof settings.quietStart === "boolean") return settings.quietStart;
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
244
|
//#endregion
|
|
159
245
|
//#region src/acp/translate/tool-content.ts
|
|
160
246
|
const textBlockSchema = z.object({
|
|
@@ -249,13 +335,31 @@ function extractContentBlocks(result) {
|
|
|
249
335
|
return blocks;
|
|
250
336
|
}
|
|
251
337
|
/**
|
|
252
|
-
*
|
|
338
|
+
* Find the longest consecutive backtick sequence in a string.
|
|
339
|
+
*/
|
|
340
|
+
function longestBacktickRun(text) {
|
|
341
|
+
let max = 0;
|
|
342
|
+
let current = 0;
|
|
343
|
+
for (const ch of text) if (ch === "`") {
|
|
344
|
+
current++;
|
|
345
|
+
if (current > max) max = current;
|
|
346
|
+
} else current = 0;
|
|
347
|
+
return max;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Wrap text in a dynamically-sized backtick fence to prevent markdown rendering.
|
|
253
351
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
352
|
+
* Instead of character-level escaping (which fails on files containing backtick
|
|
353
|
+
* sequences, indented code blocks, blockquotes, and list markers), this wraps
|
|
354
|
+
* the entire text in a backtick fence whose length exceeds any backtick sequence
|
|
355
|
+
* in the content. This approach is simpler and strictly more correct (following
|
|
356
|
+
* the claude-agent-acp pattern).
|
|
256
357
|
*/
|
|
257
358
|
function markdownEscape(text) {
|
|
258
|
-
|
|
359
|
+
if (text === "") return "";
|
|
360
|
+
const fenceLen = Math.max(3, longestBacktickRun(text) + 1);
|
|
361
|
+
const fence = "`".repeat(fenceLen);
|
|
362
|
+
return `${fence}\n${text.endsWith("\n") ? text.slice(0, -1) : text}\n${fence}`;
|
|
259
363
|
}
|
|
260
364
|
/**
|
|
261
365
|
* Format tool output into `ToolCallContent[]` by tool name.
|
|
@@ -363,6 +467,19 @@ function wrapStreamingBashOutput(text) {
|
|
|
363
467
|
return `\`\`\`console\n${text}\n\`\`\``;
|
|
364
468
|
}
|
|
365
469
|
//#endregion
|
|
470
|
+
//#region src/acp/unreachable.ts
|
|
471
|
+
/**
|
|
472
|
+
* Exhaustive switch/case helper.
|
|
473
|
+
*
|
|
474
|
+
* Writes unknown values to stderr instead of silently ignoring them, aiding
|
|
475
|
+
* debugging when the pi SDK adds new event types. Never write to stdout: it
|
|
476
|
+
* carries the ACP NDJSON stream and any other byte poisons the protocol.
|
|
477
|
+
*/
|
|
478
|
+
function unreachable(value, context) {
|
|
479
|
+
const label = context !== void 0 ? `[${context}] ` : "";
|
|
480
|
+
process.stderr.write(`${label}Unhandled value: ${String(value)}\n`);
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
366
483
|
//#region src/acp/session.ts
|
|
367
484
|
function findUniqueLineNumber(text, needle) {
|
|
368
485
|
if (!needle) return void 0;
|
|
@@ -528,11 +645,12 @@ var PiAcpSession = class {
|
|
|
528
645
|
mcpServers;
|
|
529
646
|
piSession;
|
|
530
647
|
supportsTerminalOutput;
|
|
531
|
-
startupInfo = null;
|
|
532
|
-
startupInfoSent = false;
|
|
533
648
|
conn;
|
|
534
649
|
cancelRequested = false;
|
|
650
|
+
promptRunning = false;
|
|
535
651
|
pendingTurn = null;
|
|
652
|
+
/** Queued prompts waiting for the active turn to complete. */
|
|
653
|
+
pendingMessages = [];
|
|
536
654
|
currentToolCalls = /* @__PURE__ */ new Map();
|
|
537
655
|
/** Map of toolCallId -> toolName for streaming updates (Phase 5). */
|
|
538
656
|
toolCallNames = /* @__PURE__ */ new Map();
|
|
@@ -553,21 +671,25 @@ var PiAcpSession = class {
|
|
|
553
671
|
this.unsubscribe?.();
|
|
554
672
|
this.piSession.dispose();
|
|
555
673
|
}
|
|
556
|
-
|
|
557
|
-
this.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
content: {
|
|
565
|
-
type: "text",
|
|
566
|
-
text: this.startupInfo
|
|
567
|
-
}
|
|
674
|
+
async prompt(message, images = []) {
|
|
675
|
+
if (this.promptRunning) return new Promise((resolve, reject) => {
|
|
676
|
+
this.pendingMessages.push({
|
|
677
|
+
message,
|
|
678
|
+
images,
|
|
679
|
+
resolve,
|
|
680
|
+
reject
|
|
681
|
+
});
|
|
568
682
|
});
|
|
683
|
+
return this.executePrompt(message, images);
|
|
569
684
|
}
|
|
570
|
-
async
|
|
685
|
+
async cancel() {
|
|
686
|
+
this.cancelRequested = true;
|
|
687
|
+
for (const pending of this.pendingMessages) pending.resolve("cancelled");
|
|
688
|
+
this.pendingMessages = [];
|
|
689
|
+
await this.piSession.abort();
|
|
690
|
+
}
|
|
691
|
+
executePrompt(message, images) {
|
|
692
|
+
this.promptRunning = true;
|
|
571
693
|
const turnPromise = new Promise((resolve, reject) => {
|
|
572
694
|
this.cancelRequested = false;
|
|
573
695
|
this.pendingTurn = {
|
|
@@ -585,9 +707,17 @@ var PiAcpSession = class {
|
|
|
585
707
|
});
|
|
586
708
|
return turnPromise;
|
|
587
709
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
710
|
+
/**
|
|
711
|
+
* Dequeue and execute the next pending prompt, if any.
|
|
712
|
+
* Called after a turn completes.
|
|
713
|
+
*/
|
|
714
|
+
dequeueNextPrompt() {
|
|
715
|
+
const next = this.pendingMessages.shift();
|
|
716
|
+
if (next === void 0) {
|
|
717
|
+
this.promptRunning = false;
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
this.executePrompt(next.message, next.images).then(next.resolve, next.reject);
|
|
591
721
|
}
|
|
592
722
|
wasCancelRequested() {
|
|
593
723
|
return this.cancelRequested;
|
|
@@ -622,7 +752,9 @@ var PiAcpSession = class {
|
|
|
622
752
|
case "agent_end":
|
|
623
753
|
this.handleAgentEnd();
|
|
624
754
|
break;
|
|
625
|
-
default:
|
|
755
|
+
default:
|
|
756
|
+
unreachable(ev, "handlePiEvent");
|
|
757
|
+
break;
|
|
626
758
|
}
|
|
627
759
|
}
|
|
628
760
|
handleMessageUpdate(ame) {
|
|
@@ -792,11 +924,6 @@ var PiAcpSession = class {
|
|
|
792
924
|
}, ...formatted];
|
|
793
925
|
}
|
|
794
926
|
} catch {}
|
|
795
|
-
const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_exit: {
|
|
796
|
-
terminal_id: toolCallId,
|
|
797
|
-
exit_code: extractExitCode(result),
|
|
798
|
-
signal: null
|
|
799
|
-
} } : void 0);
|
|
800
927
|
if (content === null) {
|
|
801
928
|
const formatted = formatToolContent(toolName, result, isError);
|
|
802
929
|
content = formatted.length > 0 ? formatted : null;
|
|
@@ -811,6 +938,24 @@ var PiAcpSession = class {
|
|
|
811
938
|
}
|
|
812
939
|
}];
|
|
813
940
|
}
|
|
941
|
+
if (this.supportsTerminalOutput && isTerminalTool(toolName)) {
|
|
942
|
+
const outputText = extractStreamingText(result);
|
|
943
|
+
if (outputText !== "") this.emit({
|
|
944
|
+
sessionUpdate: "tool_call_update",
|
|
945
|
+
toolCallId,
|
|
946
|
+
status: "in_progress",
|
|
947
|
+
_meta: buildToolMeta(toolName, { terminal_output: {
|
|
948
|
+
terminal_id: toolCallId,
|
|
949
|
+
data: outputText
|
|
950
|
+
} }),
|
|
951
|
+
rawOutput: result
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
const meta = buildToolMeta(toolName, this.supportsTerminalOutput && isTerminalTool(toolName) ? { terminal_exit: {
|
|
955
|
+
terminal_id: toolCallId,
|
|
956
|
+
exit_code: extractExitCode(result),
|
|
957
|
+
signal: null
|
|
958
|
+
} } : void 0);
|
|
814
959
|
this.emit({
|
|
815
960
|
sessionUpdate: "tool_call_update",
|
|
816
961
|
toolCallId,
|
|
@@ -830,6 +975,7 @@ var PiAcpSession = class {
|
|
|
830
975
|
this.lastAssistantStopReason = null;
|
|
831
976
|
this.pendingTurn?.resolve(reason);
|
|
832
977
|
this.pendingTurn = null;
|
|
978
|
+
this.dequeueNextPrompt();
|
|
833
979
|
});
|
|
834
980
|
}
|
|
835
981
|
/**
|
|
@@ -967,122 +1113,58 @@ function acpPromptToPiMessage(blocks) {
|
|
|
967
1113
|
};
|
|
968
1114
|
}
|
|
969
1115
|
//#endregion
|
|
970
|
-
//#region
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
*
|
|
974
|
-
* Checks three sources:
|
|
975
|
-
* 1. auth.json (API keys, OAuth credentials)
|
|
976
|
-
* 2. models.json custom provider apiKey entries
|
|
977
|
-
* 3. Known provider environment variables
|
|
978
|
-
*/
|
|
979
|
-
const modelsConfigSchema = z.object({ providers: z.record(z.string().trim(), z.object({ apiKey: z.string().trim().optional() })).optional() });
|
|
980
|
-
function agentDir() {
|
|
981
|
-
const env = process.env.PI_CODING_AGENT_DIR;
|
|
982
|
-
if (env === void 0) return join(homedir(), ".pi", "agent");
|
|
983
|
-
if (env === "~") return homedir();
|
|
984
|
-
if (env.startsWith("~/")) return homedir() + env.slice(1);
|
|
985
|
-
return env;
|
|
986
|
-
}
|
|
987
|
-
function readJsonFile(path) {
|
|
988
|
-
try {
|
|
989
|
-
if (!existsSync(path)) return null;
|
|
990
|
-
const raw = readFileSync(path, "utf-8").trim();
|
|
991
|
-
if (!raw) return null;
|
|
992
|
-
return JSON.parse(raw);
|
|
993
|
-
} catch {
|
|
994
|
-
return null;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
function hasAuthJson() {
|
|
998
|
-
const data = readJsonFile(join(agentDir(), "auth.json"));
|
|
999
|
-
return typeof data === "object" && data !== null && Object.keys(data).length > 0;
|
|
1000
|
-
}
|
|
1001
|
-
function hasCustomProviderKey() {
|
|
1002
|
-
const raw = readJsonFile(join(agentDir(), "models.json"));
|
|
1003
|
-
const result = modelsConfigSchema.safeParse(raw);
|
|
1004
|
-
if (!result.success || !result.data.providers) return false;
|
|
1005
|
-
return Object.values(result.data.providers).some((provider) => typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0);
|
|
1006
|
-
}
|
|
1007
|
-
/** Environment variables that indicate a configured provider API key. */
|
|
1008
|
-
const PROVIDER_ENV_VARS = [
|
|
1009
|
-
"ANTHROPIC_API_KEY",
|
|
1010
|
-
"ANTHROPIC_OAUTH_TOKEN",
|
|
1011
|
-
"OPENAI_API_KEY",
|
|
1012
|
-
"AZURE_OPENAI_API_KEY",
|
|
1013
|
-
"GEMINI_API_KEY",
|
|
1014
|
-
"GROQ_API_KEY",
|
|
1015
|
-
"CEREBRAS_API_KEY",
|
|
1016
|
-
"XAI_API_KEY",
|
|
1017
|
-
"OPENROUTER_API_KEY",
|
|
1018
|
-
"AI_GATEWAY_API_KEY",
|
|
1019
|
-
"ZAI_API_KEY",
|
|
1020
|
-
"MISTRAL_API_KEY",
|
|
1021
|
-
"MINIMAX_API_KEY",
|
|
1022
|
-
"MINIMAX_CN_API_KEY",
|
|
1023
|
-
"HF_TOKEN",
|
|
1024
|
-
"OPENCODE_API_KEY",
|
|
1025
|
-
"KIMI_API_KEY",
|
|
1026
|
-
"COPILOT_GITHUB_TOKEN",
|
|
1027
|
-
"GH_TOKEN",
|
|
1028
|
-
"GITHUB_TOKEN"
|
|
1029
|
-
];
|
|
1030
|
-
function hasProviderEnvVar() {
|
|
1031
|
-
return PROVIDER_ENV_VARS.some((key) => {
|
|
1032
|
-
const val = process.env[key];
|
|
1033
|
-
return typeof val === "string" && val.trim().length > 0;
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
function hasPiAuthConfigured() {
|
|
1037
|
-
return hasAuthJson() || hasCustomProviderKey() || hasProviderEnvVar();
|
|
1038
|
-
}
|
|
1116
|
+
//#region package.json
|
|
1117
|
+
var name = "@victor-software-house/pi-acp";
|
|
1118
|
+
var version = "0.5.0";
|
|
1039
1119
|
//#endregion
|
|
1040
1120
|
//#region src/acp/agent.ts
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1121
|
+
/** Builtin ACP slash commands handled directly by the adapter. */
|
|
1122
|
+
const BUILTIN_COMMANDS = [
|
|
1123
|
+
{
|
|
1124
|
+
name: "compact",
|
|
1125
|
+
description: "Manually compact the session context",
|
|
1126
|
+
input: { hint: "optional custom instructions" }
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
name: "autocompact",
|
|
1130
|
+
description: "Toggle automatic context compaction",
|
|
1131
|
+
input: { hint: "on|off|toggle" }
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
name: "export",
|
|
1135
|
+
description: "Export session to an HTML file in the session cwd"
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
name: "session",
|
|
1139
|
+
description: "Show session stats (messages, tokens, cost, session file)"
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
name: "name",
|
|
1143
|
+
description: "Set session display name",
|
|
1144
|
+
input: { hint: "<name>" }
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: "steering",
|
|
1148
|
+
description: "Get/set pi steering message delivery mode",
|
|
1149
|
+
input: { hint: "(no args to show) all | one-at-a-time" }
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
name: "follow-up",
|
|
1153
|
+
description: "Get/set pi follow-up message delivery mode",
|
|
1154
|
+
input: { hint: "(no args to show) all | one-at-a-time" }
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
name: "changelog",
|
|
1158
|
+
description: "Show pi changelog"
|
|
1159
|
+
}
|
|
1160
|
+
];
|
|
1161
|
+
/**
|
|
1162
|
+
* Deduplicate commands by name. First occurrence wins.
|
|
1163
|
+
*/
|
|
1164
|
+
function deduplicateCommands(commands) {
|
|
1084
1165
|
const seen = /* @__PURE__ */ new Set();
|
|
1085
|
-
|
|
1166
|
+
const out = [];
|
|
1167
|
+
for (const c of commands) {
|
|
1086
1168
|
if (seen.has(c.name)) continue;
|
|
1087
1169
|
seen.add(c.name);
|
|
1088
1170
|
out.push(c);
|
|
@@ -1113,7 +1195,6 @@ function truncateSessionTitle(text) {
|
|
|
1113
1195
|
if (oneLine.length <= SESSION_TITLE_MAX) return oneLine;
|
|
1114
1196
|
return `${oneLine.slice(0, SESSION_TITLE_MAX - 1)}…`;
|
|
1115
1197
|
}
|
|
1116
|
-
const pkg = readNearestPackageJson(import.meta.url);
|
|
1117
1198
|
var PiAcpAgent = class {
|
|
1118
1199
|
conn;
|
|
1119
1200
|
sessions = new SessionManager$1();
|
|
@@ -1138,9 +1219,9 @@ var PiAcpAgent = class {
|
|
|
1138
1219
|
return {
|
|
1139
1220
|
protocolVersion: requested === supportedVersion ? requested : supportedVersion,
|
|
1140
1221
|
agentInfo: {
|
|
1141
|
-
name
|
|
1222
|
+
name,
|
|
1142
1223
|
title: "pi ACP adapter",
|
|
1143
|
-
version
|
|
1224
|
+
version
|
|
1144
1225
|
},
|
|
1145
1226
|
authMethods: buildAuthMethods({ supportsTerminalAuthMeta: this.clientCapabilities.terminalAuth }),
|
|
1146
1227
|
agentCapabilities: {
|
|
@@ -1165,7 +1246,6 @@ var PiAcpAgent = class {
|
|
|
1165
1246
|
}
|
|
1166
1247
|
async newSession(params) {
|
|
1167
1248
|
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1168
|
-
if (!hasPiAuthConfigured()) throw RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
|
|
1169
1249
|
let result;
|
|
1170
1250
|
try {
|
|
1171
1251
|
result = await createAgentSession({ cwd: params.cwd });
|
|
@@ -1192,24 +1272,9 @@ var PiAcpAgent = class {
|
|
|
1192
1272
|
supportsTerminalOutput: this.clientCapabilities.terminalOutput
|
|
1193
1273
|
});
|
|
1194
1274
|
this.sessions.register(session);
|
|
1195
|
-
const quietStartup = quietStartupEnabled(params.cwd);
|
|
1196
|
-
const updateNotice = buildUpdateNotice();
|
|
1197
|
-
const preludeText = quietStartup ? updateNotice !== null ? `${updateNotice}\n` : "" : buildStartupInfo({
|
|
1198
|
-
cwd: params.cwd,
|
|
1199
|
-
updateNotice
|
|
1200
|
-
});
|
|
1201
|
-
if (preludeText) session.setStartupInfo(preludeText);
|
|
1202
1275
|
const modes = buildThinkingModes(piSession);
|
|
1203
1276
|
const models = buildModelState(piSession);
|
|
1204
1277
|
const configOptions = buildConfigOptions(modes, models);
|
|
1205
|
-
const response = {
|
|
1206
|
-
sessionId: session.sessionId,
|
|
1207
|
-
configOptions,
|
|
1208
|
-
modes,
|
|
1209
|
-
models,
|
|
1210
|
-
_meta: { piAcp: { startupInfo: preludeText || null } }
|
|
1211
|
-
};
|
|
1212
|
-
if (preludeText) setTimeout(() => session.sendStartupInfoIfPending(), 0);
|
|
1213
1278
|
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
1214
1279
|
setTimeout(() => {
|
|
1215
1280
|
(async () => {
|
|
@@ -1219,13 +1284,18 @@ var PiAcpAgent = class {
|
|
|
1219
1284
|
sessionId: session.sessionId,
|
|
1220
1285
|
update: {
|
|
1221
1286
|
sessionUpdate: "available_commands_update",
|
|
1222
|
-
availableCommands:
|
|
1287
|
+
availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
|
|
1223
1288
|
}
|
|
1224
1289
|
});
|
|
1225
1290
|
} catch {}
|
|
1226
1291
|
})();
|
|
1227
1292
|
}, 0);
|
|
1228
|
-
return
|
|
1293
|
+
return {
|
|
1294
|
+
sessionId: session.sessionId,
|
|
1295
|
+
configOptions,
|
|
1296
|
+
modes,
|
|
1297
|
+
models
|
|
1298
|
+
};
|
|
1229
1299
|
}
|
|
1230
1300
|
async authenticate(_params) {
|
|
1231
1301
|
return {};
|
|
@@ -1453,7 +1523,7 @@ var PiAcpAgent = class {
|
|
|
1453
1523
|
sessionId: session.sessionId,
|
|
1454
1524
|
update: {
|
|
1455
1525
|
sessionUpdate: "available_commands_update",
|
|
1456
|
-
availableCommands:
|
|
1526
|
+
availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
|
|
1457
1527
|
}
|
|
1458
1528
|
});
|
|
1459
1529
|
} catch {}
|
|
@@ -1462,16 +1532,15 @@ var PiAcpAgent = class {
|
|
|
1462
1532
|
return {
|
|
1463
1533
|
configOptions,
|
|
1464
1534
|
modes,
|
|
1465
|
-
models
|
|
1466
|
-
_meta: { piAcp: { startupInfo: null } }
|
|
1535
|
+
models
|
|
1467
1536
|
};
|
|
1468
1537
|
}
|
|
1469
|
-
async
|
|
1538
|
+
async closeSession(params) {
|
|
1470
1539
|
if (this.sessions.maybeGet(params.sessionId) === void 0) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1471
1540
|
this.sessions.close(params.sessionId);
|
|
1472
1541
|
return {};
|
|
1473
1542
|
}
|
|
1474
|
-
async
|
|
1543
|
+
async resumeSession(params) {
|
|
1475
1544
|
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1476
1545
|
const existing = this.sessions.maybeGet(params.sessionId);
|
|
1477
1546
|
if (existing !== void 0) {
|
|
@@ -1518,7 +1587,7 @@ var PiAcpAgent = class {
|
|
|
1518
1587
|
sessionId: session.sessionId,
|
|
1519
1588
|
update: {
|
|
1520
1589
|
sessionUpdate: "available_commands_update",
|
|
1521
|
-
availableCommands:
|
|
1590
|
+
availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
|
|
1522
1591
|
}
|
|
1523
1592
|
});
|
|
1524
1593
|
} catch {}
|
|
@@ -1571,7 +1640,7 @@ var PiAcpAgent = class {
|
|
|
1571
1640
|
sessionId: session.sessionId,
|
|
1572
1641
|
update: {
|
|
1573
1642
|
sessionUpdate: "available_commands_update",
|
|
1574
|
-
availableCommands:
|
|
1643
|
+
availableCommands: deduplicateCommands([...commands, ...BUILTIN_COMMANDS])
|
|
1575
1644
|
}
|
|
1576
1645
|
});
|
|
1577
1646
|
} catch {}
|
|
@@ -1603,22 +1672,10 @@ var PiAcpAgent = class {
|
|
|
1603
1672
|
}
|
|
1604
1673
|
async unstable_setSessionModel(params) {
|
|
1605
1674
|
const session = this.sessions.get(params.sessionId);
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
if (params.modelId
|
|
1609
|
-
|
|
1610
|
-
provider = p ?? null;
|
|
1611
|
-
modelId = rest.join("/");
|
|
1612
|
-
} else modelId = params.modelId;
|
|
1613
|
-
if (provider === null) {
|
|
1614
|
-
const found = session.piSession.modelRegistry.getAvailable().find((m) => m.id === modelId);
|
|
1615
|
-
if (found) {
|
|
1616
|
-
provider = found.provider;
|
|
1617
|
-
modelId = found.id;
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
if (provider === null || modelId === null) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
|
|
1621
|
-
const model = session.piSession.modelRegistry.getAvailable().find((m) => m.provider === provider && m.id === modelId);
|
|
1675
|
+
const available = session.piSession.modelRegistry.getAvailable();
|
|
1676
|
+
const resolved = resolveModelPreference(available, params.modelId);
|
|
1677
|
+
if (resolved === null) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
|
|
1678
|
+
const model = available.find((m) => m.provider === resolved.provider && m.id === resolved.id);
|
|
1622
1679
|
if (!model) throw RequestError.invalidParams(`Unknown modelId: ${params.modelId}`);
|
|
1623
1680
|
await session.piSession.setModel(model);
|
|
1624
1681
|
this.emitConfigOptionUpdate(session);
|
|
@@ -1628,22 +1685,10 @@ var PiAcpAgent = class {
|
|
|
1628
1685
|
const configId = String(params.configId);
|
|
1629
1686
|
const value = String(params.value);
|
|
1630
1687
|
if (configId === "model") {
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
if (
|
|
1634
|
-
|
|
1635
|
-
provider = p ?? null;
|
|
1636
|
-
modelId = rest.join("/");
|
|
1637
|
-
} else modelId = value;
|
|
1638
|
-
if (provider === null) {
|
|
1639
|
-
const found = session.piSession.modelRegistry.getAvailable().find((m) => m.id === modelId);
|
|
1640
|
-
if (found) {
|
|
1641
|
-
provider = found.provider;
|
|
1642
|
-
modelId = found.id;
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
if (provider === null || modelId === null) throw RequestError.invalidParams(`Unknown model: ${value}`);
|
|
1646
|
-
const model = session.piSession.modelRegistry.getAvailable().find((m) => m.provider === provider && m.id === modelId);
|
|
1688
|
+
const available = session.piSession.modelRegistry.getAvailable();
|
|
1689
|
+
const resolved = resolveModelPreference(available, value);
|
|
1690
|
+
if (resolved === null) throw RequestError.invalidParams(`Unknown model: ${value}`);
|
|
1691
|
+
const model = available.find((m) => m.provider === resolved.provider && m.id === resolved.id);
|
|
1647
1692
|
if (!model) throw RequestError.invalidParams(`Unknown model: ${value}`);
|
|
1648
1693
|
await session.piSession.setModel(model);
|
|
1649
1694
|
} else if (configId === "thought_level") {
|
|
@@ -2020,71 +2065,12 @@ function buildCommandList(piSession, enableSkillCommands) {
|
|
|
2020
2065
|
description: skill.description ?? `(skill)`
|
|
2021
2066
|
});
|
|
2022
2067
|
}
|
|
2023
|
-
const
|
|
2024
|
-
if (runner) for (const cmd of runner.getRegisteredCommands()) commands.push({
|
|
2068
|
+
for (const cmd of piSession.extensionRunner.getRegisteredCommands()) commands.push({
|
|
2025
2069
|
name: cmd.name,
|
|
2026
2070
|
description: cmd.description ?? "(extension)"
|
|
2027
2071
|
});
|
|
2028
2072
|
return commands;
|
|
2029
2073
|
}
|
|
2030
|
-
let cachedUpdateNotice;
|
|
2031
|
-
function buildUpdateNotice() {
|
|
2032
|
-
if (cachedUpdateNotice !== void 0) return cachedUpdateNotice;
|
|
2033
|
-
try {
|
|
2034
|
-
const installed = VERSION;
|
|
2035
|
-
if (!installed || !isSemver(installed)) {
|
|
2036
|
-
cachedUpdateNotice = null;
|
|
2037
|
-
return null;
|
|
2038
|
-
}
|
|
2039
|
-
const latestRes = spawnSync("npm", [
|
|
2040
|
-
"view",
|
|
2041
|
-
"@mariozechner/pi-coding-agent",
|
|
2042
|
-
"version"
|
|
2043
|
-
], {
|
|
2044
|
-
encoding: "utf-8",
|
|
2045
|
-
timeout: 800
|
|
2046
|
-
});
|
|
2047
|
-
const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
|
|
2048
|
-
if (!latest || !isSemver(latest)) {
|
|
2049
|
-
cachedUpdateNotice = null;
|
|
2050
|
-
return null;
|
|
2051
|
-
}
|
|
2052
|
-
if (compareSemver(latest, installed) <= 0) {
|
|
2053
|
-
cachedUpdateNotice = null;
|
|
2054
|
-
return null;
|
|
2055
|
-
}
|
|
2056
|
-
cachedUpdateNotice = `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
|
|
2057
|
-
return cachedUpdateNotice;
|
|
2058
|
-
} catch {
|
|
2059
|
-
cachedUpdateNotice = null;
|
|
2060
|
-
return null;
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
function buildStartupInfo(opts) {
|
|
2064
|
-
const md = [];
|
|
2065
|
-
if (VERSION) {
|
|
2066
|
-
md.push(`pi v${VERSION}`);
|
|
2067
|
-
md.push("---");
|
|
2068
|
-
md.push("");
|
|
2069
|
-
}
|
|
2070
|
-
const addSection = (title, items) => {
|
|
2071
|
-
const cleaned = items.map((s) => s.trim()).filter(Boolean);
|
|
2072
|
-
if (cleaned.length === 0) return;
|
|
2073
|
-
md.push(`## ${title}`);
|
|
2074
|
-
for (const item of cleaned) md.push(`- ${item}`);
|
|
2075
|
-
md.push("");
|
|
2076
|
-
};
|
|
2077
|
-
const contextItems = [];
|
|
2078
|
-
const contextPath = join(opts.cwd, "AGENTS.md");
|
|
2079
|
-
if (existsSync(contextPath)) contextItems.push(contextPath);
|
|
2080
|
-
addSection("Context", contextItems);
|
|
2081
|
-
if (opts.updateNotice !== void 0 && opts.updateNotice !== null) {
|
|
2082
|
-
md.push("---");
|
|
2083
|
-
md.push(opts.updateNotice);
|
|
2084
|
-
md.push("");
|
|
2085
|
-
}
|
|
2086
|
-
return `${md.join("\n").trim()}\n`;
|
|
2087
|
-
}
|
|
2088
2074
|
function findChangelog() {
|
|
2089
2075
|
try {
|
|
2090
2076
|
const which = spawnSync(process.platform === "win32" ? "where" : "which", ["pi"], { encoding: "utf-8" });
|
|
@@ -2104,44 +2090,17 @@ function findChangelog() {
|
|
|
2104
2090
|
} catch {}
|
|
2105
2091
|
return null;
|
|
2106
2092
|
}
|
|
2107
|
-
function isSemver(v) {
|
|
2108
|
-
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(v);
|
|
2109
|
-
}
|
|
2110
|
-
function compareSemver(a, b) {
|
|
2111
|
-
const pa = a.split(/[.-]/).slice(0, 3).map((n) => Number(n));
|
|
2112
|
-
const pb = b.split(/[.-]/).slice(0, 3).map((n) => Number(n));
|
|
2113
|
-
for (let i = 0; i < 3; i++) {
|
|
2114
|
-
const da = pa[i] ?? 0;
|
|
2115
|
-
const db = pb[i] ?? 0;
|
|
2116
|
-
if (da > db) return 1;
|
|
2117
|
-
if (da < db) return -1;
|
|
2118
|
-
}
|
|
2119
|
-
return 0;
|
|
2120
|
-
}
|
|
2121
|
-
function readNearestPackageJson(metaUrl) {
|
|
2122
|
-
const fallback = {
|
|
2123
|
-
name: "pi-acp",
|
|
2124
|
-
version: "0.0.0"
|
|
2125
|
-
};
|
|
2126
|
-
try {
|
|
2127
|
-
let dir = dirname(fileURLToPath(metaUrl));
|
|
2128
|
-
for (let i = 0; i < 6; i++) {
|
|
2129
|
-
const p = join(dir, "package.json");
|
|
2130
|
-
if (existsSync(p)) {
|
|
2131
|
-
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
2132
|
-
if (typeof raw !== "object" || raw === null) return fallback;
|
|
2133
|
-
return {
|
|
2134
|
-
name: "name" in raw && typeof raw.name === "string" ? raw.name : fallback.name,
|
|
2135
|
-
version: "version" in raw && typeof raw.version === "string" ? raw.version : fallback.version
|
|
2136
|
-
};
|
|
2137
|
-
}
|
|
2138
|
-
dir = dirname(dir);
|
|
2139
|
-
}
|
|
2140
|
-
} catch {}
|
|
2141
|
-
return fallback;
|
|
2142
|
-
}
|
|
2143
2093
|
//#endregion
|
|
2144
2094
|
//#region src/index.ts
|
|
2095
|
+
{
|
|
2096
|
+
const toStderr = (...args) => {
|
|
2097
|
+
process.stderr.write(`${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}\n`);
|
|
2098
|
+
};
|
|
2099
|
+
console.log = toStderr;
|
|
2100
|
+
console.info = toStderr;
|
|
2101
|
+
console.warn = toStderr;
|
|
2102
|
+
console.debug = toStderr;
|
|
2103
|
+
}
|
|
2145
2104
|
if (process.argv.includes("--terminal-login")) {
|
|
2146
2105
|
const { spawnSync } = await import("node:child_process");
|
|
2147
2106
|
const isWindows = platform() === "win32";
|
|
@@ -2151,7 +2110,7 @@ if (process.argv.includes("--terminal-login")) {
|
|
|
2151
2110
|
env: process.env
|
|
2152
2111
|
});
|
|
2153
2112
|
if (res.error && "code" in res.error && res.error.code === "ENOENT") {
|
|
2154
|
-
process.stderr.write(`pi-acp: could not start pi (command not found: ${cmd}). Install via \`npm install -g @
|
|
2113
|
+
process.stderr.write(`pi-acp: could not start pi (command not found: ${cmd}). Install via \`npm install -g @earendil-works/pi-coding-agent\` or ensure \`pi\` is on your PATH.
|
|
2155
2114
|
`);
|
|
2156
2115
|
process.exit(1);
|
|
2157
2116
|
}
|
|
@@ -2174,7 +2133,10 @@ const agent = new AgentSideConnection((conn) => new PiAcpAgent(conn), ndJsonStre
|
|
|
2174
2133
|
process.stdin.on("end", () => controller.close());
|
|
2175
2134
|
process.stdin.on("error", (err) => controller.error(err));
|
|
2176
2135
|
} })));
|
|
2136
|
+
let shuttingDown = false;
|
|
2177
2137
|
function shutdown() {
|
|
2138
|
+
if (shuttingDown) return;
|
|
2139
|
+
shuttingDown = true;
|
|
2178
2140
|
try {
|
|
2179
2141
|
if ("agent" in agent) {
|
|
2180
2142
|
const inner = agent.agent;
|
|
@@ -2183,9 +2145,7 @@ function shutdown() {
|
|
|
2183
2145
|
} catch {}
|
|
2184
2146
|
process.exit(0);
|
|
2185
2147
|
}
|
|
2186
|
-
|
|
2187
|
-
process.stdin.on("close", shutdown);
|
|
2188
|
-
process.stdin.resume();
|
|
2148
|
+
agent.closed.then(shutdown);
|
|
2189
2149
|
process.on("SIGINT", shutdown);
|
|
2190
2150
|
process.on("SIGTERM", shutdown);
|
|
2191
2151
|
process.stdout.on("error", () => process.exit(0));
|