@zigai/pi-mode 0.1.3 → 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 CHANGED
@@ -1,11 +1,15 @@
1
1
  # Pi Mode
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@zigai/pi-mode.svg?color=blue)](https://www.npmjs.com/package/@zigai/pi-mode)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@zigai/pi-mode.svg)](https://www.npmjs.com/package/@zigai/pi-mode)
5
+ [![license](https://img.shields.io/npm/l/@zigai/pi-mode.svg)](../../LICENSE)
6
+
3
7
  This Pi extension adds prompt modes for model and thinking-level switching.
4
8
 
5
9
  ## Install
6
10
 
7
11
  ```sh
8
- pi install git:github.com/zigai/pi-tweaks
12
+ pi install npm:@zigai/pi-mode
9
13
  ```
10
14
 
11
15
  ## Features
@@ -18,4 +22,8 @@ pi install git:github.com/zigai/pi-tweaks
18
22
 
19
23
  Modes can store a provider, model, thinking level, and optional color. Project-local modes live in `.pi/modes.json` when present; otherwise global modes live in `~/.pi/agent/modes.json`.
20
24
 
21
- By default, Pi Mode does not print the mode name in the editor border. To opt in, toggle `Show mode name` from `/mode` → `Configure modes…`, or set `"modeShowName": true` in `~/.pi/agent/settings.json`.
25
+ By default, Pi Mode does not print the mode name in the editor border. To opt in, toggle `Show mode name` from `/mode` → `Configure modes…`, or set `"modeShowName": true` in global `~/.pi/agent/settings.json` or trusted project `.pi/settings.json`. The `/mode` toggle writes to global settings.
26
+
27
+ ## License
28
+
29
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zigai/pi-mode",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Pi package that adds prompt modes for model and thinking-level switching.",
5
5
  "keywords": [
6
6
  "mode",
@@ -29,6 +29,9 @@
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
+ "dependencies": {
33
+ "typebox": "^1.1.38"
34
+ },
32
35
  "peerDependencies": {
33
36
  "@earendil-works/pi-agent-core": "*",
34
37
  "@earendil-works/pi-ai": "*",
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  handleSessionActivated,
8
8
  selectModeUI,
9
9
  } from "./mode-state.ts";
10
+ import { setSettingsContext } from "./settings.ts";
10
11
 
11
12
  export default function (pi: ExtensionAPI) {
12
13
  pi.registerCommand("mode", {
@@ -31,6 +32,7 @@ export default function (pi: ExtensionAPI) {
31
32
  });
32
33
 
33
34
  pi.on("session_start", async (_event, ctx) => {
35
+ setSettingsContext(ctx);
34
36
  await handleSessionActivated(pi, ctx);
35
37
  applyModeEditor(pi, ctx);
36
38
  });
package/src/mode-state.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  import type { Api, Model } from "@earendil-works/pi-ai";
8
8
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
9
9
  import fs from "node:fs/promises";
10
+ import { Type, type Static, type TSchema } from "typebox";
11
+ import { Value } from "typebox/value";
10
12
  import {
11
13
  ALL_THINKING_LEVELS,
12
14
  CUSTOM_MODE_NAME,
@@ -34,17 +36,51 @@ type ScopedModelItem = {
34
36
  thinkingLevel?: string;
35
37
  };
36
38
 
37
- type ModeSpecJson = {
38
- provider?: string;
39
- modelId?: string;
40
- thinkingLevel?: ThinkingLevel;
41
- color?: ModeSpec["color"];
42
- };
39
+ const ModeSpecJsonSchema = Type.Object({
40
+ provider: Type.Optional(Type.String()),
41
+ modelId: Type.Optional(Type.String()),
42
+ thinkingLevel: Type.Optional(Type.Unknown()),
43
+ color: Type.Optional(Type.String()),
44
+ });
45
+
46
+ const ModesFileJsonSchema = Type.Object({
47
+ $schema: Type.Optional(Type.String()),
48
+ version: Type.Optional(Type.Number()),
49
+ currentMode: Type.Optional(Type.String()),
50
+ modes: Type.Optional(Type.Record(Type.String(), ModeSpecJsonSchema)),
51
+ });
52
+
53
+ type ModeSpecJson = Static<typeof ModeSpecJsonSchema>;
54
+ type ModesFileJson = Static<typeof ModesFileJsonSchema>;
55
+
56
+ function formatSchemaPath(instancePath: string): string {
57
+ if (instancePath.length === 0) return "root";
58
+ return instancePath
59
+ .slice(1)
60
+ .split("/")
61
+ .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
62
+ .join(".");
63
+ }
43
64
 
44
- type ModesFileJson = {
45
- currentMode?: string;
46
- modes?: Record<string, ModeSpecJson>;
47
- };
65
+ function parseSchema(schema: TSchema, value: unknown, label: string): unknown {
66
+ const errors = [...Value.Errors(schema, value)];
67
+ if (errors.length > 0) {
68
+ const messages = errors
69
+ .slice(0, 5)
70
+ .map((error) => `${formatSchemaPath(error.instancePath)} ${error.message}`);
71
+ let suffix = "";
72
+ if (errors.length > messages.length) {
73
+ suffix = `; and ${errors.length - messages.length} more`;
74
+ }
75
+ throw new Error(`${label} is invalid: ${messages.join("; ")}${suffix}`);
76
+ }
77
+ const parsed: unknown = Value.Parse(schema, value);
78
+ return parsed;
79
+ }
80
+
81
+ function parseModesFileJson(value: unknown): ModesFileJson {
82
+ return parseSchema(ModesFileJsonSchema, value, "modes.json") as ModesFileJson;
83
+ }
48
84
 
49
85
  function cloneModesFile(file: ModesFile): ModesFile {
50
86
  return JSON.parse(JSON.stringify(file)) as ModesFile;
@@ -55,7 +91,7 @@ function modeSpec(modes: Record<string, ModeSpec>, name: string): ModeSpec | und
55
91
  return undefined;
56
92
  }
57
93
 
58
- function computeModesPatch(
94
+ export function computeModesPatch(
59
95
  base: ModesFile,
60
96
  next: ModesFile,
61
97
  includeCurrentMode: boolean,
@@ -108,7 +144,7 @@ function computeModesPatch(
108
144
  return patch;
109
145
  }
110
146
 
111
- function applyModesPatch(target: ModesFile, patch: ModesPatch): void {
147
+ export function applyModesPatch(target: ModesFile, patch: ModesPatch): void {
112
148
  if (patch.currentMode !== undefined) {
113
149
  target.currentMode = patch.currentMode;
114
150
  }
@@ -153,14 +189,30 @@ function applyModesPatch(target: ModesFile, patch: ModesPatch): void {
153
189
  }
154
190
  }
155
191
 
156
- function normalizeThinkingLevel(level: ThinkingLevel | undefined): ThinkingLevel | undefined {
157
- if (level === undefined) return undefined;
158
- if (ALL_THINKING_LEVELS.includes(level)) {
159
- return level;
192
+ function normalizeThinkingLevel(level: unknown): ThinkingLevel | undefined {
193
+ if (typeof level !== "string") return undefined;
194
+ if (ALL_THINKING_LEVELS.includes(level as ThinkingLevel)) {
195
+ return level as ThinkingLevel;
160
196
  }
161
197
  return undefined;
162
198
  }
163
199
 
200
+ function getLoadErrorCode(error: unknown): string | undefined {
201
+ if (!(error instanceof Error)) return undefined;
202
+ const code = (error as NodeJS.ErrnoException).code;
203
+ if (typeof code === "string") return code;
204
+ return undefined;
205
+ }
206
+
207
+ function errorMessage(error: unknown): string {
208
+ if (error instanceof Error) return error.message;
209
+ return String(error);
210
+ }
211
+
212
+ function throwLoadError(filePath: string, error: unknown): never {
213
+ throw new Error(`Failed to load ${filePath}: ${errorMessage(error)}`);
214
+ }
215
+
164
216
  function sanitizeModeSpec(spec: ModeSpecJson | undefined): ModeSpec {
165
217
  if (spec === undefined) return {};
166
218
 
@@ -169,7 +221,7 @@ function sanitizeModeSpec(spec: ModeSpecJson | undefined): ModeSpec {
169
221
  };
170
222
  if (spec.provider !== undefined) sanitized.provider = spec.provider;
171
223
  if (spec.modelId !== undefined) sanitized.modelId = spec.modelId;
172
- if (spec.color !== undefined) sanitized.color = spec.color;
224
+ if (spec.color !== undefined) sanitized.color = spec.color as ModeSpec["color"];
173
225
  return sanitized;
174
226
  }
175
227
 
@@ -225,10 +277,11 @@ async function loadModesFile(
225
277
  filePath: string,
226
278
  ctx: ExtensionContext,
227
279
  pi: ExtensionAPI,
280
+ options?: { throwOnInvalid?: boolean },
228
281
  ): Promise<ModesFile> {
229
282
  try {
230
283
  const raw = await fs.readFile(filePath, "utf8");
231
- const parsed = JSON.parse(raw) as ModesFileJson;
284
+ const parsed = parseModesFileJson(JSON.parse(raw));
232
285
  const currentMode = parsed.currentMode ?? "default";
233
286
  const modesRaw = parsed.modes ?? {};
234
287
 
@@ -244,7 +297,9 @@ async function loadModesFile(
244
297
  };
245
298
  ensureDefaultModeEntries(file, ctx, pi);
246
299
  return file;
247
- } catch {
300
+ } catch (error: unknown) {
301
+ if (getLoadErrorCode(error) === "ENOENT") return createDefaultModes(ctx, pi);
302
+ if (options?.throwOnInvalid === true) throwLoadError(filePath, error);
248
303
  return createDefaultModes(ctx, pi);
249
304
  }
250
305
  }
@@ -404,16 +459,23 @@ async function persistRuntime(pi: ExtensionAPI, ctx: ExtensionContext): Promise<
404
459
  const patch = computeModesPatch(runtime.baseline, runtime.data, false);
405
460
  if (patch === null) return;
406
461
 
407
- await withFileLock(runtime.filePath, async () => {
408
- const latest = await loadModesFile(runtime.filePath, ctx, pi);
409
- applyModesPatch(latest, patch);
410
- ensureDefaultModeEntries(latest, ctx, pi);
411
- await saveModesFile(runtime.filePath, latest);
412
-
413
- runtime.data = latest;
414
- runtime.baseline = cloneModesFile(latest);
415
- runtime.fileMtimeMs = await getMtimeMs(runtime.filePath);
416
- });
462
+ try {
463
+ await withFileLock(runtime.filePath, async () => {
464
+ const latest = await loadModesFile(runtime.filePath, ctx, pi, { throwOnInvalid: true });
465
+ applyModesPatch(latest, patch);
466
+ ensureDefaultModeEntries(latest, ctx, pi);
467
+ await saveModesFile(runtime.filePath, latest);
468
+
469
+ runtime.data = latest;
470
+ runtime.baseline = cloneModesFile(latest);
471
+ runtime.fileMtimeMs = await getMtimeMs(runtime.filePath);
472
+ });
473
+ } catch (error: unknown) {
474
+ if (ctx.hasUI) {
475
+ ctx.ui.notify(`Mode settings were not saved: ${errorMessage(error)}`, "error");
476
+ }
477
+ throw error;
478
+ }
417
479
  }
418
480
 
419
481
  function getCurrentSelectionSpec(pi: ExtensionAPI): ModeSpec {
@@ -810,7 +872,12 @@ async function configureModesUI(pi: ExtensionAPI, ctx: ExtensionContext): Promis
810
872
 
811
873
  if (choice === MODE_UI_SHOW_NAME_ON || choice === MODE_UI_SHOW_NAME_OFF) {
812
874
  const next = !shouldShowModeName();
813
- setShowModeName(next);
875
+ try {
876
+ setShowModeName(next);
877
+ } catch (error: unknown) {
878
+ ctx.ui.notify(`Mode name display was not saved: ${errorMessage(error)}`, "error");
879
+ continue;
880
+ }
814
881
  requestEditorRender?.();
815
882
  let displayState = "disabled";
816
883
  if (next) {
package/src/settings.ts CHANGED
@@ -1,59 +1,278 @@
1
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
1
+ import {
2
+ getAgentDir,
3
+ SettingsManager,
4
+ type ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ closeSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ openSync,
11
+ readFileSync,
12
+ renameSync,
13
+ statSync,
14
+ unlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
3
17
  import { dirname, join } from "node:path";
18
+ import { Type, type TSchema } from "typebox";
19
+ import { Value } from "typebox/value";
4
20
 
5
21
  export const SHOW_MODE_NAME_SETTINGS_KEY = "modeShowName";
6
22
 
23
+ const SETTINGS_LOCK_TIMEOUT_MS = 5_000;
24
+ const STALE_SETTINGS_LOCK_MS = 30_000;
25
+ const SettingsObjectSchema = Type.Object({});
26
+ const ShowModeNameSchema = Type.Boolean();
27
+
28
+ type SettingsReadContext = {
29
+ cwd: string;
30
+ projectTrusted: boolean;
31
+ };
32
+
33
+ let settingsReadContext: SettingsReadContext | undefined;
7
34
  let cachedShowModeName: boolean | undefined;
8
- let cachedSettingsMtimeMs: number | null | undefined;
35
+ let cachedSettingsMtimeKey: string | undefined;
36
+
37
+ type ProjectTrustContext = ExtensionContext & {
38
+ isProjectTrusted?: () => boolean;
39
+ };
40
+
41
+ function isProjectTrusted(ctx: ExtensionContext): boolean {
42
+ return (ctx as ProjectTrustContext).isProjectTrusted?.() ?? true;
43
+ }
44
+
45
+ export function setSettingsContext(ctx: ExtensionContext): void {
46
+ const next: SettingsReadContext = {
47
+ cwd: ctx.cwd,
48
+ projectTrusted: isProjectTrusted(ctx),
49
+ };
50
+ if (
51
+ settingsReadContext?.cwd !== next.cwd ||
52
+ settingsReadContext.projectTrusted !== next.projectTrusted
53
+ ) {
54
+ settingsReadContext = next;
55
+ cachedShowModeName = undefined;
56
+ cachedSettingsMtimeKey = undefined;
57
+ }
58
+ }
9
59
 
10
60
  function getSettingsPath(): string {
11
- return join(homedir(), ".pi", "agent", "settings.json");
61
+ return join(getAgentDir(), "settings.json");
62
+ }
63
+
64
+ function getProjectSettingsPath(): string | undefined {
65
+ if (settingsReadContext === undefined || !settingsReadContext.projectTrusted) {
66
+ return undefined;
67
+ }
68
+ return join(settingsReadContext.cwd, ".pi", "settings.json");
69
+ }
70
+
71
+ function getErrorCode(error: unknown): string | undefined {
72
+ if (!(error instanceof Error)) return undefined;
73
+ const code = (error as NodeJS.ErrnoException).code;
74
+ if (typeof code === "string") return code;
75
+ return undefined;
76
+ }
77
+
78
+ function throwError(error: unknown): never {
79
+ if (error instanceof Error) throw error;
80
+ throw new Error(String(error));
81
+ }
82
+
83
+ function sleepSync(ms: number): void {
84
+ const buffer = new SharedArrayBuffer(4);
85
+ Atomics.wait(new Int32Array(buffer), 0, 0, ms);
86
+ }
87
+
88
+ function parseOptionalBoolean(schema: TSchema, value: unknown): boolean | undefined {
89
+ if (value === undefined) return undefined;
90
+ if (!Value.Check(schema, value)) return undefined;
91
+ const parsed: unknown = Value.Parse(schema, value);
92
+ if (typeof parsed === "boolean") return parsed;
93
+ return undefined;
94
+ }
95
+
96
+ function withSettingsLock<T>(settingsPath: string, fn: () => T): T {
97
+ const lockPath = `${settingsPath}.lock`;
98
+ mkdirSync(dirname(lockPath), { recursive: true });
99
+
100
+ const start = Date.now();
101
+ while (true) {
102
+ try {
103
+ const fd = openSync(lockPath, "wx");
104
+ try {
105
+ writeFileSync(
106
+ fd,
107
+ `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
108
+ "utf8",
109
+ );
110
+ } catch {
111
+ // Ignore best-effort lock metadata.
112
+ }
113
+
114
+ try {
115
+ return fn();
116
+ } finally {
117
+ try {
118
+ closeSync(fd);
119
+ } catch {
120
+ // Ignore cleanup failures.
121
+ }
122
+ try {
123
+ unlinkSync(lockPath);
124
+ } catch {
125
+ // Ignore cleanup failures.
126
+ }
127
+ }
128
+ } catch (error: unknown) {
129
+ if (getErrorCode(error) !== "EEXIST") throwError(error);
130
+
131
+ try {
132
+ const stat = statSync(lockPath);
133
+ if (Date.now() - stat.mtimeMs > STALE_SETTINGS_LOCK_MS) {
134
+ unlinkSync(lockPath);
135
+ continue;
136
+ }
137
+ } catch {
138
+ // Ignore stale-lock checks.
139
+ }
140
+
141
+ if (Date.now() - start > SETTINGS_LOCK_TIMEOUT_MS) {
142
+ throw new Error(`Timed out waiting for lock: ${lockPath}`);
143
+ }
144
+ sleepSync(40 + Math.random() * 80);
145
+ }
146
+ }
147
+ }
148
+
149
+ function atomicWriteUtf8Sync(filePath: string, content: string): void {
150
+ mkdirSync(dirname(filePath), { recursive: true });
151
+
152
+ const tempPath = join(
153
+ dirname(filePath),
154
+ `.${filePath.split(/[\\/]/).pop() ?? "settings.json"}.tmp.${process.pid}.${Math.random()
155
+ .toString(16)
156
+ .slice(2)}`,
157
+ );
158
+
159
+ writeFileSync(tempPath, content, "utf8");
160
+
161
+ try {
162
+ renameSync(tempPath, filePath);
163
+ } catch (error: unknown) {
164
+ const code = getErrorCode(error);
165
+ if (code === "EEXIST" || code === "EPERM") {
166
+ try {
167
+ unlinkSync(filePath);
168
+ } catch {
169
+ // Ignore missing target before retrying the rename.
170
+ }
171
+ renameSync(tempPath, filePath);
172
+ return;
173
+ }
174
+ try {
175
+ unlinkSync(tempPath);
176
+ } catch {
177
+ // Ignore cleanup failures.
178
+ }
179
+ throwError(error);
180
+ }
12
181
  }
13
182
 
14
- function getSettingsMtimeMs(): number | null {
183
+ function getFileMtimeMs(filePath: string | undefined): number | null {
184
+ if (filePath === undefined) return null;
15
185
  try {
16
- if (!existsSync(getSettingsPath())) return null;
17
- return statSync(getSettingsPath()).mtimeMs;
186
+ if (!existsSync(filePath)) return null;
187
+ return statSync(filePath).mtimeMs;
18
188
  } catch {
19
189
  return null;
20
190
  }
21
191
  }
22
192
 
23
- function readSettingsObject(): Record<string, unknown> {
24
- try {
25
- const raw = readFileSync(getSettingsPath(), "utf8");
26
- const parsed = JSON.parse(raw) as unknown;
27
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
28
- return { ...parsed };
193
+ function getSettingsMtimeKey(): string {
194
+ return `${getFileMtimeMs(getSettingsPath())}:${getFileMtimeMs(getProjectSettingsPath())}`;
195
+ }
196
+
197
+ function formatSchemaPath(instancePath: string): string {
198
+ if (instancePath.length === 0) return "root";
199
+ return instancePath
200
+ .slice(1)
201
+ .split("/")
202
+ .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
203
+ .join(".");
204
+ }
205
+
206
+ function parseSettingsObject(value: unknown, settingsPath: string): Record<string, unknown> {
207
+ const errors = [...Value.Errors(SettingsObjectSchema, value)];
208
+ if (errors.length > 0) {
209
+ const messages = errors
210
+ .slice(0, 5)
211
+ .map((error) => `${formatSchemaPath(error.instancePath)} ${error.message}`);
212
+ let suffix = "";
213
+ if (errors.length > messages.length) {
214
+ suffix = `; and ${errors.length - messages.length} more`;
29
215
  }
30
- } catch {
31
- // Ignore malformed or missing settings files and fall back to defaults.
216
+ throw new Error(
217
+ `${settingsPath} must contain a JSON object: ${messages.join("; ")}${suffix}`,
218
+ );
219
+ }
220
+ return { ...(Value.Parse(SettingsObjectSchema, value) as Record<string, unknown>) };
221
+ }
222
+
223
+ function readSettingsObject(options?: { throwOnInvalid?: boolean }): Record<string, unknown> {
224
+ const settingsPath = getSettingsPath();
225
+ try {
226
+ const raw = readFileSync(settingsPath, "utf8");
227
+ return parseSettingsObject(JSON.parse(raw), settingsPath);
228
+ } catch (error: unknown) {
229
+ if (getErrorCode(error) === "ENOENT") return {};
230
+ if (options?.throwOnInvalid === true) throwError(error);
231
+ // Ignore malformed settings files while reading and fall back to defaults.
32
232
  }
33
233
 
34
234
  return {};
35
235
  }
36
236
 
237
+ function updateSettingsObject(update: (settings: Record<string, unknown>) => void): void {
238
+ const settingsPath = getSettingsPath();
239
+ withSettingsLock(settingsPath, () => {
240
+ const settings = readSettingsObject({ throwOnInvalid: true });
241
+ update(settings);
242
+ atomicWriteUtf8Sync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
243
+ });
244
+ }
245
+
246
+ function applyShowModeNameSetting(settings: Record<string, unknown>, fallback: boolean): boolean {
247
+ return (
248
+ parseOptionalBoolean(ShowModeNameSchema, settings[SHOW_MODE_NAME_SETTINGS_KEY]) ?? fallback
249
+ );
250
+ }
251
+
37
252
  export function shouldShowModeName(): boolean {
38
- const mtimeMs = getSettingsMtimeMs();
39
- if (cachedShowModeName !== undefined && cachedSettingsMtimeMs === mtimeMs) {
253
+ const mtimeKey = getSettingsMtimeKey();
254
+ if (cachedShowModeName !== undefined && cachedSettingsMtimeKey === mtimeKey) {
40
255
  return cachedShowModeName;
41
256
  }
42
257
 
43
- const settings = readSettingsObject();
44
- cachedSettingsMtimeMs = mtimeMs;
45
- cachedShowModeName = settings[SHOW_MODE_NAME_SETTINGS_KEY] === true;
258
+ const context = settingsReadContext ?? { cwd: process.cwd(), projectTrusted: false };
259
+ const manager = SettingsManager.create(context.cwd, getAgentDir(), {
260
+ projectTrusted: context.projectTrusted,
261
+ });
262
+ let show = false;
263
+ show = applyShowModeNameSetting(manager.getGlobalSettings() as Record<string, unknown>, show);
264
+ show = applyShowModeNameSetting(manager.getProjectSettings() as Record<string, unknown>, show);
265
+
266
+ cachedSettingsMtimeKey = mtimeKey;
267
+ cachedShowModeName = show;
46
268
  return cachedShowModeName;
47
269
  }
48
270
 
49
271
  export function setShowModeName(show: boolean): void {
50
- const settingsPath = getSettingsPath();
51
- const settings = readSettingsObject();
52
- settings[SHOW_MODE_NAME_SETTINGS_KEY] = show;
53
-
54
- mkdirSync(dirname(settingsPath), { recursive: true });
55
- writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
272
+ updateSettingsObject((settings) => {
273
+ settings[SHOW_MODE_NAME_SETTINGS_KEY] = show;
274
+ });
56
275
 
57
- cachedSettingsMtimeMs = getSettingsMtimeMs();
276
+ cachedSettingsMtimeKey = getSettingsMtimeKey();
58
277
  cachedShowModeName = show;
59
278
  }
package/src/storage.ts CHANGED
@@ -1,17 +1,9 @@
1
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
1
2
  import fs from "node:fs/promises";
2
- import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
- export function expandUserPath(value: string): string {
6
- if (value === "~") return os.homedir();
7
- if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
8
- return value;
9
- }
10
-
11
5
  export function getGlobalAgentDir(): string {
12
- const env = process.env.PI_CODING_AGENT_DIR;
13
- if (env !== undefined && env.length > 0) return expandUserPath(env);
14
- return path.join(os.homedir(), ".pi", "agent");
6
+ return getAgentDir();
15
7
  }
16
8
 
17
9
  export function getGlobalModesPath(): string {