@aexol/spectral 0.7.1 → 0.7.5

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.
Files changed (219) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/agent/agents.js +1 -1
  3. package/dist/agent/index.js +199 -184
  4. package/dist/commands/serve.js +0 -3
  5. package/dist/designer/data/systems/renault/DESIGN.md +1 -1
  6. package/dist/designer/philosophies.js +668 -0
  7. package/dist/mcp/sampling-handler.js +1 -1
  8. package/dist/memory/commands/status.js +1 -1
  9. package/dist/memory/compaction.js +2 -2
  10. package/dist/memory/config.js +1 -1
  11. package/dist/memory/debug-log.js +1 -1
  12. package/dist/memory/hooks/compaction-hook.js +29 -0
  13. package/dist/memory/index.js +2 -0
  14. package/dist/memory/observer.js +2 -2
  15. package/dist/memory/project-observations-store.js +14 -0
  16. package/dist/memory/tokens.js +1 -1
  17. package/dist/memory/tools/read-project-observations.js +82 -0
  18. package/dist/memory/tools/recall-observation.js +2 -2
  19. package/dist/pi/agent-core/agent-loop.js +501 -0
  20. package/dist/pi/agent-core/agent.js +401 -0
  21. package/dist/pi/agent-core/harness/agent-harness.js +899 -0
  22. package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
  23. package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
  24. package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
  25. package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
  26. package/dist/pi/agent-core/harness/messages.js +101 -0
  27. package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
  28. package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
  29. package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
  30. package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
  31. package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
  32. package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
  33. package/dist/pi/agent-core/harness/session/session.js +196 -0
  34. package/dist/pi/agent-core/harness/session/uuid.js +49 -0
  35. package/dist/pi/agent-core/harness/skills.js +310 -0
  36. package/dist/pi/agent-core/harness/system-prompt.js +29 -0
  37. package/dist/pi/agent-core/harness/types.js +93 -0
  38. package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
  39. package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
  40. package/dist/pi/agent-core/index.js +24 -0
  41. package/dist/pi/agent-core/node.js +2 -0
  42. package/dist/pi/agent-core/proxy.js +277 -0
  43. package/dist/pi/agent-core/types.js +1 -0
  44. package/dist/pi/ai/api-registry.js +43 -0
  45. package/dist/pi/ai/cli.js +120 -0
  46. package/dist/pi/ai/env-api-keys.js +169 -0
  47. package/dist/pi/ai/image-models.generated.js +441 -0
  48. package/dist/pi/ai/image-models.js +22 -0
  49. package/dist/pi/ai/images-api-registry.js +21 -0
  50. package/dist/pi/ai/images.js +13 -0
  51. package/dist/pi/ai/index.js +18 -0
  52. package/dist/pi/ai/models.generated.js +16220 -0
  53. package/dist/pi/ai/models.js +70 -0
  54. package/dist/pi/ai/oauth.js +1 -0
  55. package/dist/pi/ai/providers/anthropic.js +945 -0
  56. package/dist/pi/ai/providers/faux.js +367 -0
  57. package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
  58. package/dist/pi/ai/providers/openai-completions.js +945 -0
  59. package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
  60. package/dist/pi/ai/providers/register-builtins.js +97 -0
  61. package/dist/pi/ai/providers/simple-options.js +40 -0
  62. package/dist/pi/ai/providers/transform-messages.js +183 -0
  63. package/dist/pi/ai/session-resources.js +21 -0
  64. package/dist/pi/ai/stream.js +26 -0
  65. package/dist/pi/ai/types.js +1 -0
  66. package/dist/pi/ai/utils/diagnostics.js +24 -0
  67. package/dist/pi/ai/utils/event-stream.js +80 -0
  68. package/dist/pi/ai/utils/hash.js +13 -0
  69. package/dist/pi/ai/utils/headers.js +7 -0
  70. package/dist/pi/ai/utils/json-parse.js +112 -0
  71. package/dist/pi/ai/utils/node-http-proxy.js +96 -0
  72. package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
  73. package/dist/pi/ai/utils/oauth/device-code.js +54 -0
  74. package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
  75. package/dist/pi/ai/utils/oauth/index.js +121 -0
  76. package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
  77. package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
  78. package/dist/pi/ai/utils/oauth/pkce.js +30 -0
  79. package/dist/pi/ai/utils/oauth/types.js +1 -0
  80. package/dist/pi/ai/utils/overflow.js +150 -0
  81. package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
  82. package/dist/pi/ai/utils/typebox-helpers.js +20 -0
  83. package/dist/pi/ai/utils/validation.js +280 -0
  84. package/dist/pi/coding-agent/bun/cli.js +7 -0
  85. package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
  86. package/dist/pi/coding-agent/cli/args.js +340 -0
  87. package/dist/pi/coding-agent/cli/file-processor.js +82 -0
  88. package/dist/pi/coding-agent/cli/initial-message.js +21 -0
  89. package/dist/pi/coding-agent/cli.js +17 -0
  90. package/dist/pi/coding-agent/config.js +414 -0
  91. package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
  92. package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
  93. package/dist/pi/coding-agent/core/agent-session.js +2498 -0
  94. package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
  95. package/dist/pi/coding-agent/core/auth-storage.js +441 -0
  96. package/dist/pi/coding-agent/core/bash-executor.js +110 -0
  97. package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
  98. package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
  99. package/dist/pi/coding-agent/core/compaction/index.js +6 -0
  100. package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
  101. package/dist/pi/coding-agent/core/defaults.js +1 -0
  102. package/dist/pi/coding-agent/core/diagnostics.js +1 -0
  103. package/dist/pi/coding-agent/core/event-bus.js +24 -0
  104. package/dist/pi/coding-agent/core/exec.js +74 -0
  105. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
  106. package/dist/pi/coding-agent/core/export-html/index.js +225 -0
  107. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
  108. package/dist/pi/coding-agent/core/extensions/index.js +8 -0
  109. package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
  110. package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
  111. package/dist/pi/coding-agent/core/extensions/types.js +44 -0
  112. package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
  113. package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
  114. package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
  115. package/dist/pi/coding-agent/core/index.js +11 -0
  116. package/dist/pi/coding-agent/core/keybindings.js +294 -0
  117. package/dist/pi/coding-agent/core/messages.js +122 -0
  118. package/dist/pi/coding-agent/core/model-registry.js +728 -0
  119. package/dist/pi/coding-agent/core/model-resolver.js +494 -0
  120. package/dist/pi/coding-agent/core/output-guard.js +58 -0
  121. package/dist/pi/coding-agent/core/package-manager.js +2020 -0
  122. package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
  123. package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
  124. package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
  125. package/dist/pi/coding-agent/core/resource-loader.js +733 -0
  126. package/dist/pi/coding-agent/core/sdk.js +282 -0
  127. package/dist/pi/coding-agent/core/session-cwd.js +37 -0
  128. package/dist/pi/coding-agent/core/session-manager.js +1146 -0
  129. package/dist/pi/coding-agent/core/settings-manager.js +794 -0
  130. package/dist/pi/coding-agent/core/skills.js +386 -0
  131. package/dist/pi/coding-agent/core/slash-commands.js +24 -0
  132. package/dist/pi/coding-agent/core/source-info.js +18 -0
  133. package/dist/pi/coding-agent/core/system-prompt.js +122 -0
  134. package/dist/pi/coding-agent/core/telemetry.js +8 -0
  135. package/dist/pi/coding-agent/core/timings.js +30 -0
  136. package/dist/pi/coding-agent/core/tools/bash.js +341 -0
  137. package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
  138. package/dist/pi/coding-agent/core/tools/edit.js +324 -0
  139. package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
  140. package/dist/pi/coding-agent/core/tools/find.js +297 -0
  141. package/dist/pi/coding-agent/core/tools/grep.js +303 -0
  142. package/dist/pi/coding-agent/core/tools/index.js +111 -0
  143. package/dist/pi/coding-agent/core/tools/ls.js +168 -0
  144. package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
  145. package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
  146. package/dist/pi/coding-agent/core/tools/read.js +288 -0
  147. package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
  148. package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
  149. package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
  150. package/dist/pi/coding-agent/core/tools/write.js +212 -0
  151. package/dist/pi/coding-agent/index.js +41 -0
  152. package/dist/pi/coding-agent/main.js +5 -0
  153. package/dist/pi/coding-agent/migrations.js +280 -0
  154. package/dist/pi/coding-agent/modes/index.js +7 -0
  155. package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
  156. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
  157. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
  158. package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
  159. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
  160. package/dist/pi/coding-agent/modes/print-mode.js +130 -0
  161. package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
  162. package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
  163. package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
  164. package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
  165. package/dist/pi/coding-agent/utils/ansi.js +51 -0
  166. package/dist/pi/coding-agent/utils/changelog.js +86 -0
  167. package/dist/pi/coding-agent/utils/child-process.js +87 -0
  168. package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
  169. package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
  170. package/dist/pi/coding-agent/utils/clipboard.js +116 -0
  171. package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
  172. package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
  173. package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
  174. package/dist/pi/coding-agent/utils/git.js +162 -0
  175. package/dist/pi/coding-agent/utils/html.js +39 -0
  176. package/dist/pi/coding-agent/utils/image-convert.js +38 -0
  177. package/dist/pi/coding-agent/utils/image-resize.js +136 -0
  178. package/dist/pi/coding-agent/utils/mime.js +68 -0
  179. package/dist/pi/coding-agent/utils/paths.js +91 -0
  180. package/dist/pi/coding-agent/utils/photon.js +120 -0
  181. package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
  182. package/dist/pi/coding-agent/utils/shell.js +194 -0
  183. package/dist/pi/coding-agent/utils/sleep.js +16 -0
  184. package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
  185. package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
  186. package/dist/pi/coding-agent/utils/version-check.js +81 -0
  187. package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
  188. package/dist/pi/tui/autocomplete.js +631 -0
  189. package/dist/pi/tui/components/box.js +103 -0
  190. package/dist/pi/tui/components/cancellable-loader.js +34 -0
  191. package/dist/pi/tui/components/editor.js +1915 -0
  192. package/dist/pi/tui/components/image.js +88 -0
  193. package/dist/pi/tui/components/input.js +425 -0
  194. package/dist/pi/tui/components/loader.js +68 -0
  195. package/dist/pi/tui/components/markdown.js +633 -0
  196. package/dist/pi/tui/components/select-list.js +158 -0
  197. package/dist/pi/tui/components/settings-list.js +184 -0
  198. package/dist/pi/tui/components/spacer.js +22 -0
  199. package/dist/pi/tui/components/text.js +88 -0
  200. package/dist/pi/tui/components/truncated-text.js +50 -0
  201. package/dist/pi/tui/editor-component.js +1 -0
  202. package/dist/pi/tui/fuzzy.js +109 -0
  203. package/dist/pi/tui/index.js +31 -0
  204. package/dist/pi/tui/keybindings.js +173 -0
  205. package/dist/pi/tui/keys.js +1172 -0
  206. package/dist/pi/tui/kill-ring.js +43 -0
  207. package/dist/pi/tui/stdin-buffer.js +360 -0
  208. package/dist/pi/tui/terminal-image.js +335 -0
  209. package/dist/pi/tui/terminal.js +324 -0
  210. package/dist/pi/tui/tui.js +1076 -0
  211. package/dist/pi/tui/undo-stack.js +24 -0
  212. package/dist/pi/tui/utils.js +1016 -0
  213. package/dist/relay/dispatcher.js +30 -0
  214. package/dist/server/handlers/queue.js +52 -0
  215. package/dist/server/pi-bridge.js +9 -1
  216. package/dist/server/session-stream.js +76 -111
  217. package/dist/server/storage.js +154 -2
  218. package/dist/server/title-generator.js +14 -153
  219. package/package.json +24 -6
