@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/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, readdirSync, realpathSync, statSync } from "node:fs";
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
- const oldText = readFileSync(abs, "utf8");
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: false
826
+ embeddedContext: true
733
827
  },
734
- sessionCapabilities: { list: {} }
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: piSession.sessionManager.getSessionId(),
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
- return { stopReason: result === "error" ? "end_turn" : result };
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
- async listSessions(params) {
816
- const cwd = params.cwd;
817
- const sessions = (cwd !== void 0 && cwd !== null ? await SessionManager.list(cwd) : await SessionManager.listAll()).map((s) => ({
818
- id: s.id,
819
- cwd: s.cwd,
820
- name: s.name ?? "",
821
- modified: s.modified,
822
- messageCount: s.messageCount
823
- }));
824
- if (params.cursor !== void 0 && params.cursor !== null) {
825
- const parsed = Number.parseInt(params.cursor, 10);
826
- if (!Number.isFinite(parsed) || parsed < 0) throw RequestError.invalidParams(`Invalid cursor: ${params.cursor}`);
827
- }
828
- const start = params.cursor !== void 0 && params.cursor !== null ? Number.parseInt(params.cursor, 10) : 0;
829
- const PAGE_SIZE = 50;
830
- return {
831
- sessions: sessions.slice(start, start + PAGE_SIZE).map((s) => ({
832
- sessionId: s.id,
833
- cwd: s.cwd,
834
- title: s.name ?? null,
835
- updatedAt: s.modified.toISOString()
836
- })),
837
- nextCursor: start + PAGE_SIZE < sessions.length ? String(start + PAGE_SIZE) : null,
838
- _meta: {}
839
- };
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
- async loadSession(params) {
842
- if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
843
- this.sessions.close(params.sessionId);
844
- const sessionFile = findPiSessionFile(params.sessionId);
845
- if (sessionFile === null) throw RequestError.invalidParams(`Unknown sessionId: ${params.sessionId}`);
846
- let result;
847
- try {
848
- const sm = SessionManager.open(sessionFile);
849
- result = await createAgentSession({
850
- cwd: params.cwd,
851
- sessionManager: sm
852
- });
853
- } catch (e) {
854
- const msg = e instanceof Error ? e.message : String(e);
855
- throw RequestError.internalError({}, `Failed to load pi session: ${msg}`);
856
- }
857
- const piSession = result.session;
858
- const session = new PiAcpSession({
859
- sessionId: params.sessionId,
860
- cwd: params.cwd,
861
- mcpServers: params.mcpServers,
862
- piSession,
863
- conn: this.conn
864
- });
865
- this.sessions.register(session);
866
- this.sessions.closeAllExcept(session.sessionId);
867
- const messages = piSession.messages;
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 text = extractAssistantText(m.content);
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
- await this.conn.sessionUpdate({
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 === "read" ? "read" : toolName === "write" || toolName === "edit" ? "edit" : "other",
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 { command } of runner.getRegisteredCommandsWithPaths()) commands.push({
1393
- name: command.name,
1394
- description: command.description ?? `(extension)`
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
- function findPiSessionFile(sessionId) {
1399
- const sessionsDir = join(piAgentDir(), "sessions");
1400
- if (!existsSync(sessionsDir)) return null;
1401
- const walkJsonl = (dir) => {
1402
- let entries;
1403
- try {
1404
- entries = readdirSync(dir);
1405
- } catch {
1406
- return null;
1407
- }
1408
- for (const name of entries) {
1409
- const p = join(dir, name);
1410
- try {
1411
- const st = statSync(p);
1412
- if (st.isDirectory()) {
1413
- const found = walkJsonl(p);
1414
- if (found !== void 0) return found;
1415
- } else if (st.isFile() && name.endsWith(".jsonl")) try {
1416
- const firstLine = readFileSync(p, "utf8").split("\n")[0];
1417
- if (firstLine === void 0) continue;
1418
- const header = JSON.parse(firstLine);
1419
- if (typeof header === "object" && header !== null && "type" in header && header.type === "session" && "id" in header && header.id === sessionId) return p;
1420
- } catch {}
1421
- } catch {}
1422
- }
1423
- return null;
1424
- };
1425
- return walkJsonl(sessionsDir);
1426
- }
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)) return null;
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)) return null;
1441
- if (compareSemver(latest, installed) <= 0) return null;
1442
- return `New version available: v${latest} (installed v${installed}). Run: \`npm i -g @mariozechner/pi-coding-agent\``;
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
  }