@victor-software-house/pi-acp 0.1.1 → 0.2.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 -8
- package/dist/index.mjs +469 -183
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -6
package/dist/index.mjs
CHANGED
|
@@ -2,81 +2,11 @@
|
|
|
2
2
|
import { homedir, platform } from "node:os";
|
|
3
3
|
import { AgentSideConnection, RequestError, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { existsSync, readFileSync,
|
|
5
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
6
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { SessionManager, VERSION, createAgentSession } from "@mariozechner/pi-coding-agent";
|
|
9
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
10
|
//#region src/acp/auth.ts
|
|
81
11
|
const AUTH_METHOD_ID = "pi_terminal_login";
|
|
82
12
|
function buildAuthMethods(opts) {
|
|
@@ -111,6 +41,29 @@ function resolveTerminalLaunchCommand() {
|
|
|
111
41
|
};
|
|
112
42
|
}
|
|
113
43
|
//#endregion
|
|
44
|
+
//#region src/acp/auth-required.ts
|
|
45
|
+
/**
|
|
46
|
+
* Detect common auth/credential errors from pi and surface them as ACP AUTH_REQUIRED.
|
|
47
|
+
*/
|
|
48
|
+
const AUTH_ERROR_PATTERNS = [
|
|
49
|
+
"api key",
|
|
50
|
+
"apikey",
|
|
51
|
+
"missing key",
|
|
52
|
+
"no key",
|
|
53
|
+
"not configured",
|
|
54
|
+
"unauthorized",
|
|
55
|
+
"authentication",
|
|
56
|
+
"permission denied",
|
|
57
|
+
"forbidden",
|
|
58
|
+
"401",
|
|
59
|
+
"403"
|
|
60
|
+
];
|
|
61
|
+
function detectAuthError(err) {
|
|
62
|
+
const lower = (err instanceof Error ? err.message : String(err ?? "")).toLowerCase();
|
|
63
|
+
if (!AUTH_ERROR_PATTERNS.some((p) => lower.includes(p))) return null;
|
|
64
|
+
return RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
114
67
|
//#region src/acp/pi-settings.ts
|
|
115
68
|
/**
|
|
116
69
|
* Read pi settings from global and project config files.
|
|
@@ -261,6 +214,30 @@ function toToolKind(toolName) {
|
|
|
261
214
|
default: return "other";
|
|
262
215
|
}
|
|
263
216
|
}
|
|
217
|
+
const MAX_TITLE_LEN = 80;
|
|
218
|
+
function truncateTitle(text) {
|
|
219
|
+
const oneLine = text.replace(/\n/g, " ").trim();
|
|
220
|
+
if (oneLine.length <= MAX_TITLE_LEN) return oneLine;
|
|
221
|
+
return `${oneLine.slice(0, MAX_TITLE_LEN - 1)}…`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Build a descriptive tool title from tool name and args.
|
|
225
|
+
*
|
|
226
|
+
* Returns a short human-readable label like "Read src/index.ts" or "Run ls -la".
|
|
227
|
+
*/
|
|
228
|
+
function buildToolTitle(toolName, args) {
|
|
229
|
+
const p = args.path;
|
|
230
|
+
switch (toolName) {
|
|
231
|
+
case "read": return p !== void 0 ? `Read ${p}` : "Read";
|
|
232
|
+
case "write": return p !== void 0 ? `Write ${p}` : "Write";
|
|
233
|
+
case "edit": return p !== void 0 ? `Edit ${p}` : "Edit";
|
|
234
|
+
case "bash": {
|
|
235
|
+
const command = typeof args["command"] === "string" ? args["command"] : typeof args["cmd"] === "string" ? args["cmd"] : void 0;
|
|
236
|
+
return command !== void 0 ? truncateTitle(`Run ${command}`) : "bash";
|
|
237
|
+
}
|
|
238
|
+
default: return toolName;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
264
241
|
/**
|
|
265
242
|
* Map pi assistant stopReason to ACP StopReason.
|
|
266
243
|
* pi: "stop" | "length" | "toolUse" | "error" | "aborted"
|
|
@@ -285,8 +262,8 @@ function parseToolInput(tc) {
|
|
|
285
262
|
return tc.arguments;
|
|
286
263
|
}
|
|
287
264
|
const toolArgsSchema = z.object({
|
|
288
|
-
path: z.string().optional(),
|
|
289
|
-
oldText: z.string().optional()
|
|
265
|
+
path: z.string().trim().optional(),
|
|
266
|
+
oldText: z.string().trim().optional()
|
|
290
267
|
}).loose();
|
|
291
268
|
function toToolArgs(raw) {
|
|
292
269
|
const result = toolArgsSchema.safeParse(raw);
|
|
@@ -452,7 +429,7 @@ var PiAcpSession = class {
|
|
|
452
429
|
this.emit({
|
|
453
430
|
sessionUpdate: "tool_call",
|
|
454
431
|
toolCallId: toolCall.id,
|
|
455
|
-
title: toolCall.name,
|
|
432
|
+
title: buildToolTitle(toolCall.name, rawInput),
|
|
456
433
|
kind: toToolKind(toolCall.name),
|
|
457
434
|
status,
|
|
458
435
|
...locations ? { locations } : {},
|
|
@@ -472,14 +449,17 @@ var PiAcpSession = class {
|
|
|
472
449
|
}
|
|
473
450
|
handleToolStart(toolCallId, toolName, args) {
|
|
474
451
|
let line;
|
|
475
|
-
if (toolName === "edit" && args.path !== void 0) try {
|
|
452
|
+
if ((toolName === "edit" || toolName === "write") && args.path !== void 0) try {
|
|
476
453
|
const abs = isAbsolute(args.path) ? args.path : resolve(this.cwd, args.path);
|
|
477
|
-
|
|
454
|
+
let oldText = "";
|
|
455
|
+
try {
|
|
456
|
+
oldText = readFileSync(abs, "utf8");
|
|
457
|
+
} catch {}
|
|
478
458
|
this.editSnapshots.set(toolCallId, {
|
|
479
459
|
path: abs,
|
|
480
460
|
oldText
|
|
481
461
|
});
|
|
482
|
-
line = findUniqueLineNumber(oldText, args.oldText ?? "");
|
|
462
|
+
if (toolName === "edit") line = findUniqueLineNumber(oldText, args.oldText ?? "");
|
|
483
463
|
} catch {}
|
|
484
464
|
const locations = resolveToolPath(args, this.cwd, line);
|
|
485
465
|
if (!this.currentToolCalls.has(toolCallId)) {
|
|
@@ -487,7 +467,7 @@ var PiAcpSession = class {
|
|
|
487
467
|
this.emit({
|
|
488
468
|
sessionUpdate: "tool_call",
|
|
489
469
|
toolCallId,
|
|
490
|
-
title: toolName,
|
|
470
|
+
title: buildToolTitle(toolName, args),
|
|
491
471
|
kind: toToolKind(toolName),
|
|
492
472
|
status: "in_progress",
|
|
493
473
|
...locations ? { locations } : {},
|
|
@@ -498,6 +478,7 @@ var PiAcpSession = class {
|
|
|
498
478
|
this.emit({
|
|
499
479
|
sessionUpdate: "tool_call_update",
|
|
500
480
|
toolCallId,
|
|
481
|
+
title: buildToolTitle(toolName, args),
|
|
501
482
|
status: "in_progress",
|
|
502
483
|
...locations ? { locations } : {},
|
|
503
484
|
rawInput: args
|
|
@@ -557,6 +538,7 @@ var PiAcpSession = class {
|
|
|
557
538
|
this.editSnapshots.delete(toolCallId);
|
|
558
539
|
}
|
|
559
540
|
handleAgentEnd() {
|
|
541
|
+
this.emitUsageUpdate();
|
|
560
542
|
this.flushEmits().finally(() => {
|
|
561
543
|
const reason = this.cancelRequested ? "cancelled" : mapPiStopReason(this.lastAssistantStopReason);
|
|
562
544
|
this.lastAssistantStopReason = null;
|
|
@@ -564,6 +546,42 @@ var PiAcpSession = class {
|
|
|
564
546
|
this.pendingTurn = null;
|
|
565
547
|
});
|
|
566
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Emit a usage_update notification with current context and cost data.
|
|
551
|
+
*/
|
|
552
|
+
emitUsageUpdate() {
|
|
553
|
+
const contextUsage = this.piSession.getContextUsage?.();
|
|
554
|
+
const stats = this.piSession.getSessionStats();
|
|
555
|
+
const used = contextUsage?.tokens ?? 0;
|
|
556
|
+
const size = contextUsage?.contextWindow ?? 0;
|
|
557
|
+
this.emit({
|
|
558
|
+
sessionUpdate: "usage_update",
|
|
559
|
+
used,
|
|
560
|
+
size,
|
|
561
|
+
cost: stats.cost > 0 ? {
|
|
562
|
+
amount: stats.cost,
|
|
563
|
+
currency: "USD"
|
|
564
|
+
} : null
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Build ACP Usage data from pi session stats for prompt response.
|
|
569
|
+
*/
|
|
570
|
+
getUsage() {
|
|
571
|
+
const stats = this.piSession.getSessionStats();
|
|
572
|
+
return {
|
|
573
|
+
inputTokens: stats.tokens.input,
|
|
574
|
+
outputTokens: stats.tokens.output,
|
|
575
|
+
cachedReadTokens: stats.tokens.cacheRead,
|
|
576
|
+
cachedWriteTokens: stats.tokens.cacheWrite
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Get cumulative session cost.
|
|
581
|
+
*/
|
|
582
|
+
getCost() {
|
|
583
|
+
return this.piSession.getSessionStats().cost;
|
|
584
|
+
}
|
|
567
585
|
};
|
|
568
586
|
/**
|
|
569
587
|
* Type guard to narrow AgentSessionEvent to the AgentEvent subset
|
|
@@ -584,10 +602,6 @@ function extractUserMessageText(content) {
|
|
|
584
602
|
if (!Array.isArray(content)) return "";
|
|
585
603
|
return content.filter(isTextBlock).map((b) => b.text).join("");
|
|
586
604
|
}
|
|
587
|
-
function extractAssistantText(content) {
|
|
588
|
-
if (!Array.isArray(content)) return "";
|
|
589
|
-
return content.filter(isTextBlock).map((b) => b.text).join("");
|
|
590
|
-
}
|
|
591
605
|
//#endregion
|
|
592
606
|
//#region src/acp/translate/prompt.ts
|
|
593
607
|
function acpPromptToPiMessage(blocks) {
|
|
@@ -631,6 +645,76 @@ function acpPromptToPiMessage(blocks) {
|
|
|
631
645
|
};
|
|
632
646
|
}
|
|
633
647
|
//#endregion
|
|
648
|
+
//#region src/pi-auth/status.ts
|
|
649
|
+
/**
|
|
650
|
+
* Detect whether the user has any pi authentication configured.
|
|
651
|
+
*
|
|
652
|
+
* Checks three sources:
|
|
653
|
+
* 1. auth.json (API keys, OAuth credentials)
|
|
654
|
+
* 2. models.json custom provider apiKey entries
|
|
655
|
+
* 3. Known provider environment variables
|
|
656
|
+
*/
|
|
657
|
+
const modelsConfigSchema = z.object({ providers: z.record(z.string().trim(), z.object({ apiKey: z.string().trim().optional() })).optional() });
|
|
658
|
+
function agentDir() {
|
|
659
|
+
const env = process.env.PI_CODING_AGENT_DIR;
|
|
660
|
+
if (env === void 0) return join(homedir(), ".pi", "agent");
|
|
661
|
+
if (env === "~") return homedir();
|
|
662
|
+
if (env.startsWith("~/")) return homedir() + env.slice(1);
|
|
663
|
+
return env;
|
|
664
|
+
}
|
|
665
|
+
function readJsonFile(path) {
|
|
666
|
+
try {
|
|
667
|
+
if (!existsSync(path)) return null;
|
|
668
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
669
|
+
if (!raw) return null;
|
|
670
|
+
return JSON.parse(raw);
|
|
671
|
+
} catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function hasAuthJson() {
|
|
676
|
+
const data = readJsonFile(join(agentDir(), "auth.json"));
|
|
677
|
+
return typeof data === "object" && data !== null && Object.keys(data).length > 0;
|
|
678
|
+
}
|
|
679
|
+
function hasCustomProviderKey() {
|
|
680
|
+
const raw = readJsonFile(join(agentDir(), "models.json"));
|
|
681
|
+
const result = modelsConfigSchema.safeParse(raw);
|
|
682
|
+
if (!result.success || !result.data.providers) return false;
|
|
683
|
+
return Object.values(result.data.providers).some((provider) => typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0);
|
|
684
|
+
}
|
|
685
|
+
/** Environment variables that indicate a configured provider API key. */
|
|
686
|
+
const PROVIDER_ENV_VARS = [
|
|
687
|
+
"ANTHROPIC_API_KEY",
|
|
688
|
+
"ANTHROPIC_OAUTH_TOKEN",
|
|
689
|
+
"OPENAI_API_KEY",
|
|
690
|
+
"AZURE_OPENAI_API_KEY",
|
|
691
|
+
"GEMINI_API_KEY",
|
|
692
|
+
"GROQ_API_KEY",
|
|
693
|
+
"CEREBRAS_API_KEY",
|
|
694
|
+
"XAI_API_KEY",
|
|
695
|
+
"OPENROUTER_API_KEY",
|
|
696
|
+
"AI_GATEWAY_API_KEY",
|
|
697
|
+
"ZAI_API_KEY",
|
|
698
|
+
"MISTRAL_API_KEY",
|
|
699
|
+
"MINIMAX_API_KEY",
|
|
700
|
+
"MINIMAX_CN_API_KEY",
|
|
701
|
+
"HF_TOKEN",
|
|
702
|
+
"OPENCODE_API_KEY",
|
|
703
|
+
"KIMI_API_KEY",
|
|
704
|
+
"COPILOT_GITHUB_TOKEN",
|
|
705
|
+
"GH_TOKEN",
|
|
706
|
+
"GITHUB_TOKEN"
|
|
707
|
+
];
|
|
708
|
+
function hasProviderEnvVar() {
|
|
709
|
+
return PROVIDER_ENV_VARS.some((key) => {
|
|
710
|
+
const val = process.env[key];
|
|
711
|
+
return typeof val === "string" && val.trim().length > 0;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function hasPiAuthConfigured() {
|
|
715
|
+
return hasAuthJson() || hasCustomProviderKey() || hasProviderEnvVar();
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
634
718
|
//#region src/acp/agent.ts
|
|
635
719
|
function builtinAvailableCommands() {
|
|
636
720
|
return [
|
|
@@ -699,10 +783,20 @@ function parseArgs(input) {
|
|
|
699
783
|
if (current !== "") args.push(current);
|
|
700
784
|
return args;
|
|
701
785
|
}
|
|
786
|
+
const SESSION_TITLE_MAX = 100;
|
|
787
|
+
function truncateSessionTitle(text) {
|
|
788
|
+
const trimmed = text.trim();
|
|
789
|
+
if (trimmed === "") return null;
|
|
790
|
+
const oneLine = trimmed.replace(/\n/g, " ");
|
|
791
|
+
if (oneLine.length <= SESSION_TITLE_MAX) return oneLine;
|
|
792
|
+
return `${oneLine.slice(0, SESSION_TITLE_MAX - 1)}…`;
|
|
793
|
+
}
|
|
702
794
|
const pkg = readNearestPackageJson(import.meta.url);
|
|
703
795
|
var PiAcpAgent = class {
|
|
704
796
|
conn;
|
|
705
797
|
sessions = new SessionManager$1();
|
|
798
|
+
/** Cache of sessionId → file path, populated by listSessions and newSession. */
|
|
799
|
+
sessionPaths = /* @__PURE__ */ new Map();
|
|
706
800
|
dispose() {
|
|
707
801
|
this.sessions.disposeAll();
|
|
708
802
|
}
|
|
@@ -729,9 +823,14 @@ var PiAcpAgent = class {
|
|
|
729
823
|
promptCapabilities: {
|
|
730
824
|
image: true,
|
|
731
825
|
audio: false,
|
|
732
|
-
embeddedContext:
|
|
826
|
+
embeddedContext: true
|
|
733
827
|
},
|
|
734
|
-
sessionCapabilities: {
|
|
828
|
+
sessionCapabilities: {
|
|
829
|
+
list: {},
|
|
830
|
+
close: {},
|
|
831
|
+
resume: {},
|
|
832
|
+
fork: {}
|
|
833
|
+
}
|
|
735
834
|
}
|
|
736
835
|
};
|
|
737
836
|
}
|
|
@@ -742,6 +841,8 @@ var PiAcpAgent = class {
|
|
|
742
841
|
try {
|
|
743
842
|
result = await createAgentSession({ cwd: params.cwd });
|
|
744
843
|
} catch (e) {
|
|
844
|
+
const authErr = detectAuthError(e);
|
|
845
|
+
if (authErr !== null) throw authErr;
|
|
745
846
|
const msg = e instanceof Error ? e.message : String(e);
|
|
746
847
|
throw RequestError.internalError({}, `Failed to create pi session: ${msg}`);
|
|
747
848
|
}
|
|
@@ -750,8 +851,11 @@ var PiAcpAgent = class {
|
|
|
750
851
|
piSession.dispose();
|
|
751
852
|
throw RequestError.authRequired({ authMethods: buildAuthMethods() }, "Configure an API key or log in with an OAuth provider.");
|
|
752
853
|
}
|
|
854
|
+
const sessionId = piSession.sessionManager.getSessionId();
|
|
855
|
+
const sessionFile = piSession.sessionManager.getSessionFile();
|
|
856
|
+
if (sessionFile !== void 0) this.sessionPaths.set(sessionId, sessionFile);
|
|
753
857
|
const session = new PiAcpSession({
|
|
754
|
-
sessionId
|
|
858
|
+
sessionId,
|
|
755
859
|
cwd: params.cwd,
|
|
756
860
|
mcpServers: params.mcpServers,
|
|
757
861
|
piSession,
|
|
@@ -765,7 +869,6 @@ var PiAcpAgent = class {
|
|
|
765
869
|
updateNotice
|
|
766
870
|
});
|
|
767
871
|
if (preludeText) session.setStartupInfo(preludeText);
|
|
768
|
-
this.sessions.closeAllExcept(session.sessionId);
|
|
769
872
|
const modes = buildThinkingModes(piSession);
|
|
770
873
|
const models = buildModelState(piSession);
|
|
771
874
|
const configOptions = buildConfigOptions(modes, models);
|
|
@@ -794,7 +897,9 @@ var PiAcpAgent = class {
|
|
|
794
897
|
}, 0);
|
|
795
898
|
return response;
|
|
796
899
|
}
|
|
797
|
-
async authenticate(_params) {
|
|
900
|
+
async authenticate(_params) {
|
|
901
|
+
return {};
|
|
902
|
+
}
|
|
798
903
|
async prompt(params) {
|
|
799
904
|
const session = this.sessions.get(params.sessionId);
|
|
800
905
|
const { message, images } = acpPromptToPiMessage(params.prompt);
|
|
@@ -807,64 +912,49 @@ var PiAcpAgent = class {
|
|
|
807
912
|
if (handled) return handled;
|
|
808
913
|
}
|
|
809
914
|
const result = await session.prompt(message, images);
|
|
810
|
-
|
|
915
|
+
const stopReason = result === "error" ? "end_turn" : result;
|
|
916
|
+
const usage = session.getUsage();
|
|
917
|
+
const cost = session.getCost();
|
|
918
|
+
return {
|
|
919
|
+
stopReason,
|
|
920
|
+
usage: {
|
|
921
|
+
inputTokens: usage.inputTokens,
|
|
922
|
+
outputTokens: usage.outputTokens,
|
|
923
|
+
cachedReadTokens: usage.cachedReadTokens,
|
|
924
|
+
cachedWriteTokens: usage.cachedWriteTokens,
|
|
925
|
+
totalTokens: usage.inputTokens + usage.outputTokens
|
|
926
|
+
},
|
|
927
|
+
_meta: cost > 0 ? { cost: {
|
|
928
|
+
amount: cost,
|
|
929
|
+
currency: "USD"
|
|
930
|
+
} } : {}
|
|
931
|
+
};
|
|
811
932
|
}
|
|
812
933
|
async cancel(params) {
|
|
813
934
|
await this.sessions.get(params.sessionId).cancel();
|
|
814
935
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
};
|
|
936
|
+
/**
|
|
937
|
+
* Resolve a session ID to a file path.
|
|
938
|
+
* Checks the local cache first (populated by listSessions/newSession),
|
|
939
|
+
* falls back to a full listAll() scan on cache miss.
|
|
940
|
+
*/
|
|
941
|
+
async resolveSessionFile(sessionId) {
|
|
942
|
+
const cached = this.sessionPaths.get(sessionId);
|
|
943
|
+
if (cached !== void 0) return cached;
|
|
944
|
+
const all = await SessionManager.listAll();
|
|
945
|
+
for (const s of all) this.sessionPaths.set(s.id, s.path);
|
|
946
|
+
return this.sessionPaths.get(sessionId) ?? null;
|
|
840
947
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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;
|
|
948
|
+
/**
|
|
949
|
+
* Replay persisted session messages as ACP session updates.
|
|
950
|
+
*
|
|
951
|
+
* Iterates through the message history, emitting structured updates for each
|
|
952
|
+
* content block type: text, thinking, tool calls, and tool results. A map of
|
|
953
|
+
* tool call IDs to their invocation data (from assistant messages) is built
|
|
954
|
+
* to enrich subsequent tool result updates with rawInput and locations.
|
|
955
|
+
*/
|
|
956
|
+
async replaySessionHistory(session, messages) {
|
|
957
|
+
const toolCallMap = /* @__PURE__ */ new Map();
|
|
868
958
|
for (const m of messages) {
|
|
869
959
|
if (!("role" in m)) continue;
|
|
870
960
|
if (m.role === "user") {
|
|
@@ -879,32 +969,67 @@ var PiAcpAgent = class {
|
|
|
879
969
|
}
|
|
880
970
|
}
|
|
881
971
|
});
|
|
972
|
+
continue;
|
|
882
973
|
}
|
|
883
974
|
if (m.role === "assistant") {
|
|
884
|
-
const
|
|
885
|
-
if (text) await this.conn.sessionUpdate({
|
|
975
|
+
const am = m;
|
|
976
|
+
for (const block of am.content) if (block.type === "text" && block.text) await this.conn.sessionUpdate({
|
|
886
977
|
sessionId: session.sessionId,
|
|
887
978
|
update: {
|
|
888
979
|
sessionUpdate: "agent_message_chunk",
|
|
889
980
|
content: {
|
|
890
981
|
type: "text",
|
|
891
|
-
text
|
|
982
|
+
text: block.text
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
else if (block.type === "thinking" && block.thinking) await this.conn.sessionUpdate({
|
|
987
|
+
sessionId: session.sessionId,
|
|
988
|
+
update: {
|
|
989
|
+
sessionUpdate: "agent_thought_chunk",
|
|
990
|
+
content: {
|
|
991
|
+
type: "text",
|
|
992
|
+
text: block.thinking
|
|
892
993
|
}
|
|
893
994
|
}
|
|
894
995
|
});
|
|
996
|
+
else if (block.type === "toolCall") {
|
|
997
|
+
const args = toToolArgs(block.arguments);
|
|
998
|
+
toolCallMap.set(block.id, {
|
|
999
|
+
name: block.name,
|
|
1000
|
+
args
|
|
1001
|
+
});
|
|
1002
|
+
const locations = resolveToolPath(args, session.cwd);
|
|
1003
|
+
await this.conn.sessionUpdate({
|
|
1004
|
+
sessionId: session.sessionId,
|
|
1005
|
+
update: {
|
|
1006
|
+
sessionUpdate: "tool_call",
|
|
1007
|
+
toolCallId: block.id,
|
|
1008
|
+
title: buildToolTitle(block.name, args),
|
|
1009
|
+
kind: toToolKind(block.name),
|
|
1010
|
+
status: "completed",
|
|
1011
|
+
rawInput: args,
|
|
1012
|
+
...locations ? { locations } : {}
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
continue;
|
|
895
1017
|
}
|
|
896
1018
|
if (m.role === "toolResult") {
|
|
897
1019
|
const tr = m;
|
|
898
1020
|
const toolName = tr.toolName;
|
|
899
1021
|
const toolCallId = tr.toolCallId;
|
|
900
1022
|
const isError = tr.isError;
|
|
901
|
-
|
|
1023
|
+
const invocation = toolCallMap.get(toolCallId);
|
|
1024
|
+
const args = invocation?.args;
|
|
1025
|
+
const locations = args !== void 0 ? resolveToolPath(args, session.cwd) : void 0;
|
|
1026
|
+
if (invocation === void 0) await this.conn.sessionUpdate({
|
|
902
1027
|
sessionId: session.sessionId,
|
|
903
1028
|
update: {
|
|
904
1029
|
sessionUpdate: "tool_call",
|
|
905
1030
|
toolCallId,
|
|
906
|
-
title: toolName,
|
|
907
|
-
kind: toolName
|
|
1031
|
+
title: buildToolTitle(toolName, {}),
|
|
1032
|
+
kind: toToolKind(toolName),
|
|
908
1033
|
status: "completed",
|
|
909
1034
|
rawInput: null,
|
|
910
1035
|
rawOutput: m
|
|
@@ -924,11 +1049,70 @@ var PiAcpAgent = class {
|
|
|
924
1049
|
text
|
|
925
1050
|
}
|
|
926
1051
|
}] : null,
|
|
927
|
-
rawOutput: m
|
|
1052
|
+
rawOutput: m,
|
|
1053
|
+
...locations ? { locations } : {}
|
|
928
1054
|
}
|
|
929
1055
|
});
|
|
930
1056
|
}
|
|
931
1057
|
}
|
|
1058
|
+
}
|
|
1059
|
+
async listSessions(params) {
|
|
1060
|
+
const cwd = params.cwd;
|
|
1061
|
+
const raw = cwd !== void 0 && cwd !== null ? await SessionManager.list(cwd) : await SessionManager.listAll();
|
|
1062
|
+
for (const s of raw) this.sessionPaths.set(s.id, s.path);
|
|
1063
|
+
const sessions = raw.map((s) => ({
|
|
1064
|
+
id: s.id,
|
|
1065
|
+
cwd: s.cwd,
|
|
1066
|
+
name: s.name,
|
|
1067
|
+
firstMessage: s.firstMessage,
|
|
1068
|
+
modified: s.modified,
|
|
1069
|
+
messageCount: s.messageCount
|
|
1070
|
+
}));
|
|
1071
|
+
if (params.cursor !== void 0 && params.cursor !== null) {
|
|
1072
|
+
const parsed = Number.parseInt(params.cursor, 10);
|
|
1073
|
+
if (!Number.isFinite(parsed) || parsed < 0) throw RequestError.invalidParams(`Invalid cursor: ${params.cursor}`);
|
|
1074
|
+
}
|
|
1075
|
+
const start = params.cursor !== void 0 && params.cursor !== null ? Number.parseInt(params.cursor, 10) : 0;
|
|
1076
|
+
const PAGE_SIZE = 50;
|
|
1077
|
+
return {
|
|
1078
|
+
sessions: sessions.slice(start, start + PAGE_SIZE).map((s) => ({
|
|
1079
|
+
sessionId: s.id,
|
|
1080
|
+
cwd: s.cwd,
|
|
1081
|
+
title: (s.name !== void 0 && s.name !== "" ? s.name : null) ?? truncateSessionTitle(s.firstMessage) ?? null,
|
|
1082
|
+
updatedAt: s.modified.toISOString()
|
|
1083
|
+
})),
|
|
1084
|
+
nextCursor: start + PAGE_SIZE < sessions.length ? String(start + PAGE_SIZE) : null,
|
|
1085
|
+
_meta: {}
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
async loadSession(params) {
|
|
1089
|
+
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1090
|
+
this.sessions.close(params.sessionId);
|
|
1091
|
+
const sessionFile = await this.resolveSessionFile(params.sessionId);
|
|
1092
|
+
if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1093
|
+
let result;
|
|
1094
|
+
try {
|
|
1095
|
+
const sm = SessionManager.open(sessionFile);
|
|
1096
|
+
result = await createAgentSession({
|
|
1097
|
+
cwd: params.cwd,
|
|
1098
|
+
sessionManager: sm
|
|
1099
|
+
});
|
|
1100
|
+
} catch (e) {
|
|
1101
|
+
const authErr = detectAuthError(e);
|
|
1102
|
+
if (authErr !== null) throw authErr;
|
|
1103
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1104
|
+
throw RequestError.internalError({}, `Failed to load pi session: ${msg}`);
|
|
1105
|
+
}
|
|
1106
|
+
const piSession = result.session;
|
|
1107
|
+
const session = new PiAcpSession({
|
|
1108
|
+
sessionId: params.sessionId,
|
|
1109
|
+
cwd: params.cwd,
|
|
1110
|
+
mcpServers: params.mcpServers,
|
|
1111
|
+
piSession,
|
|
1112
|
+
conn: this.conn
|
|
1113
|
+
});
|
|
1114
|
+
this.sessions.register(session);
|
|
1115
|
+
await this.replaySessionHistory(session, piSession.messages);
|
|
932
1116
|
const modes = buildThinkingModes(piSession);
|
|
933
1117
|
const models = buildModelState(piSession);
|
|
934
1118
|
const configOptions = buildConfigOptions(modes, models);
|
|
@@ -954,6 +1138,124 @@ var PiAcpAgent = class {
|
|
|
954
1138
|
_meta: { piAcp: { startupInfo: null } }
|
|
955
1139
|
};
|
|
956
1140
|
}
|
|
1141
|
+
async unstable_closeSession(params) {
|
|
1142
|
+
if (this.sessions.maybeGet(params.sessionId) === void 0) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1143
|
+
this.sessions.close(params.sessionId);
|
|
1144
|
+
return {};
|
|
1145
|
+
}
|
|
1146
|
+
async unstable_resumeSession(params) {
|
|
1147
|
+
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1148
|
+
const existing = this.sessions.maybeGet(params.sessionId);
|
|
1149
|
+
if (existing !== void 0) {
|
|
1150
|
+
const modes = buildThinkingModes(existing.piSession);
|
|
1151
|
+
const models = buildModelState(existing.piSession);
|
|
1152
|
+
return {
|
|
1153
|
+
configOptions: buildConfigOptions(modes, models),
|
|
1154
|
+
modes,
|
|
1155
|
+
models
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
const sessionFile = await this.resolveSessionFile(params.sessionId);
|
|
1159
|
+
if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1160
|
+
let result;
|
|
1161
|
+
try {
|
|
1162
|
+
const sm = SessionManager.open(sessionFile);
|
|
1163
|
+
result = await createAgentSession({
|
|
1164
|
+
cwd: params.cwd,
|
|
1165
|
+
sessionManager: sm
|
|
1166
|
+
});
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
const authErr = detectAuthError(e);
|
|
1169
|
+
if (authErr !== null) throw authErr;
|
|
1170
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1171
|
+
throw RequestError.internalError({}, `Failed to resume pi session: ${msg}`);
|
|
1172
|
+
}
|
|
1173
|
+
const piSession = result.session;
|
|
1174
|
+
const session = new PiAcpSession({
|
|
1175
|
+
sessionId: params.sessionId,
|
|
1176
|
+
cwd: params.cwd,
|
|
1177
|
+
mcpServers: params.mcpServers ?? [],
|
|
1178
|
+
piSession,
|
|
1179
|
+
conn: this.conn
|
|
1180
|
+
});
|
|
1181
|
+
this.sessions.register(session);
|
|
1182
|
+
this.sessionPaths.set(params.sessionId, sessionFile);
|
|
1183
|
+
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
1184
|
+
setTimeout(() => {
|
|
1185
|
+
(async () => {
|
|
1186
|
+
try {
|
|
1187
|
+
const commands = buildCommandList(piSession, enableSkillCommands);
|
|
1188
|
+
await this.conn.sessionUpdate({
|
|
1189
|
+
sessionId: session.sessionId,
|
|
1190
|
+
update: {
|
|
1191
|
+
sessionUpdate: "available_commands_update",
|
|
1192
|
+
availableCommands: mergeCommands(commands, builtinAvailableCommands())
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
} catch {}
|
|
1196
|
+
})();
|
|
1197
|
+
}, 0);
|
|
1198
|
+
const modes = buildThinkingModes(piSession);
|
|
1199
|
+
const models = buildModelState(piSession);
|
|
1200
|
+
return {
|
|
1201
|
+
configOptions: buildConfigOptions(modes, models),
|
|
1202
|
+
modes,
|
|
1203
|
+
models
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
async unstable_forkSession(params) {
|
|
1207
|
+
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1208
|
+
const sourceFile = await this.resolveSessionFile(params.sessionId);
|
|
1209
|
+
if (sourceFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
|
|
1210
|
+
let result;
|
|
1211
|
+
try {
|
|
1212
|
+
const sm = SessionManager.forkFrom(sourceFile, params.cwd);
|
|
1213
|
+
result = await createAgentSession({
|
|
1214
|
+
cwd: params.cwd,
|
|
1215
|
+
sessionManager: sm
|
|
1216
|
+
});
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
const authErr = detectAuthError(e);
|
|
1219
|
+
if (authErr !== null) throw authErr;
|
|
1220
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1221
|
+
throw RequestError.internalError({}, `Failed to fork pi session: ${msg}`);
|
|
1222
|
+
}
|
|
1223
|
+
const piSession = result.session;
|
|
1224
|
+
const newSessionId = piSession.sessionManager.getSessionId();
|
|
1225
|
+
const newSessionFile = piSession.sessionManager.getSessionFile();
|
|
1226
|
+
if (newSessionFile !== void 0) this.sessionPaths.set(newSessionId, newSessionFile);
|
|
1227
|
+
const session = new PiAcpSession({
|
|
1228
|
+
sessionId: newSessionId,
|
|
1229
|
+
cwd: params.cwd,
|
|
1230
|
+
mcpServers: params.mcpServers ?? [],
|
|
1231
|
+
piSession,
|
|
1232
|
+
conn: this.conn
|
|
1233
|
+
});
|
|
1234
|
+
this.sessions.register(session);
|
|
1235
|
+
const enableSkillCommands = skillCommandsEnabled(params.cwd);
|
|
1236
|
+
setTimeout(() => {
|
|
1237
|
+
(async () => {
|
|
1238
|
+
try {
|
|
1239
|
+
const commands = buildCommandList(piSession, enableSkillCommands);
|
|
1240
|
+
await this.conn.sessionUpdate({
|
|
1241
|
+
sessionId: session.sessionId,
|
|
1242
|
+
update: {
|
|
1243
|
+
sessionUpdate: "available_commands_update",
|
|
1244
|
+
availableCommands: mergeCommands(commands, builtinAvailableCommands())
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
} catch {}
|
|
1248
|
+
})();
|
|
1249
|
+
}, 0);
|
|
1250
|
+
const modes = buildThinkingModes(piSession);
|
|
1251
|
+
const models = buildModelState(piSession);
|
|
1252
|
+
return {
|
|
1253
|
+
sessionId: newSessionId,
|
|
1254
|
+
configOptions: buildConfigOptions(modes, models),
|
|
1255
|
+
modes,
|
|
1256
|
+
models
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
957
1259
|
async setSessionMode(params) {
|
|
958
1260
|
const session = this.sessions.get(params.sessionId);
|
|
959
1261
|
const mode = String(params.modeId);
|
|
@@ -1389,45 +1691,21 @@ function buildCommandList(piSession, enableSkillCommands) {
|
|
|
1389
1691
|
});
|
|
1390
1692
|
}
|
|
1391
1693
|
const runner = piSession.extensionRunner;
|
|
1392
|
-
if (runner) for (const
|
|
1393
|
-
name:
|
|
1394
|
-
description:
|
|
1694
|
+
if (runner) for (const cmd of runner.getRegisteredCommands()) commands.push({
|
|
1695
|
+
name: cmd.name,
|
|
1696
|
+
description: cmd.description ?? "(extension)"
|
|
1395
1697
|
});
|
|
1396
1698
|
return commands;
|
|
1397
1699
|
}
|
|
1398
|
-
|
|
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
|
-
}
|
|
1700
|
+
let cachedUpdateNotice;
|
|
1427
1701
|
function buildUpdateNotice() {
|
|
1702
|
+
if (cachedUpdateNotice !== void 0) return cachedUpdateNotice;
|
|
1428
1703
|
try {
|
|
1429
1704
|
const installed = VERSION;
|
|
1430
|
-
if (!installed || !isSemver(installed))
|
|
1705
|
+
if (!installed || !isSemver(installed)) {
|
|
1706
|
+
cachedUpdateNotice = null;
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1431
1709
|
const latestRes = spawnSync("npm", [
|
|
1432
1710
|
"view",
|
|
1433
1711
|
"@mariozechner/pi-coding-agent",
|
|
@@ -1437,10 +1715,18 @@ function buildUpdateNotice() {
|
|
|
1437
1715
|
timeout: 800
|
|
1438
1716
|
});
|
|
1439
1717
|
const latest = String(latestRes.stdout ?? "").trim().replace(/^v/i, "");
|
|
1440
|
-
if (!latest || !isSemver(latest))
|
|
1441
|
-
|
|
1442
|
-
|
|
1718
|
+
if (!latest || !isSemver(latest)) {
|
|
1719
|
+
cachedUpdateNotice = null;
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
if (compareSemver(latest, installed) <= 0) {
|
|
1723
|
+
cachedUpdateNotice = null;
|
|
1724
|
+
return null;
|
|
1725
|
+
}
|
|
1726
|
+
cachedUpdateNotice = `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
|
|
1727
|
+
return cachedUpdateNotice;
|
|
1443
1728
|
} catch {
|
|
1729
|
+
cachedUpdateNotice = null;
|
|
1444
1730
|
return null;
|
|
1445
1731
|
}
|
|
1446
1732
|
}
|