@@ -0,0 +1,794 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import lockfile from "proper-lockfile";
4
+ import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
5
+ import { normalizePath, resolvePath } from "../utils/paths.js";
6
+ import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.js";
7
+ /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
8
+ function deepMergeSettings(base, overrides) {
9
+ const result = { ...base };
10
+ for (const key of Object.keys(overrides)) {
11
+ const overrideValue = overrides[key];
12
+ const baseValue = base[key];
13
+ if (overrideValue === undefined) {
14
+ continue;
15
+ }
16
+ // For nested objects, merge recursively
17
+ if (typeof overrideValue === "object" &&
18
+ overrideValue !== null &&
19
+ !Array.isArray(overrideValue) &&
20
+ typeof baseValue === "object" &&
21
+ baseValue !== null &&
22
+ !Array.isArray(baseValue)) {
23
+ result[key] = { ...baseValue, ...overrideValue };
24
+ }
25
+ else {
26
+ // For primitives and arrays, override value wins
27
+ result[key] = overrideValue;
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+ export class FileSettingsStorage {
33
+ globalSettingsPath;
34
+ projectSettingsPath;
35
+ constructor(cwd, agentDir) {
36
+ const resolvedCwd = resolvePath(cwd);
37
+ const resolvedAgentDir = resolvePath(agentDir);
38
+ this.globalSettingsPath = join(resolvedAgentDir, "settings.json");
39
+ this.projectSettingsPath = join(resolvedCwd, CONFIG_DIR_NAME, "settings.json");
40
+ }
41
+ acquireLockSyncWithRetry(path) {
42
+ const maxAttempts = 10;
43
+ const delayMs = 20;
44
+ let lastError;
45
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
46
+ try {
47
+ return lockfile.lockSync(path, { realpath: false });
48
+ }
49
+ catch (error) {
50
+ const code = typeof error === "object" && error !== null && "code" in error
51
+ ? String(error.code)
52
+ : undefined;
53
+ if (code !== "ELOCKED" || attempt === maxAttempts) {
54
+ throw error;
55
+ }
56
+ lastError = error;
57
+ const start = Date.now();
58
+ while (Date.now() - start < delayMs) {
59
+ // Sleep synchronously to avoid changing callers to async.
60
+ }
61
+ }
62
+ }
63
+ throw lastError ?? new Error("Failed to acquire settings lock");
64
+ }
65
+ withLock(scope, fn) {
66
+ const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath;
67
+ const dir = dirname(path);
68
+ let release;
69
+ try {
70
+ // Only create directory and lock if file exists or we need to write
71
+ const fileExists = existsSync(path);
72
+ if (fileExists) {
73
+ release = this.acquireLockSyncWithRetry(path);
74
+ }
75
+ const current = fileExists ? readFileSync(path, "utf-8") : undefined;
76
+ const next = fn(current);
77
+ if (next !== undefined) {
78
+ // Only create directory when we actually need to write
79
+ if (!existsSync(dir)) {
80
+ mkdirSync(dir, { recursive: true });
81
+ }
82
+ if (!release) {
83
+ release = this.acquireLockSyncWithRetry(path);
84
+ }
85
+ writeFileSync(path, next, "utf-8");
86
+ }
87
+ }
88
+ finally {
89
+ if (release) {
90
+ release();
91
+ }
92
+ }
93
+ }
94
+ }
95
+ export class InMemorySettingsStorage {
96
+ global;
97
+ project;
98
+ withLock(scope, fn) {
99
+ const current = scope === "global" ? this.global : this.project;
100
+ const next = fn(current);
101
+ if (next !== undefined) {
102
+ if (scope === "global") {
103
+ this.global = next;
104
+ }
105
+ else {
106
+ this.project = next;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ export class SettingsManager {
112
+ storage;
113
+ globalSettings;
114
+ projectSettings;
115
+ settings;
116
+ modifiedFields = new Set(); // Track global fields modified during session
117
+ modifiedNestedFields = new Map(); // Track global nested field modifications
118
+ modifiedProjectFields = new Set(); // Track project fields modified during session
119
+ modifiedProjectNestedFields = new Map(); // Track project nested field modifications
120
+ globalSettingsLoadError = null; // Track if global settings file had parse errors
121
+ projectSettingsLoadError = null; // Track if project settings file had parse errors
122
+ writeQueue = Promise.resolve();
123
+ errors;
124
+ constructor(storage, initialGlobal, initialProject, globalLoadError = null, projectLoadError = null, initialErrors = []) {
125
+ this.storage = storage;
126
+ this.globalSettings = initialGlobal;
127
+ this.projectSettings = initialProject;
128
+ this.globalSettingsLoadError = globalLoadError;
129
+ this.projectSettingsLoadError = projectLoadError;
130
+ this.errors = [...initialErrors];
131
+ this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
132
+ }
133
+ /** Create a SettingsManager that loads from files */
134
+ static create(cwd, agentDir = getAgentDir()) {
135
+ const storage = new FileSettingsStorage(cwd, agentDir);
136
+ return SettingsManager.fromStorage(storage);
137
+ }
138
+ /** Create a SettingsManager from an arbitrary storage backend */
139
+ static fromStorage(storage) {
140
+ const globalLoad = SettingsManager.tryLoadFromStorage(storage, "global");
141
+ const projectLoad = SettingsManager.tryLoadFromStorage(storage, "project");
142
+ const initialErrors = [];
143
+ if (globalLoad.error) {
144
+ initialErrors.push({ scope: "global", error: globalLoad.error });
145
+ }
146
+ if (projectLoad.error) {
147
+ initialErrors.push({ scope: "project", error: projectLoad.error });
148
+ }
149
+ return new SettingsManager(storage, globalLoad.settings, projectLoad.settings, globalLoad.error, projectLoad.error, initialErrors);
150
+ }
151
+ /** Create an in-memory SettingsManager (no file I/O) */
152
+ static inMemory(settings = {}) {
153
+ const storage = new InMemorySettingsStorage();
154
+ const initialSettings = SettingsManager.migrateSettings(structuredClone(settings));
155
+ storage.withLock("global", () => JSON.stringify(initialSettings, null, 2));
156
+ return SettingsManager.fromStorage(storage);
157
+ }
158
+ static loadFromStorage(storage, scope) {
159
+ let content;
160
+ storage.withLock(scope, (current) => {
161
+ content = current;
162
+ return undefined;
163
+ });
164
+ if (!content) {
165
+ return {};
166
+ }
167
+ const settings = JSON.parse(content);
168
+ return SettingsManager.migrateSettings(settings);
169
+ }
170
+ static tryLoadFromStorage(storage, scope) {
171
+ try {
172
+ return { settings: SettingsManager.loadFromStorage(storage, scope), error: null };
173
+ }
174
+ catch (error) {
175
+ return { settings: {}, error: error };
176
+ }
177
+ }
178
+ /** Migrate old settings format to new format */
179
+ static migrateSettings(settings) {
180
+ // Migrate queueMode -> steeringMode
181
+ if ("queueMode" in settings && !("steeringMode" in settings)) {
182
+ settings.steeringMode = settings.queueMode;
183
+ delete settings.queueMode;
184
+ }
185
+ // Migrate legacy websockets boolean -> transport enum
186
+ if (!("transport" in settings) && typeof settings.websockets === "boolean") {
187
+ settings.transport = settings.websockets ? "websocket" : "sse";
188
+ delete settings.websockets;
189
+ }
190
+ // Migrate old skills object format to new array format
191
+ if ("skills" in settings &&
192
+ typeof settings.skills === "object" &&
193
+ settings.skills !== null &&
194
+ !Array.isArray(settings.skills)) {
195
+ const skillsSettings = settings.skills;
196
+ if (skillsSettings.enableSkillCommands !== undefined && settings.enableSkillCommands === undefined) {
197
+ settings.enableSkillCommands = skillsSettings.enableSkillCommands;
198
+ }
199
+ if (Array.isArray(skillsSettings.customDirectories) && skillsSettings.customDirectories.length > 0) {
200
+ settings.skills = skillsSettings.customDirectories;
201
+ }
202
+ else {
203
+ delete settings.skills;
204
+ }
205
+ }
206
+ // Migrate retry.maxDelayMs -> retry.provider.maxRetryDelayMs
207
+ if ("retry" in settings &&
208
+ typeof settings.retry === "object" &&
209
+ settings.retry !== null &&
210
+ !Array.isArray(settings.retry)) {
211
+ const retrySettings = settings.retry;
212
+ const providerSettings = typeof retrySettings.provider === "object" && retrySettings.provider !== null
213
+ ? retrySettings.provider
214
+ : undefined;
215
+ if (typeof retrySettings.maxDelayMs === "number" &&
216
+ (providerSettings?.maxRetryDelayMs === undefined || providerSettings?.maxRetryDelayMs === null)) {
217
+ retrySettings.provider = {
218
+ ...(providerSettings ?? {}),
219
+ maxRetryDelayMs: retrySettings.maxDelayMs,
220
+ };
221
+ }
222
+ delete retrySettings.maxDelayMs;
223
+ }
224
+ return settings;
225
+ }
226
+ getGlobalSettings() {
227
+ return structuredClone(this.globalSettings);
228
+ }
229
+ getProjectSettings() {
230
+ return structuredClone(this.projectSettings);
231
+ }
232
+ async reload() {
233
+ await this.writeQueue;
234
+ const globalLoad = SettingsManager.tryLoadFromStorage(this.storage, "global");
235
+ if (!globalLoad.error) {
236
+ this.globalSettings = globalLoad.settings;
237
+ this.globalSettingsLoadError = null;
238
+ }
239
+ else {
240
+ this.globalSettingsLoadError = globalLoad.error;
241
+ this.recordError("global", globalLoad.error);
242
+ }
243
+ this.modifiedFields.clear();
244
+ this.modifiedNestedFields.clear();
245
+ this.modifiedProjectFields.clear();
246
+ this.modifiedProjectNestedFields.clear();
247
+ const projectLoad = SettingsManager.tryLoadFromStorage(this.storage, "project");
248
+ if (!projectLoad.error) {
249
+ this.projectSettings = projectLoad.settings;
250
+ this.projectSettingsLoadError = null;
251
+ }
252
+ else {
253
+ this.projectSettingsLoadError = projectLoad.error;
254
+ this.recordError("project", projectLoad.error);
255
+ }
256
+ this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
257
+ }
258
+ /** Apply additional overrides on top of current settings */
259
+ applyOverrides(overrides) {
260
+ this.settings = deepMergeSettings(this.settings, overrides);
261
+ }
262
+ /** Mark a global field as modified during this session */
263
+ markModified(field, nestedKey) {
264
+ this.modifiedFields.add(field);
265
+ if (nestedKey) {
266
+ if (!this.modifiedNestedFields.has(field)) {
267
+ this.modifiedNestedFields.set(field, new Set());
268
+ }
269
+ this.modifiedNestedFields.get(field).add(nestedKey);
270
+ }
271
+ }
272
+ /** Mark a project field as modified during this session */
273
+ markProjectModified(field, nestedKey) {
274
+ this.modifiedProjectFields.add(field);
275
+ if (nestedKey) {
276
+ if (!this.modifiedProjectNestedFields.has(field)) {
277
+ this.modifiedProjectNestedFields.set(field, new Set());
278
+ }
279
+ this.modifiedProjectNestedFields.get(field).add(nestedKey);
280
+ }
281
+ }
282
+ recordError(scope, error) {
283
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
284
+ this.errors.push({ scope, error: normalizedError });
285
+ }
286
+ clearModifiedScope(scope) {
287
+ if (scope === "global") {
288
+ this.modifiedFields.clear();
289
+ this.modifiedNestedFields.clear();
290
+ return;
291
+ }
292
+ this.modifiedProjectFields.clear();
293
+ this.modifiedProjectNestedFields.clear();
294
+ }
295
+ enqueueWrite(scope, task) {
296
+ this.writeQueue = this.writeQueue
297
+ .then(() => {
298
+ task();
299
+ this.clearModifiedScope(scope);
300
+ })
301
+ .catch((error) => {
302
+ this.recordError(scope, error);
303
+ });
304
+ }
305
+ cloneModifiedNestedFields(source) {
306
+ const snapshot = new Map();
307
+ for (const [key, value] of source.entries()) {
308
+ snapshot.set(key, new Set(value));
309
+ }
310
+ return snapshot;
311
+ }
312
+ persistScopedSettings(scope, snapshotSettings, modifiedFields, modifiedNestedFields) {
313
+ this.storage.withLock(scope, (current) => {
314
+ const currentFileSettings = current
315
+ ? SettingsManager.migrateSettings(JSON.parse(current))
316
+ : {};
317
+ const mergedSettings = { ...currentFileSettings };
318
+ for (const field of modifiedFields) {
319
+ const value = snapshotSettings[field];
320
+ if (modifiedNestedFields.has(field) && typeof value === "object" && value !== null) {
321
+ const nestedModified = modifiedNestedFields.get(field);
322
+ const baseNested = currentFileSettings[field] ?? {};
323
+ const inMemoryNested = value;
324
+ const mergedNested = { ...baseNested };
325
+ for (const nestedKey of nestedModified) {
326
+ mergedNested[nestedKey] = inMemoryNested[nestedKey];
327
+ }
328
+ mergedSettings[field] = mergedNested;
329
+ }
330
+ else {
331
+ mergedSettings[field] = value;
332
+ }
333
+ }
334
+ return JSON.stringify(mergedSettings, null, 2);
335
+ });
336
+ }
337
+ save() {
338
+ this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
339
+ if (this.globalSettingsLoadError) {
340
+ return;
341
+ }
342
+ const snapshotGlobalSettings = structuredClone(this.globalSettings);
343
+ const modifiedFields = new Set(this.modifiedFields);
344
+ const modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedNestedFields);
345
+ this.enqueueWrite("global", () => {
346
+ this.persistScopedSettings("global", snapshotGlobalSettings, modifiedFields, modifiedNestedFields);
347
+ });
348
+ }
349
+ saveProjectSettings(settings) {
350
+ this.projectSettings = structuredClone(settings);
351
+ this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
352
+ if (this.projectSettingsLoadError) {
353
+ return;
354
+ }
355
+ const snapshotProjectSettings = structuredClone(this.projectSettings);
356
+ const modifiedFields = new Set(this.modifiedProjectFields);
357
+ const modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedProjectNestedFields);
358
+ this.enqueueWrite("project", () => {
359
+ this.persistScopedSettings("project", snapshotProjectSettings, modifiedFields, modifiedNestedFields);
360
+ });
361
+ }
362
+ async flush() {
363
+ await this.writeQueue;
364
+ }
365
+ drainErrors() {
366
+ const drained = [...this.errors];
367
+ this.errors = [];
368
+ return drained;
369
+ }
370
+ getLastChangelogVersion() {
371
+ return this.settings.lastChangelogVersion;
372
+ }
373
+ setLastChangelogVersion(version) {
374
+ this.globalSettings.lastChangelogVersion = version;
375
+ this.markModified("lastChangelogVersion");
376
+ this.save();
377
+ }
378
+ getSessionDir() {
379
+ const sessionDir = this.settings.sessionDir;
380
+ return sessionDir ? normalizePath(sessionDir) : sessionDir;
381
+ }
382
+ getDefaultProvider() {
383
+ return this.settings.defaultProvider;
384
+ }
385
+ getDefaultModel() {
386
+ return this.settings.defaultModel;
387
+ }
388
+ setDefaultProvider(provider) {
389
+ this.globalSettings.defaultProvider = provider;
390
+ this.markModified("defaultProvider");
391
+ this.save();
392
+ }
393
+ setDefaultModel(modelId) {
394
+ this.globalSettings.defaultModel = modelId;
395
+ this.markModified("defaultModel");
396
+ this.save();
397
+ }
398
+ setDefaultModelAndProvider(provider, modelId) {
399
+ this.globalSettings.defaultProvider = provider;
400
+ this.globalSettings.defaultModel = modelId;
401
+ this.markModified("defaultProvider");
402
+ this.markModified("defaultModel");
403
+ this.save();
404
+ }
405
+ getSteeringMode() {
406
+ return this.settings.steeringMode || "one-at-a-time";
407
+ }
408
+ setSteeringMode(mode) {
409
+ this.globalSettings.steeringMode = mode;
410
+ this.markModified("steeringMode");
411
+ this.save();
412
+ }
413
+ getFollowUpMode() {
414
+ return this.settings.followUpMode || "one-at-a-time";
415
+ }
416
+ setFollowUpMode(mode) {
417
+ this.globalSettings.followUpMode = mode;
418
+ this.markModified("followUpMode");
419
+ this.save();
420
+ }
421
+ getTheme() {
422
+ return this.settings.theme;
423
+ }
424
+ setTheme(theme) {
425
+ this.globalSettings.theme = theme;
426
+ this.markModified("theme");
427
+ this.save();
428
+ }
429
+ getDefaultThinkingLevel() {
430
+ return this.settings.defaultThinkingLevel;
431
+ }
432
+ setDefaultThinkingLevel(level) {
433
+ this.globalSettings.defaultThinkingLevel = level;
434
+ this.markModified("defaultThinkingLevel");
435
+ this.save();
436
+ }
437
+ getTransport() {
438
+ return this.settings.transport ?? "auto";
439
+ }
440
+ setTransport(transport) {
441
+ this.globalSettings.transport = transport;
442
+ this.markModified("transport");
443
+ this.save();
444
+ }
445
+ getCompactionEnabled() {
446
+ return this.settings.compaction?.enabled ?? true;
447
+ }
448
+ setCompactionEnabled(enabled) {
449
+ if (!this.globalSettings.compaction) {
450
+ this.globalSettings.compaction = {};
451
+ }
452
+ this.globalSettings.compaction.enabled = enabled;
453
+ this.markModified("compaction", "enabled");
454
+ this.save();
455
+ }
456
+ getCompactionReserveTokens() {
457
+ return this.settings.compaction?.reserveTokens ?? 16384;
458
+ }
459
+ getCompactionKeepRecentTokens() {
460
+ return this.settings.compaction?.keepRecentTokens ?? 20000;
461
+ }
462
+ getCompactionSettings() {
463
+ return {
464
+ enabled: this.getCompactionEnabled(),
465
+ reserveTokens: this.getCompactionReserveTokens(),
466
+ keepRecentTokens: this.getCompactionKeepRecentTokens(),
467
+ };
468
+ }
469
+ getBranchSummarySettings() {
470
+ return {
471
+ reserveTokens: this.settings.branchSummary?.reserveTokens ?? 16384,
472
+ skipPrompt: this.settings.branchSummary?.skipPrompt ?? false,
473
+ };
474
+ }
475
+ getBranchSummarySkipPrompt() {
476
+ return this.settings.branchSummary?.skipPrompt ?? false;
477
+ }
478
+ getRetryEnabled() {
479
+ return this.settings.retry?.enabled ?? true;
480
+ }
481
+ setRetryEnabled(enabled) {
482
+ if (!this.globalSettings.retry) {
483
+ this.globalSettings.retry = {};
484
+ }
485
+ this.globalSettings.retry.enabled = enabled;
486
+ this.markModified("retry", "enabled");
487
+ this.save();
488
+ }
489
+ getRetrySettings() {
490
+ return {
491
+ enabled: this.getRetryEnabled(),
492
+ maxRetries: this.settings.retry?.maxRetries ?? 3,
493
+ baseDelayMs: this.settings.retry?.baseDelayMs ?? 2000,
494
+ };
495
+ }
496
+ getHttpIdleTimeoutMs() {
497
+ const value = this.settings.httpIdleTimeoutMs;
498
+ const timeoutMs = parseHttpIdleTimeoutMs(value);
499
+ if (timeoutMs !== undefined) {
500
+ return timeoutMs;
501
+ }
502
+ if (value !== undefined) {
503
+ throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(value)}`);
504
+ }
505
+ return DEFAULT_HTTP_IDLE_TIMEOUT_MS;
506
+ }
507
+ setHttpIdleTimeoutMs(timeoutMs) {
508
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
509
+ throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(timeoutMs)}`);
510
+ }
511
+ this.globalSettings.httpIdleTimeoutMs = Math.floor(timeoutMs);
512
+ this.markModified("httpIdleTimeoutMs");
513
+ this.save();
514
+ }
515
+ getProviderRetrySettings() {
516
+ return {
517
+ timeoutMs: this.settings.retry?.provider?.timeoutMs,
518
+ maxRetries: this.settings.retry?.provider?.maxRetries,
519
+ maxRetryDelayMs: this.settings.retry?.provider?.maxRetryDelayMs ?? 60000,
520
+ };
521
+ }
522
+ getHideThinkingBlock() {
523
+ return this.settings.hideThinkingBlock ?? false;
524
+ }
525
+ setHideThinkingBlock(hide) {
526
+ this.globalSettings.hideThinkingBlock = hide;
527
+ this.markModified("hideThinkingBlock");
528
+ this.save();
529
+ }
530
+ getShellPath() {
531
+ return this.settings.shellPath;
532
+ }
533
+ setShellPath(path) {
534
+ this.globalSettings.shellPath = path;
535
+ this.markModified("shellPath");
536
+ this.save();
537
+ }
538
+ getQuietStartup() {
539
+ return this.settings.quietStartup ?? false;
540
+ }
541
+ setQuietStartup(quiet) {
542
+ this.globalSettings.quietStartup = quiet;
543
+ this.markModified("quietStartup");
544
+ this.save();
545
+ }
546
+ getShellCommandPrefix() {
547
+ return this.settings.shellCommandPrefix;
548
+ }
549
+ setShellCommandPrefix(prefix) {
550
+ this.globalSettings.shellCommandPrefix = prefix;
551
+ this.markModified("shellCommandPrefix");
552
+ this.save();
553
+ }
554
+ getNpmCommand() {
555
+ return this.settings.npmCommand ? [...this.settings.npmCommand] : undefined;
556
+ }
557
+ setNpmCommand(command) {
558
+ this.globalSettings.npmCommand = command ? [...command] : undefined;
559
+ this.markModified("npmCommand");
560
+ this.save();
561
+ }
562
+ getCollapseChangelog() {
563
+ return this.settings.collapseChangelog ?? false;
564
+ }
565
+ setCollapseChangelog(collapse) {
566
+ this.globalSettings.collapseChangelog = collapse;
567
+ this.markModified("collapseChangelog");
568
+ this.save();
569
+ }
570
+ getEnableInstallTelemetry() {
571
+ return this.settings.enableInstallTelemetry ?? true;
572
+ }
573
+ setEnableInstallTelemetry(enabled) {
574
+ this.globalSettings.enableInstallTelemetry = enabled;
575
+ this.markModified("enableInstallTelemetry");
576
+ this.save();
577
+ }
578
+ getPackages() {
579
+ return [...(this.settings.packages ?? [])];
580
+ }
581
+ setPackages(packages) {
582
+ this.globalSettings.packages = packages;
583
+ this.markModified("packages");
584
+ this.save();
585
+ }
586
+ setProjectPackages(packages) {
587
+ const projectSettings = structuredClone(this.projectSettings);
588
+ projectSettings.packages = packages;
589
+ this.markProjectModified("packages");
590
+ this.saveProjectSettings(projectSettings);
591
+ }
592
+ getExtensionPaths() {
593
+ return [...(this.settings.extensions ?? [])];
594
+ }
595
+ setExtensionPaths(paths) {
596
+ this.globalSettings.extensions = paths;
597
+ this.markModified("extensions");
598
+ this.save();
599
+ }
600
+ setProjectExtensionPaths(paths) {
601
+ const projectSettings = structuredClone(this.projectSettings);
602
+ projectSettings.extensions = paths;
603
+ this.markProjectModified("extensions");
604
+ this.saveProjectSettings(projectSettings);
605
+ }
606
+ getSkillPaths() {
607
+ return [...(this.settings.skills ?? [])];
608
+ }
609
+ setSkillPaths(paths) {
610
+ this.globalSettings.skills = paths;
611
+ this.markModified("skills");
612
+ this.save();
613
+ }
614
+ setProjectSkillPaths(paths) {
615
+ const projectSettings = structuredClone(this.projectSettings);
616
+ projectSettings.skills = paths;
617
+ this.markProjectModified("skills");
618
+ this.saveProjectSettings(projectSettings);
619
+ }
620
+ getPromptTemplatePaths() {
621
+ return [...(this.settings.prompts ?? [])];
622
+ }
623
+ setPromptTemplatePaths(paths) {
624
+ this.globalSettings.prompts = paths;
625
+ this.markModified("prompts");
626
+ this.save();
627
+ }
628
+ setProjectPromptTemplatePaths(paths) {
629
+ const projectSettings = structuredClone(this.projectSettings);
630
+ projectSettings.prompts = paths;
631
+ this.markProjectModified("prompts");
632
+ this.saveProjectSettings(projectSettings);
633
+ }
634
+ getThemePaths() {
635
+ return [...(this.settings.themes ?? [])];
636
+ }
637
+ setThemePaths(paths) {
638
+ this.globalSettings.themes = paths;
639
+ this.markModified("themes");
640
+ this.save();
641
+ }
642
+ setProjectThemePaths(paths) {
643
+ const projectSettings = structuredClone(this.projectSettings);
644
+ projectSettings.themes = paths;
645
+ this.markProjectModified("themes");
646
+ this.saveProjectSettings(projectSettings);
647
+ }
648
+ getEnableSkillCommands() {
649
+ return this.settings.enableSkillCommands ?? true;
650
+ }
651
+ setEnableSkillCommands(enabled) {
652
+ this.globalSettings.enableSkillCommands = enabled;
653
+ this.markModified("enableSkillCommands");
654
+ this.save();
655
+ }
656
+ getThinkingBudgets() {
657
+ return this.settings.thinkingBudgets;
658
+ }
659
+ getShowImages() {
660
+ return this.settings.terminal?.showImages ?? true;
661
+ }
662
+ setShowImages(show) {
663
+ if (!this.globalSettings.terminal) {
664
+ this.globalSettings.terminal = {};
665
+ }
666
+ this.globalSettings.terminal.showImages = show;
667
+ this.markModified("terminal", "showImages");
668
+ this.save();
669
+ }
670
+ getImageWidthCells() {
671
+ const width = this.settings.terminal?.imageWidthCells;
672
+ if (typeof width !== "number" || !Number.isFinite(width)) {
673
+ return 60;
674
+ }
675
+ return Math.max(1, Math.floor(width));
676
+ }
677
+ setImageWidthCells(width) {
678
+ if (!this.globalSettings.terminal) {
679
+ this.globalSettings.terminal = {};
680
+ }
681
+ this.globalSettings.terminal.imageWidthCells = Math.max(1, Math.floor(width));
682
+ this.markModified("terminal", "imageWidthCells");
683
+ this.save();
684
+ }
685
+ getClearOnShrink() {
686
+ // Settings takes precedence, then env var, then default false
687
+ if (this.settings.terminal?.clearOnShrink !== undefined) {
688
+ return this.settings.terminal.clearOnShrink;
689
+ }
690
+ return process.env.PI_CLEAR_ON_SHRINK === "1";
691
+ }
692
+ setClearOnShrink(enabled) {
693
+ if (!this.globalSettings.terminal) {
694
+ this.globalSettings.terminal = {};
695
+ }
696
+ this.globalSettings.terminal.clearOnShrink = enabled;
697
+ this.markModified("terminal", "clearOnShrink");
698
+ this.save();
699
+ }
700
+ getShowTerminalProgress() {
701
+ return this.settings.terminal?.showTerminalProgress ?? false;
702
+ }
703
+ setShowTerminalProgress(enabled) {
704
+ if (!this.globalSettings.terminal) {
705
+ this.globalSettings.terminal = {};
706
+ }
707
+ this.globalSettings.terminal.showTerminalProgress = enabled;
708
+ this.markModified("terminal", "showTerminalProgress");
709
+ this.save();
710
+ }
711
+ getImageAutoResize() {
712
+ return this.settings.images?.autoResize ?? true;
713
+ }
714
+ setImageAutoResize(enabled) {
715
+ if (!this.globalSettings.images) {
716
+ this.globalSettings.images = {};
717
+ }
718
+ this.globalSettings.images.autoResize = enabled;
719
+ this.markModified("images", "autoResize");
720
+ this.save();
721
+ }
722
+ getBlockImages() {
723
+ return this.settings.images?.blockImages ?? false;
724
+ }
725
+ setBlockImages(blocked) {
726
+ if (!this.globalSettings.images) {
727
+ this.globalSettings.images = {};
728
+ }
729
+ this.globalSettings.images.blockImages = blocked;
730
+ this.markModified("images", "blockImages");
731
+ this.save();
732
+ }
733
+ getEnabledModels() {
734
+ return this.settings.enabledModels;
735
+ }
736
+ setEnabledModels(patterns) {
737
+ this.globalSettings.enabledModels = patterns;
738
+ this.markModified("enabledModels");
739
+ this.save();
740
+ }
741
+ getDoubleEscapeAction() {
742
+ return this.settings.doubleEscapeAction ?? "tree";
743
+ }
744
+ setDoubleEscapeAction(action) {
745
+ this.globalSettings.doubleEscapeAction = action;
746
+ this.markModified("doubleEscapeAction");
747
+ this.save();
748
+ }
749
+ getTreeFilterMode() {
750
+ const mode = this.settings.treeFilterMode;
751
+ const valid = ["default", "no-tools", "user-only", "labeled-only", "all"];
752
+ return mode && valid.includes(mode) ? mode : "default";
753
+ }
754
+ setTreeFilterMode(mode) {
755
+ this.globalSettings.treeFilterMode = mode;
756
+ this.markModified("treeFilterMode");
757
+ this.save();
758
+ }
759
+ getShowHardwareCursor() {
760
+ return this.settings.showHardwareCursor ?? process.env.PI_HARDWARE_CURSOR === "1";
761
+ }
762
+ setShowHardwareCursor(enabled) {
763
+ this.globalSettings.showHardwareCursor = enabled;
764
+ this.markModified("showHardwareCursor");
765
+ this.save();
766
+ }
767
+ getEditorPaddingX() {
768
+ return this.settings.editorPaddingX ?? 0;
769
+ }
770
+ setEditorPaddingX(padding) {
771
+ this.globalSettings.editorPaddingX = Math.max(0, Math.min(3, Math.floor(padding)));
772
+ this.markModified("editorPaddingX");
773
+ this.save();
774
+ }
775
+ getAutocompleteMaxVisible() {
776
+ return this.settings.autocompleteMaxVisible ?? 5;
777
+ }
778
+ setAutocompleteMaxVisible(maxVisible) {
779
+ this.globalSettings.autocompleteMaxVisible = Math.max(3, Math.min(20, Math.floor(maxVisible)));
780
+ this.markModified("autocompleteMaxVisible");
781
+ this.save();
782
+ }
783
+ getCodeBlockIndent() {
784
+ return this.settings.markdown?.codeBlockIndent ?? " ";
785
+ }
786
+ getWarnings() {
787
+ return { ...(this.settings.warnings ?? {}) };
788
+ }
789
+ setWarnings(warnings) {
790
+ this.globalSettings.warnings = { ...warnings };
791
+ this.markModified("warnings");
792
+ this.save();
793
+ }
794
+ }