@zigai/pi-mode 0.4.0 → 0.4.1

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/src/settings.ts DELETED
@@ -1,439 +0,0 @@
1
- import {
2
- CONFIG_DIR_NAME,
3
- getAgentDir,
4
- type ExtensionContext,
5
- } from "@earendil-works/pi-coding-agent";
6
- import {
7
- closeSync,
8
- mkdirSync,
9
- openSync,
10
- readFileSync,
11
- renameSync,
12
- statSync,
13
- unlinkSync,
14
- writeFileSync,
15
- } from "node:fs";
16
- import { dirname, join } from "node:path";
17
- import { Type, type TSchema } from "typebox";
18
- import { Value } from "typebox/value";
19
-
20
- export const SHOW_MODE_NAME_SETTINGS_KEY = "modeShowName";
21
- export const USE_THINKING_BORDER_COLORS_SETTINGS_KEY = "modeUseThinkingBorderColors";
22
- export const SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY = "modeShowThinkingLevelStatus";
23
-
24
- const SETTINGS_LOCK_TIMEOUT_MS = 5_000;
25
- const STALE_SETTINGS_LOCK_MS = 30_000;
26
- const EXTENSION_ID = "pi-mode";
27
- const CONFIG_FILE = "config.json";
28
- const SCHEMA_FILE = "config.schema.json";
29
- const ModeSpecJsonSchema = Type.Object(
30
- {
31
- provider: Type.Optional(Type.String()),
32
- modelId: Type.Optional(Type.String()),
33
- thinkingLevel: Type.Optional(Type.Unknown()),
34
- color: Type.Optional(Type.String()),
35
- },
36
- { additionalProperties: false },
37
- );
38
-
39
- const SettingsObjectSchema = Type.Object(
40
- {
41
- $schema: Type.Optional(Type.String()),
42
- version: Type.Optional(Type.Number()),
43
- currentMode: Type.Optional(Type.String()),
44
- [SHOW_MODE_NAME_SETTINGS_KEY]: Type.Optional(Type.Boolean()),
45
- [USE_THINKING_BORDER_COLORS_SETTINGS_KEY]: Type.Optional(Type.Boolean()),
46
- [SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY]: Type.Optional(Type.Boolean()),
47
- modes: Type.Optional(Type.Record(Type.String(), ModeSpecJsonSchema)),
48
- },
49
- { additionalProperties: false },
50
- );
51
- const BooleanSettingSchema = Type.Boolean();
52
-
53
- type SettingsReadContext = {
54
- cwd: string;
55
- projectTrusted: boolean;
56
- };
57
-
58
- const DEFAULT_MODE_CONFIG_FILE = {
59
- $schema: `./${SCHEMA_FILE}`,
60
- version: 1,
61
- currentMode: "default",
62
- [SHOW_MODE_NAME_SETTINGS_KEY]: false,
63
- [USE_THINKING_BORDER_COLORS_SETTINGS_KEY]: false,
64
- [SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY]: false,
65
- modes: {},
66
- };
67
-
68
- let settingsReadContext: SettingsReadContext | undefined;
69
- let cachedSettings:
70
- | {
71
- showModeName: boolean;
72
- useThinkingBorderColors: boolean;
73
- showThinkingLevelStatus: boolean;
74
- }
75
- | undefined;
76
-
77
- type ProjectTrustContext = ExtensionContext & {
78
- isProjectTrusted?: () => boolean;
79
- };
80
-
81
- function isProjectTrusted(ctx: ExtensionContext): boolean {
82
- return (ctx as ProjectTrustContext).isProjectTrusted?.() ?? true;
83
- }
84
-
85
- export function setSettingsContext(ctx: ExtensionContext): void {
86
- const next: SettingsReadContext = {
87
- cwd: ctx.cwd,
88
- projectTrusted: isProjectTrusted(ctx),
89
- };
90
- if (
91
- settingsReadContext?.cwd !== next.cwd ||
92
- settingsReadContext.projectTrusted !== next.projectTrusted
93
- ) {
94
- settingsReadContext = next;
95
- cachedSettings = undefined;
96
- }
97
- }
98
-
99
- function getSettingsPath(): string {
100
- return join(getAgentDir(), EXTENSION_ID, CONFIG_FILE);
101
- }
102
-
103
- function getSchemaPath(configPath: string): string {
104
- return join(dirname(configPath), SCHEMA_FILE);
105
- }
106
-
107
- function writeIfMissing(filePath: string, content: string): void {
108
- try {
109
- mkdirSync(dirname(filePath), { recursive: true });
110
- writeFileSync(filePath, content, { encoding: "utf8", flag: "wx" });
111
- } catch (error: unknown) {
112
- if (error instanceof Error && (error as NodeJS.ErrnoException).code === "EEXIST") return;
113
- if (error instanceof Error) throw error;
114
- throw new Error(String(error));
115
- }
116
- }
117
-
118
- function refreshSchemaFile(filePath: string, content: string): void {
119
- let temporaryPath: string | undefined;
120
- try {
121
- mkdirSync(dirname(filePath), { recursive: true });
122
- try {
123
- if (readFileSync(filePath, "utf8") === content) return;
124
- } catch (error: unknown) {
125
- if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "ENOENT") {
126
- throw error;
127
- }
128
- }
129
-
130
- const nextTemporaryPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
131
- writeFileSync(nextTemporaryPath, content, { encoding: "utf8", flag: "wx" });
132
- temporaryPath = nextTemporaryPath;
133
- renameSync(temporaryPath, filePath);
134
- temporaryPath = undefined;
135
- } catch (error: unknown) {
136
- if (temporaryPath !== undefined) {
137
- try {
138
- unlinkSync(temporaryPath);
139
- } catch {
140
- // Ignore cleanup failure while reporting the original scaffold failure.
141
- }
142
- }
143
- if (error instanceof Error) throw error;
144
- throw new Error(String(error));
145
- }
146
- }
147
-
148
- function readBundledSchema(): string | undefined {
149
- try {
150
- return readFileSync(new URL("../config.schema.json", import.meta.url), "utf8");
151
- } catch {
152
- return undefined;
153
- }
154
- }
155
-
156
- function scaffoldGlobalConfig(): void {
157
- const globalConfigPath = getSettingsPath();
158
- const schema = readBundledSchema();
159
- if (schema !== undefined) {
160
- refreshSchemaFile(getSchemaPath(globalConfigPath), schema);
161
- }
162
- writeIfMissing(globalConfigPath, `${JSON.stringify(DEFAULT_MODE_CONFIG_FILE, null, 2)}\n`);
163
- }
164
-
165
- function getProjectSettingsPath(): string | undefined {
166
- if (settingsReadContext === undefined || !settingsReadContext.projectTrusted) {
167
- return undefined;
168
- }
169
- return join(settingsReadContext.cwd, CONFIG_DIR_NAME, EXTENSION_ID, CONFIG_FILE);
170
- }
171
-
172
- function getErrorCode(error: unknown): string | undefined {
173
- if (!(error instanceof Error)) return undefined;
174
- const code = (error as NodeJS.ErrnoException).code;
175
- if (typeof code === "string") return code;
176
- return undefined;
177
- }
178
-
179
- function throwError(error: unknown): never {
180
- if (error instanceof Error) throw error;
181
- throw new Error(String(error));
182
- }
183
-
184
- function sleepSync(ms: number): void {
185
- const buffer = new SharedArrayBuffer(4);
186
- Atomics.wait(new Int32Array(buffer), 0, 0, ms);
187
- }
188
-
189
- function parseOptionalBoolean(schema: TSchema, value: unknown): boolean | undefined {
190
- if (value === undefined) return undefined;
191
- if (!Value.Check(schema, value)) return undefined;
192
- const parsed: unknown = Value.Parse(schema, value);
193
- if (typeof parsed === "boolean") return parsed;
194
- return undefined;
195
- }
196
-
197
- function withSettingsLock<T>(settingsPath: string, fn: () => T): T {
198
- const lockPath = `${settingsPath}.lock`;
199
- mkdirSync(dirname(lockPath), { recursive: true });
200
-
201
- const start = Date.now();
202
- while (true) {
203
- try {
204
- const fd = openSync(lockPath, "wx");
205
- try {
206
- writeFileSync(
207
- fd,
208
- `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`,
209
- "utf8",
210
- );
211
- } catch {
212
- // Ignore best-effort lock metadata.
213
- }
214
-
215
- try {
216
- return fn();
217
- } finally {
218
- try {
219
- closeSync(fd);
220
- } catch {
221
- // Ignore cleanup failures.
222
- }
223
- try {
224
- unlinkSync(lockPath);
225
- } catch {
226
- // Ignore cleanup failures.
227
- }
228
- }
229
- } catch (error: unknown) {
230
- if (getErrorCode(error) !== "EEXIST") throwError(error);
231
-
232
- try {
233
- const stat = statSync(lockPath);
234
- if (Date.now() - stat.mtimeMs > STALE_SETTINGS_LOCK_MS) {
235
- unlinkSync(lockPath);
236
- continue;
237
- }
238
- } catch {
239
- // Ignore stale-lock checks.
240
- }
241
-
242
- if (Date.now() - start > SETTINGS_LOCK_TIMEOUT_MS) {
243
- throw new Error(`Timed out waiting for lock: ${lockPath}`);
244
- }
245
- sleepSync(40 + Math.random() * 80);
246
- }
247
- }
248
- }
249
-
250
- function atomicWriteUtf8Sync(filePath: string, content: string): void {
251
- mkdirSync(dirname(filePath), { recursive: true });
252
-
253
- const tempPath = join(
254
- dirname(filePath),
255
- `.${filePath.split(/[\\/]/).pop() ?? "settings.json"}.tmp.${process.pid}.${Math.random()
256
- .toString(16)
257
- .slice(2)}`,
258
- );
259
-
260
- writeFileSync(tempPath, content, "utf8");
261
-
262
- try {
263
- renameSync(tempPath, filePath);
264
- } catch (error: unknown) {
265
- const code = getErrorCode(error);
266
- if (code === "EEXIST" || code === "EPERM") {
267
- try {
268
- unlinkSync(filePath);
269
- } catch {
270
- // Ignore missing target before retrying the rename.
271
- }
272
- renameSync(tempPath, filePath);
273
- return;
274
- }
275
- try {
276
- unlinkSync(tempPath);
277
- } catch {
278
- // Ignore cleanup failures.
279
- }
280
- throwError(error);
281
- }
282
- }
283
-
284
- function formatSchemaPath(instancePath: string): string {
285
- if (instancePath.length === 0) return "root";
286
- return instancePath
287
- .slice(1)
288
- .split("/")
289
- .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
290
- .join(".");
291
- }
292
-
293
- function parseSettingsObject(value: unknown, settingsPath: string): Record<string, unknown> {
294
- const errors = [...Value.Errors(SettingsObjectSchema, value)];
295
- if (errors.length > 0) {
296
- const messages = errors
297
- .slice(0, 5)
298
- .map((error) => `${formatSchemaPath(error.instancePath)} ${error.message}`);
299
- let suffix = "";
300
- if (errors.length > messages.length) {
301
- suffix = `; and ${errors.length - messages.length} more`;
302
- }
303
- throw new Error(
304
- `${settingsPath} must contain a JSON object: ${messages.join("; ")}${suffix}`,
305
- );
306
- }
307
- return Object.fromEntries(Object.entries(Value.Parse(SettingsObjectSchema, value)));
308
- }
309
-
310
- function readSettingsObject(
311
- settingsPath: string,
312
- options?: { throwOnInvalid?: boolean },
313
- ): Record<string, unknown> {
314
- if (settingsPath === getSettingsPath()) {
315
- scaffoldGlobalConfig();
316
- }
317
-
318
- try {
319
- const raw = readFileSync(settingsPath, "utf8");
320
- const parsedJson: unknown = JSON.parse(raw);
321
- return parseSettingsObject(parsedJson, settingsPath);
322
- } catch (error: unknown) {
323
- if (getErrorCode(error) === "ENOENT") return {};
324
- if (options?.throwOnInvalid === true) throwError(error);
325
- // Ignore malformed config files while reading and fall back to defaults.
326
- }
327
-
328
- return {};
329
- }
330
-
331
- function updateSettingsObject(update: (settings: Record<string, unknown>) => void): void {
332
- scaffoldGlobalConfig();
333
- const settingsPath = getSettingsPath();
334
- withSettingsLock(settingsPath, () => {
335
- const settings = readSettingsObject(settingsPath, { throwOnInvalid: true });
336
- update(settings);
337
- atomicWriteUtf8Sync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
338
- });
339
- }
340
-
341
- function applyBooleanSetting(
342
- settings: Record<string, unknown>,
343
- key: string,
344
- fallback: boolean,
345
- ): boolean {
346
- const parsed = parseOptionalBoolean(BooleanSettingSchema, settings[key]);
347
- if (parsed === undefined) return fallback;
348
- return parsed;
349
- }
350
-
351
- function readModeSettings(): {
352
- showModeName: boolean;
353
- useThinkingBorderColors: boolean;
354
- showThinkingLevelStatus: boolean;
355
- } {
356
- if (cachedSettings !== undefined) {
357
- return cachedSettings;
358
- }
359
-
360
- let showModeName = false;
361
- let useThinkingBorderColors = false;
362
- let showThinkingLevelStatus = false;
363
-
364
- const globalSettings = readSettingsObject(getSettingsPath());
365
- showModeName = applyBooleanSetting(globalSettings, SHOW_MODE_NAME_SETTINGS_KEY, showModeName);
366
- useThinkingBorderColors = applyBooleanSetting(
367
- globalSettings,
368
- USE_THINKING_BORDER_COLORS_SETTINGS_KEY,
369
- useThinkingBorderColors,
370
- );
371
- showThinkingLevelStatus = applyBooleanSetting(
372
- globalSettings,
373
- SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY,
374
- showThinkingLevelStatus,
375
- );
376
-
377
- const projectSettingsPath = getProjectSettingsPath();
378
- if (projectSettingsPath !== undefined) {
379
- const projectSettings = readSettingsObject(projectSettingsPath);
380
- showModeName = applyBooleanSetting(
381
- projectSettings,
382
- SHOW_MODE_NAME_SETTINGS_KEY,
383
- showModeName,
384
- );
385
- useThinkingBorderColors = applyBooleanSetting(
386
- projectSettings,
387
- USE_THINKING_BORDER_COLORS_SETTINGS_KEY,
388
- useThinkingBorderColors,
389
- );
390
- showThinkingLevelStatus = applyBooleanSetting(
391
- projectSettings,
392
- SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY,
393
- showThinkingLevelStatus,
394
- );
395
- }
396
-
397
- cachedSettings = {
398
- showModeName,
399
- useThinkingBorderColors,
400
- showThinkingLevelStatus,
401
- };
402
- return cachedSettings;
403
- }
404
-
405
- export function shouldShowModeName(): boolean {
406
- return readModeSettings().showModeName;
407
- }
408
-
409
- export function shouldUseThinkingBorderColors(): boolean {
410
- return readModeSettings().useThinkingBorderColors;
411
- }
412
-
413
- export function shouldShowThinkingLevelStatus(): boolean {
414
- return readModeSettings().showThinkingLevelStatus;
415
- }
416
-
417
- export function setShowModeName(show: boolean): void {
418
- updateSettingsObject((settings) => {
419
- settings[SHOW_MODE_NAME_SETTINGS_KEY] = show;
420
- });
421
-
422
- cachedSettings = undefined;
423
- }
424
-
425
- export function setUseThinkingBorderColors(useThinkingBorderColors: boolean): void {
426
- updateSettingsObject((settings) => {
427
- settings[USE_THINKING_BORDER_COLORS_SETTINGS_KEY] = useThinkingBorderColors;
428
- });
429
-
430
- cachedSettings = undefined;
431
- }
432
-
433
- export function setShowThinkingLevelStatus(showThinkingLevelStatus: boolean): void {
434
- updateSettingsObject((settings) => {
435
- settings[SHOW_THINKING_LEVEL_STATUS_SETTINGS_KEY] = showThinkingLevelStatus;
436
- });
437
-
438
- cachedSettings = undefined;
439
- }
package/src/status.ts DELETED
@@ -1,116 +0,0 @@
1
- import { dirname, join } from "node:path";
2
- import { fileURLToPath, pathToFileURL } from "node:url";
3
- import { shouldShowThinkingLevelStatus } from "./settings.ts";
4
-
5
- const STATUS_PATCH_MARKER = Symbol.for("zigai.pi-mode.thinking-status-patch");
6
- const STATUS_PATCH_STATE = Symbol.for("zigai.pi-mode.thinking-status-state");
7
- const THINKING_LEVEL_STATUS_PREFIX = "Thinking level: ";
8
-
9
- type InteractiveModePrototype = {
10
- showStatus(message: string): void;
11
- [STATUS_PATCH_MARKER]?: true;
12
- };
13
-
14
- type StatusPatchState = {
15
- shouldShowThinkingLevelStatus: () => boolean;
16
- };
17
-
18
- type InteractiveModeClass = {
19
- prototype: InteractiveModePrototype;
20
- };
21
-
22
- type InteractiveModeModule = {
23
- InteractiveMode?: InteractiveModeClass;
24
- };
25
-
26
- type ShowStatus = (this: InteractiveModePrototype, message: string) => void;
27
-
28
- type ThinkingLevelStatusPatchOptions = {
29
- readonly loadInteractiveModeModule?: () => Promise<InteractiveModeModule | undefined>;
30
- readonly shouldShowThinkingLevelStatus?: () => boolean;
31
- };
32
-
33
- function isShowStatus(value: unknown): value is ShowStatus {
34
- return typeof value === "function";
35
- }
36
-
37
- function getStatusPatchState(): StatusPatchState {
38
- const existing = Reflect.get(globalThis, STATUS_PATCH_STATE);
39
- if (typeof existing === "object" && existing !== null) {
40
- const reader = Reflect.get(existing, "shouldShowThinkingLevelStatus");
41
- if (typeof reader === "function") {
42
- return existing as StatusPatchState;
43
- }
44
- }
45
-
46
- const patchState: StatusPatchState = {
47
- shouldShowThinkingLevelStatus: () => true,
48
- };
49
- Reflect.set(globalThis, STATUS_PATCH_STATE, patchState);
50
- return patchState;
51
- }
52
-
53
- function warnStatusPatchUnavailable(error?: unknown): void {
54
- let suffix = "";
55
- if (error instanceof Error && error.message.length > 0) {
56
- suffix = `: ${error.message}`;
57
- }
58
- console.warn(
59
- `[pi-mode] thinking-level status patch unavailable; Pi internals may have changed${suffix}`,
60
- );
61
- }
62
-
63
- async function loadInteractiveModeModule(): Promise<InteractiveModeModule | undefined> {
64
- try {
65
- const codingAgentEntry = fileURLToPath(
66
- import.meta.resolve("@earendil-works/pi-coding-agent"),
67
- );
68
- const distDir = dirname(codingAgentEntry);
69
- const interactiveModePath = pathToFileURL(
70
- join(distDir, "modes/interactive/interactive-mode.js"),
71
- ).href;
72
- return (await import(interactiveModePath)) as InteractiveModeModule;
73
- } catch (error: unknown) {
74
- warnStatusPatchUnavailable(error);
75
- return undefined;
76
- }
77
- }
78
-
79
- export async function applyThinkingLevelStatusPatch(
80
- options: ThinkingLevelStatusPatchOptions = {},
81
- ): Promise<void> {
82
- const patchState = getStatusPatchState();
83
- patchState.shouldShowThinkingLevelStatus =
84
- options.shouldShowThinkingLevelStatus ?? shouldShowThinkingLevelStatus;
85
-
86
- const loadModule = options.loadInteractiveModeModule ?? loadInteractiveModeModule;
87
- const module = await loadModule();
88
- const prototype = module?.InteractiveMode?.prototype;
89
- if (prototype === undefined) return;
90
- if (prototype[STATUS_PATCH_MARKER] === true) return;
91
- if (typeof prototype.showStatus !== "function") {
92
- warnStatusPatchUnavailable(new Error("InteractiveMode.showStatus is not a function"));
93
- return;
94
- }
95
-
96
- const showStatusDescriptor = Object.getOwnPropertyDescriptor(prototype, "showStatus");
97
- const showStatus: unknown = showStatusDescriptor?.value;
98
- if (!isShowStatus(showStatus)) {
99
- warnStatusPatchUnavailable(new Error("InteractiveMode.showStatus descriptor is invalid"));
100
- return;
101
- }
102
-
103
- prototype.showStatus = function patchedShowStatus(
104
- this: InteractiveModePrototype,
105
- message: string,
106
- ): void {
107
- if (
108
- message.startsWith(THINKING_LEVEL_STATUS_PREFIX) &&
109
- patchState.shouldShowThinkingLevelStatus() === false
110
- ) {
111
- return;
112
- }
113
- showStatus.call(this, message);
114
- };
115
- prototype[STATUS_PATCH_MARKER] = true;
116
- }