pi-agent-extensions 0.3.2 → 0.3.4

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 CHANGED
@@ -98,6 +98,10 @@ pi
98
98
 
99
99
  You'll see a loader while context is extracted, then an editor to review the handoff prompt.
100
100
 
101
+ ## Changelog
102
+
103
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
104
+
101
105
  ## Update
102
106
 
103
107
  ```bash
@@ -458,8 +458,8 @@ pi.on("tool_result", (event, ctx) => {
458
458
  ```typescript
459
459
  // Store extension state in session
460
460
  pi.appendEntry<StateType>(CUSTOM_TYPE, stateData);
461
- // Restore on session switch
462
- pi.on("session_switch", (event, ctx) => {
461
+ // Restore on session start (covers startup, switch, fork, etc.)
462
+ pi.on("session_start", (event, ctx) => {
463
463
  applyState(ctx);
464
464
  });
465
465
  ```
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { complete, type Model, type Api, type UserMessage } from "@mariozechner/pi-ai";
14
14
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
15
+ import { getRequestAuth, hasRequestAuth } from "../shared/auth.js";
15
16
  import { BorderedLoader } from "@mariozechner/pi-coding-agent";
16
17
  import {
17
18
  type Component,
@@ -77,15 +78,17 @@ async function selectExtractionModel(
77
78
  currentModel: Model<Api>,
78
79
  modelRegistry: {
79
80
  find: (provider: string, modelId: string) => Model<Api> | undefined;
80
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
81
+ getApiKeyAndHeaders: (
82
+ model: Model<Api>,
83
+ ) => Promise<
84
+ | { ok: true; apiKey?: string; headers?: Record<string, string> }
85
+ | { ok: false; error: string }
86
+ >;
81
87
  },
82
88
  ): Promise<Model<Api>> {
83
89
  const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
84
- if (codexModel) {
85
- const apiKey = await modelRegistry.getApiKey(codexModel);
86
- if (apiKey) {
87
- return codexModel;
88
- }
90
+ if (codexModel && (await hasRequestAuth(modelRegistry, codexModel))) {
91
+ return codexModel;
89
92
  }
90
93
 
91
94
  const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
@@ -93,8 +96,7 @@ async function selectExtractionModel(
93
96
  return currentModel;
94
97
  }
95
98
 
96
- const apiKey = await modelRegistry.getApiKey(haikuModel);
97
- if (!apiKey) {
99
+ if (!(await hasRequestAuth(modelRegistry, haikuModel))) {
98
100
  return currentModel;
99
101
  }
100
102
 
@@ -457,7 +459,7 @@ export default function (pi: ExtensionAPI) {
457
459
  loader.onAbort = () => done(null);
458
460
 
459
461
  const doExtract = async () => {
460
- const apiKey = await ctx.modelRegistry.getApiKey(extractionModel);
462
+ const requestAuth = await getRequestAuth(ctx.modelRegistry, extractionModel);
461
463
  const userMessage: UserMessage = {
462
464
  role: "user",
463
465
  content: [{ type: "text", text: lastAssistantText! }],
@@ -467,7 +469,7 @@ export default function (pi: ExtensionAPI) {
467
469
  const response = await complete(
468
470
  extractionModel,
469
471
  { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
470
- { apiKey, signal: loader.signal },
472
+ { ...requestAuth, signal: loader.signal },
471
473
  );
472
474
 
473
475
  if (response.stopReason === "aborted") {
@@ -40,7 +40,7 @@ export async function createPendingFile(params: AskUserParams, ctx: ExtensionCon
40
40
  };
41
41
 
42
42
  // Write file
43
- await writeFile(pendingFile, JSON.stringify(pending, null, 2), "utf-8");
43
+ await writeFile(pendingFile, JSON.stringify(pending, null, 2), { encoding: "utf-8", mode: 0o600 });
44
44
 
45
45
  return pendingFile;
46
46
  }
@@ -38,9 +38,11 @@
38
38
  import type { ExtensionAPI, ExtensionContext, TurnEndEvent, MessageRenderer } from "@mariozechner/pi-coding-agent";
39
39
  import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
40
40
  import { complete, type Model, type Api, type UserMessage, type TextContent } from "@mariozechner/pi-ai";
41
+ import { getRequestAuth, hasRequestAuth } from "../shared/auth.js";
41
42
  import { StringEnum } from "@mariozechner/pi-ai";
42
43
  import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
43
44
  import { Type } from "@sinclair/typebox";
45
+ import crypto from "node:crypto";
44
46
  import { promises as fs } from "node:fs";
45
47
  import * as net from "node:net";
46
48
  import * as os from "node:os";
@@ -155,20 +157,19 @@ async function selectSummarizationModel(
155
157
  currentModel: Model<Api> | undefined,
156
158
  modelRegistry: {
157
159
  find: (provider: string, modelId: string) => Model<Api> | undefined;
158
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
160
+ getApiKeyAndHeaders: (
161
+ model: Model<Api>,
162
+ ) => Promise<
163
+ | { ok: true; apiKey?: string; headers?: Record<string, string> }
164
+ | { ok: false; error: string }
165
+ >;
159
166
  },
160
167
  ): Promise<Model<Api> | undefined> {
161
168
  const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
162
- if (codexModel) {
163
- const apiKey = await modelRegistry.getApiKey(codexModel);
164
- if (apiKey) return codexModel;
165
- }
169
+ if (codexModel && (await hasRequestAuth(modelRegistry, codexModel))) return codexModel;
166
170
 
167
171
  const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
168
- if (haikuModel) {
169
- const apiKey = await modelRegistry.getApiKey(haikuModel);
170
- if (apiKey) return haikuModel;
171
- }
172
+ if (haikuModel && (await hasRequestAuth(modelRegistry, haikuModel))) return haikuModel;
172
173
 
173
174
  return currentModel;
174
175
  }
@@ -603,7 +604,7 @@ async function handleCommand(
603
604
  // Subscribe to turn_end
604
605
  if (command.type === "subscribe") {
605
606
  if (command.event === "turn_end") {
606
- const subscriptionId = id ?? `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
607
+ const subscriptionId = id ?? `sub_${crypto.randomBytes(16).toString("hex")}`;
607
608
  state.turnEndSubscriptions.push({ socket, subscriptionId });
608
609
 
609
610
  const cleanup = () => {
@@ -645,9 +646,9 @@ async function handleCommand(
645
646
  return;
646
647
  }
647
648
 
648
- const apiKey = await ctx.modelRegistry.getApiKey(model);
649
- if (!apiKey) {
650
- respond(false, "get_summary", undefined, "No API key available for summarization model");
649
+ const requestAuth = await getRequestAuth(ctx.modelRegistry, model);
650
+ if (!requestAuth) {
651
+ respond(false, "get_summary", undefined, "No auth available for summarization model");
651
652
  return;
652
653
  }
653
654
 
@@ -665,7 +666,7 @@ async function handleCommand(
665
666
  const response = await complete(
666
667
  model,
667
668
  { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: [userMessage] },
668
- { apiKey },
669
+ requestAuth,
669
670
  );
670
671
 
671
672
  if (response.stopReason === "aborted" || response.stopReason === "error") {
@@ -1004,10 +1005,6 @@ export default function (pi: ExtensionAPI) {
1004
1005
  await refreshServer(ctx);
1005
1006
  });
1006
1007
 
1007
- pi.on("session_switch", async (_event, ctx) => {
1008
- await refreshServer(ctx);
1009
- });
1010
-
1011
1008
  pi.on("session_shutdown", async () => {
1012
1009
  if (state.aliasTimer) {
1013
1010
  clearInterval(state.aliasTimer);
@@ -230,8 +230,4 @@ export default function (pi: ExtensionAPI) {
230
230
  pi.on("session_start", (_event, ctx) => {
231
231
  applyEditorWithHistory(pi, ctx);
232
232
  });
233
-
234
- pi.on("session_switch", (_event, ctx) => {
235
- applyEditorWithHistory(pi, ctx);
236
- });
237
233
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { spawnSync } from "node:child_process";
10
+ import crypto from "node:crypto";
10
11
  import {
11
12
  existsSync,
12
13
  mkdtempSync,
@@ -24,7 +25,7 @@ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
24
25
  import {
25
26
  Container,
26
27
  fuzzyFilter,
27
- getEditorKeybindings,
28
+ getKeybindings,
28
29
  Input,
29
30
  matchesKey,
30
31
  type SelectItem,
@@ -693,10 +694,10 @@ const openPath = async (pi: ExtensionAPI, ctx: ExtensionContext, target: FileEnt
693
694
  };
694
695
 
695
696
  const openExternalEditor = (tui: TUI, editorCmd: string, content: string): string | null => {
696
- const tmpFile = path.join(os.tmpdir(), `pi-files-edit-${Date.now()}.txt`);
697
+ const tmpFile = path.join(os.tmpdir(), `pi-files-edit-${crypto.randomBytes(16).toString("hex")}.txt`);
697
698
 
698
699
  try {
699
- writeFileSync(tmpFile, content, "utf8");
700
+ writeFileSync(tmpFile, content, { encoding: "utf8", mode: 0o600 });
700
701
  tui.stop();
701
702
 
702
703
  const [editor, ...editorArgs] = editorCmd.split(" ");
@@ -813,15 +814,15 @@ const openDiff = async (pi: ExtensionAPI, ctx: ExtensionContext, target: FileEnt
813
814
  ctx.ui.notify(errorMessage, "error");
814
815
  return;
815
816
  }
816
- writeFileSync(tmpFile, result.stdout ?? "", "utf8");
817
+ writeFileSync(tmpFile, result.stdout ?? "", { encoding: "utf8", mode: 0o600 });
817
818
  } else {
818
- writeFileSync(tmpFile, "", "utf8");
819
+ writeFileSync(tmpFile, "", { encoding: "utf8", mode: 0o600 });
819
820
  }
820
821
 
821
822
  let workingPath = target.resolvedPath;
822
823
  if (!existsSync(target.resolvedPath)) {
823
824
  workingPath = path.join(tmpDir, `pi-files-working-${path.basename(target.displayPath)}`);
824
- writeFileSync(workingPath, "", "utf8");
825
+ writeFileSync(workingPath, "", { encoding: "utf8", mode: 0o600 });
825
826
  }
826
827
 
827
828
  const openResult = await pi.exec("code", ["--diff", tmpFile, workingPath], { cwd: gitRoot });
@@ -937,16 +938,16 @@ const showFileSelector = async (
937
938
  }
938
939
  }
939
940
 
940
- const kb = getEditorKeybindings();
941
+ const kb = getKeybindings();
941
942
  if (
942
- kb.matches(data, "selectUp") ||
943
- kb.matches(data, "selectDown") ||
944
- kb.matches(data, "selectConfirm") ||
945
- kb.matches(data, "selectCancel")
943
+ kb.matches(data, "tui.select.up") ||
944
+ kb.matches(data, "tui.select.down") ||
945
+ kb.matches(data, "tui.select.confirm") ||
946
+ kb.matches(data, "tui.select.cancel")
946
947
  ) {
947
948
  if (selectList) {
948
949
  selectList.handleInput(data);
949
- } else if (kb.matches(data, "selectCancel")) {
950
+ } else if (kb.matches(data, "tui.select.cancel")) {
950
951
  done(null);
951
952
  }
952
953
  tui.requestRender();
@@ -22,6 +22,7 @@ import {
22
22
  serializeConversation,
23
23
  } from "@mariozechner/pi-coding-agent";
24
24
 
25
+ import { getRequestAuthOrThrow } from "../shared/auth.js";
25
26
  import { loadConfig, validateGoal } from "./config.js";
26
27
  import { ProgressLoader, EXTRACTION_PHASES } from "./progress.js";
27
28
  import {
@@ -290,7 +291,7 @@ async function doExtraction(
290
291
  model: Model<any>,
291
292
  signal?: AbortSignal,
292
293
  ): Promise<ExtractionResult> {
293
- const apiKey = await ctx.modelRegistry.getApiKey(model);
294
+ const requestAuth = await getRequestAuthOrThrow(ctx.modelRegistry, model);
294
295
 
295
296
  // Build user message
296
297
  const userMessage: Message = {
@@ -305,7 +306,7 @@ async function doExtraction(
305
306
  const response = await complete(
306
307
  model,
307
308
  { systemPrompt: EXTRACTION_SYSTEM_PROMPT, messages: [userMessage] },
308
- { apiKey, signal },
309
+ { ...requestAuth, signal },
309
310
  );
310
311
 
311
312
  if (response.stopReason === "aborted") {
@@ -347,7 +348,7 @@ async function doExtraction(
347
348
  systemPrompt: EXTRACTION_SYSTEM_PROMPT,
348
349
  messages: [userMessage, assistantMessage, retryMessage],
349
350
  },
350
- { apiKey, signal },
351
+ { ...requestAuth, signal },
351
352
  );
352
353
 
353
354
  if (retryResponse.stopReason === "aborted") {
@@ -383,7 +384,7 @@ async function doExtractionWithPhases(
383
384
  signal: AbortSignal,
384
385
  onPhase: (phase: string) => void,
385
386
  ): Promise<ExtractionResult> {
386
- const apiKey = await ctx.modelRegistry.getApiKey(model);
387
+ const requestAuth = await getRequestAuthOrThrow(ctx.modelRegistry, model);
387
388
 
388
389
  // Phase 1: Analyzing conversation
389
390
  onPhase(EXTRACTION_PHASES[0]);
@@ -404,7 +405,7 @@ async function doExtractionWithPhases(
404
405
  const response = await complete(
405
406
  model,
406
407
  { systemPrompt: EXTRACTION_SYSTEM_PROMPT, messages: [userMessage] },
407
- { apiKey, signal },
408
+ { ...requestAuth, signal },
408
409
  );
409
410
 
410
411
  if (response.stopReason === "aborted") {
@@ -451,7 +452,7 @@ async function doExtractionWithPhases(
451
452
  systemPrompt: EXTRACTION_SYSTEM_PROMPT,
452
453
  messages: [userMessage, assistantMessage, retryMessage],
453
454
  },
454
- { apiKey, signal },
455
+ { ...requestAuth, signal },
455
456
  );
456
457
 
457
458
  if (retryResponse.stopReason === "aborted") {
@@ -8,10 +8,11 @@
8
8
 
9
9
  import { Type } from "@sinclair/typebox";
10
10
  import { complete, type Api, type Model, type UserMessage } from "@mariozechner/pi-ai";
11
- import type { ExtensionAPI, ExtensionContext, SessionSwitchEvent } from "@mariozechner/pi-coding-agent";
11
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
12
  import { compact } from "@mariozechner/pi-coding-agent";
13
13
  import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
14
14
  import { DynamicBorder } from "@mariozechner/pi-coding-agent";
15
+ import { getRequestAuth, hasRequestAuth } from "../shared/auth.js";
15
16
 
16
17
  type LoopMode = "tests" | "custom" | "self";
17
18
 
@@ -87,22 +88,22 @@ function getConditionText(mode: LoopMode, condition?: string): string {
87
88
 
88
89
  async function selectSummaryModel(
89
90
  ctx: ExtensionContext,
90
- ): Promise<{ model: Model<Api>; apiKey: string } | null> {
91
+ ): Promise<{ model: Model<Api>; requestAuth: { apiKey?: string; headers?: Record<string, string> } } | null> {
91
92
  if (!ctx.model) return null;
92
93
 
93
94
  if (ctx.model.provider === "anthropic") {
94
95
  const haikuModel = ctx.modelRegistry.find("anthropic", HAIKU_MODEL_ID);
95
- if (haikuModel) {
96
- const apiKey = await ctx.modelRegistry.getApiKey(haikuModel);
97
- if (apiKey) {
98
- return { model: haikuModel, apiKey };
96
+ if (haikuModel && (await hasRequestAuth(ctx.modelRegistry, haikuModel))) {
97
+ const requestAuth = await getRequestAuth(ctx.modelRegistry, haikuModel);
98
+ if (requestAuth) {
99
+ return { model: haikuModel, requestAuth };
99
100
  }
100
101
  }
101
102
  }
102
103
 
103
- const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
104
- if (!apiKey) return null;
105
- return { model: ctx.model, apiKey };
104
+ const requestAuth = await getRequestAuth(ctx.modelRegistry, ctx.model);
105
+ if (!requestAuth) return null;
106
+ return { model: ctx.model, requestAuth };
106
107
  }
107
108
 
108
109
  async function summarizeBreakoutCondition(
@@ -124,7 +125,7 @@ async function summarizeBreakoutCondition(
124
125
  const response = await complete(
125
126
  selection.model,
126
127
  { systemPrompt: SUMMARY_SYSTEM_PROMPT, messages: [userMessage] },
127
- { apiKey: selection.apiKey },
128
+ selection.requestAuth,
128
129
  );
129
130
 
130
131
  if (response.stopReason === "aborted" || response.stopReason === "error") {
@@ -400,15 +401,22 @@ export default function loopExtension(pi: ExtensionAPI): void {
400
401
 
401
402
  pi.on("session_before_compact", async (event, ctx) => {
402
403
  if (!loopState.active || !loopState.mode || !ctx.model) return;
403
- const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
404
- if (!apiKey) return;
404
+ const requestAuth = await getRequestAuth(ctx.modelRegistry, ctx.model);
405
+ if (!requestAuth) return;
405
406
 
406
407
  const instructionParts = [event.customInstructions, getCompactionInstructions(loopState.mode, loopState.condition)]
407
408
  .filter(Boolean)
408
409
  .join("\n\n");
409
410
 
410
411
  try {
411
- const compaction = await compact(event.preparation, ctx.model, apiKey, instructionParts, event.signal);
412
+ const compaction = await compact(
413
+ event.preparation,
414
+ ctx.model,
415
+ requestAuth.apiKey ?? "",
416
+ requestAuth.headers,
417
+ instructionParts,
418
+ event.signal,
419
+ );
412
420
  return { compaction };
413
421
  } catch (error) {
414
422
  if (ctx.hasUI) {
@@ -439,8 +447,4 @@ export default function loopExtension(pi: ExtensionAPI): void {
439
447
  pi.on("session_start", async (_event, ctx) => {
440
448
  await restoreLoopState(ctx);
441
449
  });
442
-
443
- pi.on("session_switch", async (_event: SessionSwitchEvent, ctx) => {
444
- await restoreLoopState(ctx);
445
- });
446
450
  }
@@ -69,7 +69,7 @@ export function loadModelsConfigSync(): NvidiaModelsConfig | null {
69
69
  export async function saveModelsConfig(config: NvidiaModelsConfig): Promise<void> {
70
70
  const configPath = getConfigPath();
71
71
  await fs.mkdir(path.dirname(configPath), { recursive: true });
72
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
72
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 0o600 });
73
73
  }
74
74
 
75
75
  /** Parse model IDs from editor text, ignoring comments and blank lines.
@@ -485,10 +485,6 @@ export default function reviewExtension(pi: ExtensionAPI) {
485
485
  applyReviewState(ctx);
486
486
  });
487
487
 
488
- pi.on("session_switch", (_event, ctx) => {
489
- applyReviewState(ctx);
490
- });
491
-
492
488
  pi.on("session_tree", (_event, ctx) => {
493
489
  applyReviewState(ctx);
494
490
  });
@@ -67,7 +67,7 @@ async function listSessions(ctx: ExtensionCommandContext): Promise<SessionInfoLi
67
67
  container.addChild(new DynamicBorder(borderColor));
68
68
  container.addChild(loader);
69
69
  container.addChild(new Spacer(1));
70
- container.addChild(new Text(keyHint("selectCancel", "cancel"), 1, 0));
70
+ container.addChild(new Text(keyHint("tui.select.cancel", "cancel"), 1, 0));
71
71
  container.addChild(new Spacer(1));
72
72
  container.addChild(new DynamicBorder(borderColor));
73
73
 
@@ -0,0 +1,40 @@
1
+ import type { Api, Model, ProviderStreamOptions } from "@mariozechner/pi-ai";
2
+
3
+ export type RequestAuth = Pick<ProviderStreamOptions, "apiKey" | "headers">;
4
+
5
+ type ModelRegistryWithRequestAuth = {
6
+ getApiKeyAndHeaders: (
7
+ model: Model<Api>,
8
+ ) => Promise<
9
+ | { ok: true; apiKey?: string; headers?: Record<string, string> }
10
+ | { ok: false; error: string }
11
+ >;
12
+ };
13
+
14
+ export async function getRequestAuth(
15
+ modelRegistry: ModelRegistryWithRequestAuth,
16
+ model: Model<Api>,
17
+ ): Promise<RequestAuth | undefined> {
18
+ const auth = await modelRegistry.getApiKeyAndHeaders(model);
19
+ if (!auth.ok) return undefined;
20
+ return { apiKey: auth.apiKey, headers: auth.headers };
21
+ }
22
+
23
+ export async function getRequestAuthOrThrow(
24
+ modelRegistry: ModelRegistryWithRequestAuth,
25
+ model: Model<Api>,
26
+ ): Promise<RequestAuth> {
27
+ const auth = await modelRegistry.getApiKeyAndHeaders(model);
28
+ if (!auth.ok) {
29
+ throw new Error(auth.error);
30
+ }
31
+ return { apiKey: auth.apiKey, headers: auth.headers };
32
+ }
33
+
34
+ export async function hasRequestAuth(
35
+ modelRegistry: ModelRegistryWithRequestAuth,
36
+ model: Model<Api>,
37
+ ): Promise<boolean> {
38
+ const auth = await modelRegistry.getApiKeyAndHeaders(model);
39
+ return auth.ok;
40
+ }
@@ -48,7 +48,7 @@ import {
48
48
  Text,
49
49
  TUI,
50
50
  fuzzyMatch,
51
- getEditorKeybindings,
51
+ getKeybindings,
52
52
  matchesKey,
53
53
  truncateToWidth,
54
54
  visibleWidth,
@@ -397,25 +397,25 @@ class TodoSelectorComponent extends Container implements Focusable {
397
397
  }
398
398
 
399
399
  handleInput(keyData: string): void {
400
- const kb = getEditorKeybindings();
401
- if (kb.matches(keyData, "selectUp")) {
400
+ const kb = getKeybindings();
401
+ if (kb.matches(keyData, "tui.select.up")) {
402
402
  if (this.filteredTodos.length === 0) return;
403
403
  this.selectedIndex = this.selectedIndex === 0 ? this.filteredTodos.length - 1 : this.selectedIndex - 1;
404
404
  this.updateList();
405
405
  return;
406
406
  }
407
- if (kb.matches(keyData, "selectDown")) {
407
+ if (kb.matches(keyData, "tui.select.down")) {
408
408
  if (this.filteredTodos.length === 0) return;
409
409
  this.selectedIndex = this.selectedIndex === this.filteredTodos.length - 1 ? 0 : this.selectedIndex + 1;
410
410
  this.updateList();
411
411
  return;
412
412
  }
413
- if (kb.matches(keyData, "selectConfirm")) {
413
+ if (kb.matches(keyData, "tui.select.confirm")) {
414
414
  const selected = this.filteredTodos[this.selectedIndex];
415
415
  if (selected) this.onSelectCallback(selected);
416
416
  return;
417
417
  }
418
- if (kb.matches(keyData, "selectCancel")) {
418
+ if (kb.matches(keyData, "tui.select.cancel")) {
419
419
  this.onCancelCallback();
420
420
  return;
421
421
  }
@@ -573,28 +573,28 @@ class TodoDetailOverlayComponent {
573
573
  }
574
574
 
575
575
  handleInput(keyData: string): void {
576
- const kb = getEditorKeybindings();
577
- if (kb.matches(keyData, "selectCancel")) {
576
+ const kb = getKeybindings();
577
+ if (kb.matches(keyData, "tui.select.cancel")) {
578
578
  this.onAction("back");
579
579
  return;
580
580
  }
581
- if (kb.matches(keyData, "selectConfirm")) {
581
+ if (kb.matches(keyData, "tui.select.confirm")) {
582
582
  this.onAction("work");
583
583
  return;
584
584
  }
585
- if (kb.matches(keyData, "selectUp")) {
585
+ if (kb.matches(keyData, "tui.select.up")) {
586
586
  this.scrollBy(-1);
587
587
  return;
588
588
  }
589
- if (kb.matches(keyData, "selectDown")) {
589
+ if (kb.matches(keyData, "tui.select.down")) {
590
590
  this.scrollBy(1);
591
591
  return;
592
592
  }
593
- if (kb.matches(keyData, "selectPageUp")) {
593
+ if (kb.matches(keyData, "tui.select.pageUp")) {
594
594
  this.scrollBy(-this.viewHeight || -1);
595
595
  return;
596
596
  }
597
- if (kb.matches(keyData, "selectPageDown")) {
597
+ if (kb.matches(keyData, "tui.select.pageDown")) {
598
598
  this.scrollBy(this.viewHeight || 1);
599
599
  return;
600
600
  }
@@ -923,7 +923,7 @@ async function readTodoFile(filePath: string, idFallback: string): Promise<TodoR
923
923
  }
924
924
 
925
925
  async function writeTodoFile(filePath: string, todo: TodoRecord) {
926
- await fs.writeFile(filePath, serializeTodo(todo), "utf8");
926
+ await fs.writeFile(filePath, serializeTodo(todo), { encoding: "utf8", mode: 0o600 });
927
927
  }
928
928
 
929
929
  async function generateTodoId(todosDir: string): Promise<string> {
@@ -955,14 +955,14 @@ async function acquireLock(
955
955
 
956
956
  for (let attempt = 0; attempt < 2; attempt += 1) {
957
957
  try {
958
- const handle = await fs.open(lockPath, "wx");
958
+ const handle = await fs.open(lockPath, "wx", 0o600);
959
959
  const info: LockInfo = {
960
960
  id,
961
961
  pid: process.pid,
962
962
  session,
963
963
  created_at: new Date(now).toISOString(),
964
964
  };
965
- await handle.writeFile(JSON.stringify(info, null, 2), "utf8");
965
+ await handle.writeFile(JSON.stringify(info, null, 2), { encoding: "utf8", mode: 0o600 });
966
966
  await handle.close();
967
967
  return async () => {
968
968
  try {
@@ -1256,7 +1256,7 @@ function renderTodoDetail(theme: Theme, todo: TodoRecord, expanded: boolean): st
1256
1256
  }
1257
1257
 
1258
1258
  function appendExpandHint(theme: Theme, text: string): string {
1259
- return `${text}\n${theme.fg("dim", `(${keyHint("expandTools", "to expand")})`)}`;
1259
+ return `${text}\n${theme.fg("dim", `(${keyHint("app.tools.expand", "to expand")})`)}`;
1260
1260
  }
1261
1261
 
1262
1262
  async function ensureTodoExists(filePath: string, id: string): Promise<TodoRecord | null> {
@@ -263,7 +263,7 @@ async function saveStateToSettings(): Promise<void> {
263
263
  } satisfies PersistedWhimsyConfig;
264
264
 
265
265
  await fs.mkdir(dir, { recursive: true });
266
- await fs.writeFile(settingsPath, JSON.stringify(parsed, null, 2), "utf-8");
266
+ await fs.writeFile(settingsPath, JSON.stringify(parsed, null, 2), { encoding: "utf-8", mode: 0o600 });
267
267
  }
268
268
 
269
269
  async function ensureStateLoaded(): Promise<void> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-extensions",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Collection of extensions for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@sinclair/typebox": "^0.34.48",
65
- "tsx": "^4.19.0"
65
+ "tsx": "^4.21.0"
66
66
  },
67
67
  "scripts": {
68
68
  "test": "node --import tsx --test 'tests/**/*.test.ts' 'tests/*.test.ts'"