@oh-my-pi/pi-coding-agent 9.4.0 → 9.6.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/package.json +9 -8
  3. package/src/capability/index.ts +7 -9
  4. package/src/cli/config-cli.ts +86 -73
  5. package/src/cli/update-cli.ts +45 -3
  6. package/src/commit/agentic/agent.ts +4 -4
  7. package/src/commit/agentic/index.ts +6 -5
  8. package/src/commit/agentic/tools/analyze-file.ts +5 -7
  9. package/src/commit/agentic/tools/index.ts +3 -3
  10. package/src/commit/model-selection.ts +13 -17
  11. package/src/commit/pipeline.ts +5 -5
  12. package/src/config/model-registry.ts +7 -0
  13. package/src/config/settings-schema.ts +836 -0
  14. package/src/config/settings.ts +702 -0
  15. package/src/discovery/helpers.ts +55 -11
  16. package/src/exa/index.ts +1 -1
  17. package/src/exec/bash-executor.ts +13 -13
  18. package/src/exec/shell-session.ts +15 -3
  19. package/src/export/ttsr.ts +1 -1
  20. package/src/extensibility/skills.ts +40 -9
  21. package/src/index.ts +2 -10
  22. package/src/ipy/gateway-coordinator.ts +5 -143
  23. package/src/ipy/kernel.ts +6 -171
  24. package/src/ipy/runtime.ts +198 -0
  25. package/src/lsp/client.ts +14 -1
  26. package/src/lsp/defaults.json +0 -6
  27. package/src/lsp/index.ts +1 -1
  28. package/src/lsp/types.ts +2 -0
  29. package/src/main.ts +26 -48
  30. package/src/modes/components/extensions/extension-dashboard.ts +22 -11
  31. package/src/modes/components/index.ts +1 -1
  32. package/src/modes/components/model-selector.ts +7 -7
  33. package/src/modes/components/settings-defs.ts +210 -915
  34. package/src/modes/components/settings-selector.ts +80 -106
  35. package/src/modes/components/status-line/types.ts +2 -8
  36. package/src/modes/components/status-line-segment-editor.ts +1 -1
  37. package/src/modes/components/status-line.ts +26 -3
  38. package/src/modes/controllers/event-controller.ts +9 -8
  39. package/src/modes/controllers/input-controller.ts +19 -15
  40. package/src/modes/controllers/selector-controller.ts +30 -14
  41. package/src/modes/interactive-mode.ts +10 -10
  42. package/src/modes/rpc/rpc-mode.ts +10 -0
  43. package/src/modes/rpc/rpc-types.ts +3 -0
  44. package/src/modes/types.ts +2 -2
  45. package/src/modes/utils/ui-helpers.ts +4 -3
  46. package/src/patch/index.ts +7 -7
  47. package/src/prompts/system/system-prompt.md +0 -1
  48. package/src/prompts/tools/bash.md +12 -2
  49. package/src/prompts/tools/task.md +180 -73
  50. package/src/sdk.ts +38 -61
  51. package/src/session/agent-session.ts +66 -55
  52. package/src/session/agent-storage.ts +1 -1
  53. package/src/session/session-manager.ts +10 -10
  54. package/src/system-prompt.ts +2 -2
  55. package/src/task/executor.ts +9 -9
  56. package/src/task/index.ts +2 -2
  57. package/src/tools/ask.ts +5 -6
  58. package/src/tools/bash-interceptor.ts +39 -1
  59. package/src/tools/bash-normalize.ts +126 -0
  60. package/src/tools/bash.ts +31 -5
  61. package/src/tools/find.ts +51 -33
  62. package/src/tools/index.ts +5 -23
  63. package/src/tools/plan-mode-guard.ts +1 -6
  64. package/src/tools/python.ts +2 -2
  65. package/src/tools/read.ts +2 -2
  66. package/src/tools/write.ts +2 -2
  67. package/src/utils/ignore-files.ts +119 -0
  68. package/src/web/search/providers/perplexity.ts +1 -1
  69. package/examples/sdk/10-settings.ts +0 -37
  70. package/src/config/settings-manager.ts +0 -2015
package/CHANGELOG.md CHANGED
@@ -1,6 +1,90 @@
1
1
  # Changelog
2
2
 
3
3
  ## [Unreleased]
4
+ ### Fixed
5
+
6
+ - Fixed bash command normalization to preserve newlines in heredocs and multiline commands
7
+
8
+ ## [9.6.0] - 2026-02-01
9
+ ### Breaking Changes
10
+
11
+ - Replaced `SettingsManager` class with new `Settings` singleton providing sync get/set API with background persistence
12
+ - Changed settings access from method calls (e.g., `getTheme()`) to path-based access (e.g., `settings.get("theme")`)
13
+ - Removed `settingsManager` parameter from `CreateAgentSessionOptions` in favor of `settingsInstance`
14
+ - Removed `loadSettings()` export from public API
15
+ - Removed example file `examples/sdk/10-settings.ts` demonstrating old SettingsManager API
16
+
17
+ ### Added
18
+
19
+ - New `Settings` singleton class with sync get/set operations and background persistence
20
+ - Added `Settings.isolated()` factory for creating isolated settings instances in tests
21
+ - Added `Settings.init()` for initializing global settings instance
22
+ - Added `settings` global export for convenient access to settings singleton
23
+ - New `settings-schema.ts` providing unified, type-safe settings definitions with UI metadata
24
+ - Added "none" option to `doubleEscapeAction` setting to disable double-escape behavior entirely ([#973](https://github.com/badlogic/pi-mono/issues/973) by [@juanibiapina](https://github.com/juanibiapina))
25
+
26
+ ### Changed
27
+
28
+ - Unified settings schema into single source of truth with `settings-schema.ts` replacing scattered definitions
29
+ - Refactored settings CLI to use new schema-based path resolution instead of SETTINGS_DEFS
30
+ - Updated config command examples to use new nested path syntax (e.g., `compaction.enabled` instead of `autoCompact`)
31
+ - Changed `InteractiveModeContext.settingsManager` to `InteractiveModeContext.settings`
32
+ - Updated all internal settings access throughout codebase to use new `settings.get()` and `settings.set()` API
33
+ - Moved `DEFAULT_BASH_INTERCEPTOR_RULES` from settings-manager to bash-interceptor module
34
+
35
+ ### Removed
36
+
37
+ - Deleted `settings-manager.ts` (2035 lines) - functionality replaced by new Settings singleton
38
+ - Removed `SettingsManager.create()`, `SettingsManager.acquire()`, and `SettingsManager.inMemory()` factory methods
39
+ - Removed individual getter/setter methods from settings API (e.g., `getTheme()`, `setTheme()`, `getCompactionSettings()`)
40
+
41
+ ### Fixed
42
+
43
+ - Respect .gitignore, .ignore, and .fdignore files when scanning package resources for skills, prompts, themes, and extensions
44
+
45
+ ## [9.5.1] - 2026-02-01
46
+
47
+ ### Changed
48
+
49
+ - Changed persistent shell from opt-out to opt-in (default: off) for improved reliability; enable via Settings > Bash > Persistent shell or `OMP_SHELL_PERSIST=1`
50
+ - Added new "Bash" settings tab grouping shell-related settings (force basic shell, persistent shell, interceptor, intercept ls)
51
+
52
+ ## [9.5.0] - 2026-02-01
53
+ ### Added
54
+
55
+ - Added `head` and `tail` parameters to bash tool to limit output lines without breaking streaming
56
+ - Added automatic normalization of bash commands to extract `| head -n N` and `| tail -n N` patterns into native parameters
57
+ - Added `maxResults` parameter to find tool to limit result set at the native layer
58
+ - Added context-structure template showing required sections (Goal, Constraints, Existing Code, API Contract) with examples of good vs bad context
59
+ - Added explicit dependency test: 'Can agent B write correct code without seeing agent A's output?' to determine sequencing
60
+ - Added detailed phased execution pattern with four phases (Foundation, Parallel Implementation, Integration, Dependent Layer) and WASM-to-N-API migration example
61
+ - Added table of dependency patterns that must be sequential (API creation before bindings, interface definition before implementation, etc.)
62
+ - Added phased execution guidance for migrations and refactors to prevent parallel work on dependent layers
63
+ - Added example demonstrating phased execution pattern for porting WASM to N-API with sequential foundation, parallel implementation, integration, and dependent layer phases
64
+
65
+ ### Changed
66
+
67
+ - Improved find tool performance by delegating mtime-based sorting to native layer instead of post-processing results in JavaScript
68
+ - Simplified find tool result processing by removing redundant filesystem stat calls when native metadata is available
69
+ - Updated bash tool documentation to recommend using `head` and `tail` parameters instead of piping through head/tail commands
70
+ - Updated binary build process to exclude worker files from compilation, reducing binary size
71
+ - Modified update mechanism to download and install native addon alongside CLI binary for platform-specific functionality
72
+ - Updated find tool to emit streaming match updates via callback, allowing real-time progress feedback during file searches
73
+ - Modified find tool to use native match metadata (mtime, fileType) from WASM layer instead of redundant filesystem stats, improving performance
74
+ - Restructured Task tool documentation to emphasize context quality and explicit API contracts for subagent success
75
+ - Updated task execution guidance to require structured context with Goal, Constraints, Existing Code, and API Contract sections
76
+ - Reorganized parallelization rules with explicit dependency patterns and phased execution guidance for migrations
77
+ - Clarified that response format requirements must go in schema parameter, never in context descriptions
78
+ - Centralized Python runtime resolution into shared `ipy/runtime.ts` module, removing duplicate code from kernel and gateway coordinator
79
+
80
+ ### Removed
81
+
82
+ - Removed Nushell language server configuration from LSP defaults
83
+
84
+ ### Fixed
85
+
86
+ - Fixed race condition in shell session where command completion could occur before stream data was fully processed
87
+ - Fixed Python gateway spawning console window on Windows by using windowless Python interpreter (pythonw.exe)
4
88
 
5
89
  ## [9.4.0] - 2026-01-31
6
90
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "9.4.0",
3
+ "version": "9.6.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -74,17 +74,17 @@
74
74
  "scripts": {
75
75
  "check": "tsgo -p tsconfig.json",
76
76
  "format-prompts": "bun scripts/format-prompts.ts",
77
- "build:binary": "cd ../.. && bun build --compile --define OMP_COMPILED=true --root . ./packages/coding-agent/src/cli.ts ./packages/natives/src/grep/worker.ts ./packages/natives/src/html/worker.ts ./packages/natives/src/image/worker.ts --outfile packages/coding-agent/dist/omp",
77
+ "build:binary": "cd ../.. && bun build --compile --define OMP_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp",
78
78
  "generate-template": "bun scripts/generate-template.ts",
79
79
  "test": "bun test"
80
80
  },
81
81
  "dependencies": {
82
- "@oh-my-pi/omp-stats": "9.4.0",
83
- "@oh-my-pi/pi-agent-core": "9.4.0",
84
- "@oh-my-pi/pi-ai": "9.4.0",
85
- "@oh-my-pi/pi-natives": "9.4.0",
86
- "@oh-my-pi/pi-tui": "9.4.0",
87
- "@oh-my-pi/pi-utils": "9.4.0",
82
+ "@oh-my-pi/omp-stats": "9.6.0",
83
+ "@oh-my-pi/pi-agent-core": "9.6.0",
84
+ "@oh-my-pi/pi-ai": "9.6.0",
85
+ "@oh-my-pi/pi-natives": "9.6.0",
86
+ "@oh-my-pi/pi-tui": "9.6.0",
87
+ "@oh-my-pi/pi-utils": "9.6.0",
88
88
  "@openai/agents": "^0.4.4",
89
89
  "@sinclair/typebox": "^0.34.48",
90
90
  "ajv": "^8.17.1",
@@ -93,6 +93,7 @@
93
93
  "file-type": "^21.3.0",
94
94
  "glob": "^13.0.0",
95
95
  "handlebars": "^4.7.8",
96
+ "ignore": "^7.0.5",
96
97
  "marked": "^17.0.1",
97
98
  "nanoid": "^5.1.6",
98
99
  "node-html-parser": "^7.0.2",
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
+ import type { Settings } from "../config/settings";
11
12
  import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
12
13
  import type {
13
14
  Capability,
@@ -37,7 +38,7 @@ const providerMeta = new Map<string, { displayName: string; description: string
37
38
  const disabledProviders = new Set<string>();
38
39
 
39
40
  /** Settings manager for persistence (if set) */
40
- let settingsManager: { getDisabledProviders(): string[]; setDisabledProviders(ids: string[]): void } | null = null;
41
+ let settings: Settings | null = null;
41
42
 
42
43
  // =============================================================================
43
44
  // Registration API
@@ -228,13 +229,10 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
228
229
  * Initialize capability system with settings manager for persistence.
229
230
  * Call this once on startup to enable persistent provider state.
230
231
  */
231
- export function initializeWithSettings(manager: {
232
- getDisabledProviders(): string[];
233
- setDisabledProviders(ids: string[]): void;
234
- }): void {
235
- settingsManager = manager;
232
+ export function initializeWithSettings(settingsInstance: Settings): void {
233
+ settings = settingsInstance;
236
234
  // Load disabled providers from settings
237
- const disabled = manager.getDisabledProviders();
235
+ const disabled = settings.get("disabledProviders");
238
236
  disabledProviders.clear();
239
237
  for (const id of disabled) {
240
238
  disabledProviders.add(id);
@@ -245,8 +243,8 @@ export function initializeWithSettings(manager: {
245
243
  * Persist current disabled providers to settings.
246
244
  */
247
245
  function persistDisabledProviders(): void {
248
- if (settingsManager) {
249
- settingsManager.setDisabledProviders(Array.from(disabledProviders));
246
+ if (settings) {
247
+ settings.set("disabledProviders", Array.from(disabledProviders));
250
248
  }
251
249
  }
252
250
 
@@ -2,12 +2,20 @@
2
2
  * Config CLI command handlers.
3
3
  *
4
4
  * Handles `omp config <command>` subcommands for managing settings.
5
- * Uses SETTINGS_DEFS as the source of truth for available settings.
5
+ * Uses settings-defs as the source of truth for available settings.
6
6
  */
7
7
  import chalk from "chalk";
8
8
  import { APP_NAME, getAgentDir } from "../config";
9
- import { SettingsManager } from "../config/settings-manager";
10
- import { SETTINGS_DEFS, type SettingDef } from "../modes/components/settings-defs";
9
+ import {
10
+ getDefault,
11
+ getEnumValues,
12
+ getType,
13
+ type SettingPath,
14
+ Settings,
15
+ type SettingValue,
16
+ settings,
17
+ } from "../config/settings";
18
+ import { getAllSettingDefs, type SettingDef } from "../modes/components/settings-defs";
11
19
  import { theme } from "../modes/theme/theme";
12
20
 
13
21
  // =============================================================================
@@ -29,18 +37,18 @@ export interface ConfigCommandArgs {
29
37
  // Setting Filtering
30
38
  // =============================================================================
31
39
 
32
- /** Find setting definition by ID */
33
- function findSettingDef(id: string): SettingDef | undefined {
34
- return SETTINGS_DEFS.find(def => def.id === id);
40
+ /** Find setting definition by path */
41
+ function findSettingDef(path: string): SettingDef | undefined {
42
+ return getAllSettingDefs().find(def => def.path === path);
35
43
  }
36
44
 
37
45
  /** Get available values for a setting */
38
- function getSettingValues(def: SettingDef, sm: SettingsManager): readonly string[] | undefined {
46
+ function getSettingValues(def: SettingDef): readonly string[] | undefined {
39
47
  if (def.type === "enum") {
40
48
  return def.values;
41
49
  }
42
50
  if (def.type === "submenu") {
43
- const options = def.getOptions(sm);
51
+ const options = def.getOptions();
44
52
  if (options.length > 0) {
45
53
  return options.map(o => o.value);
46
54
  }
@@ -100,29 +108,9 @@ export function parseConfigArgs(args: string[]): ConfigCommandArgs | undefined {
100
108
  }
101
109
 
102
110
  // =============================================================================
103
- // Value Parsing
111
+ // Value Formatting
104
112
  // =============================================================================
105
113
 
106
- function parseValue(value: string, def: SettingDef, sm: SettingsManager): unknown {
107
- if (def.type === "boolean") {
108
- const lower = value.toLowerCase();
109
- if (lower === "true" || lower === "1" || lower === "yes" || lower === "on") {
110
- return true;
111
- }
112
- if (lower === "false" || lower === "0" || lower === "no" || lower === "off") {
113
- return false;
114
- }
115
- throw new Error(`Invalid boolean value: ${value}. Use true/false, yes/no, on/off, or 1/0`);
116
- }
117
-
118
- const validValues = getSettingValues(def, sm);
119
- if (validValues && validValues.length > 0 && !validValues.includes(value)) {
120
- throw new Error(`Invalid value: ${value}. Valid values: ${validValues.join(", ")}`);
121
- }
122
-
123
- return value;
124
- }
125
-
126
114
  function formatValue(value: unknown): string {
127
115
  if (value === undefined || value === null) {
128
116
  return chalk.dim("(not set)");
@@ -136,36 +124,72 @@ function formatValue(value: unknown): string {
136
124
  return chalk.yellow(String(value));
137
125
  }
138
126
 
139
- function getTypeDisplay(def: SettingDef, sm: SettingsManager): string {
127
+ function getTypeDisplay(def: SettingDef): string {
140
128
  if (def.type === "boolean") {
141
129
  return "(boolean)";
142
130
  }
143
- const values = getSettingValues(def, sm);
131
+ const values = getSettingValues(def);
144
132
  if (values && values.length > 0) {
145
133
  return `(${values.join("|")})`;
146
134
  }
147
135
  return "(string)";
148
136
  }
149
137
 
138
+ // =============================================================================
139
+ // Schema-Driven Value Parsing
140
+ // =============================================================================
141
+
142
+ function parseAndSetValue(path: SettingPath, rawValue: string): void {
143
+ const schemaType = getType(path);
144
+ let parsedValue: unknown;
145
+
146
+ const trimmed = rawValue.trim();
147
+ switch (schemaType) {
148
+ case "boolean": {
149
+ const lower = trimmed.toLowerCase();
150
+ if (["true", "1", "yes", "on"].includes(lower)) parsedValue = true;
151
+ else if (["false", "0", "no", "off"].includes(lower)) parsedValue = false;
152
+ else throw new Error(`Invalid boolean value: ${rawValue}. Use true/false, yes/no, on/off, or 1/0`);
153
+ break;
154
+ }
155
+ case "number":
156
+ parsedValue = Number(trimmed);
157
+ if (!Number.isFinite(parsedValue)) throw new Error(`Invalid number: ${rawValue}`);
158
+ break;
159
+ case "enum": {
160
+ const valid = getEnumValues(path);
161
+ if (valid && !valid.includes(trimmed)) {
162
+ throw new Error(`Invalid value: ${rawValue}. Valid values: ${valid.join(", ")}`);
163
+ }
164
+ parsedValue = trimmed;
165
+ break;
166
+ }
167
+ default:
168
+ parsedValue = trimmed;
169
+ }
170
+
171
+ settings.set(path, parsedValue as SettingValue<typeof path>);
172
+ }
173
+
150
174
  // =============================================================================
151
175
  // Command Handlers
152
176
  // =============================================================================
153
177
 
154
178
  export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
155
- const settingsManager = await SettingsManager.create();
179
+ await Settings.init();
156
180
 
157
181
  switch (cmd.action) {
158
182
  case "list":
159
- handleList(settingsManager, cmd.flags);
183
+ handleList(cmd.flags);
160
184
  break;
161
185
  case "get":
162
- handleGet(settingsManager, cmd.key, cmd.flags);
186
+ handleGet(cmd.key, cmd.flags);
163
187
  break;
164
188
  case "set":
165
- await handleSet(settingsManager, cmd.key, cmd.value, cmd.flags);
189
+ await handleSet(cmd.key, cmd.value, cmd.flags);
166
190
  break;
167
191
  case "reset":
168
- await handleReset(settingsManager, cmd.key, cmd.flags);
192
+ await handleReset(cmd.key, cmd.flags);
169
193
  break;
170
194
  case "path":
171
195
  handlePath();
@@ -173,12 +197,14 @@ export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
173
197
  }
174
198
  }
175
199
 
176
- function handleList(settingsManager: SettingsManager, flags: { json?: boolean }): void {
200
+ function handleList(flags: { json?: boolean }): void {
201
+ const defs = getAllSettingDefs();
202
+
177
203
  if (flags.json) {
178
204
  const result: Record<string, { value: unknown; type: string; description: string }> = {};
179
- for (const def of SETTINGS_DEFS) {
180
- result[def.id] = {
181
- value: def.get(settingsManager),
205
+ for (const def of defs) {
206
+ result[def.path] = {
207
+ value: settings.get(def.path as SettingPath),
182
208
  type: def.type,
183
209
  description: def.description,
184
210
  };
@@ -189,9 +215,8 @@ function handleList(settingsManager: SettingsManager, flags: { json?: boolean })
189
215
 
190
216
  console.log(chalk.bold("Settings:\n"));
191
217
 
192
- // Group by tab
193
218
  const groups: Record<string, SettingDef[]> = {};
194
- for (const def of SETTINGS_DEFS) {
219
+ for (const def of defs) {
195
220
  if (!groups[def.tab]) {
196
221
  groups[def.tab] = [];
197
222
  }
@@ -207,16 +232,16 @@ function handleList(settingsManager: SettingsManager, flags: { json?: boolean })
207
232
  for (const group of sortedGroups) {
208
233
  console.log(chalk.bold.blue(`[${group}]`));
209
234
  for (const def of groups[group]) {
210
- const value = def.get(settingsManager);
235
+ const value = settings.get(def.path as SettingPath);
211
236
  const valueStr = formatValue(value);
212
- const typeStr = getTypeDisplay(def, settingsManager);
213
- console.log(` ${chalk.white(def.id)} = ${valueStr} ${chalk.dim(typeStr)}`);
237
+ const typeStr = getTypeDisplay(def);
238
+ console.log(` ${chalk.white(def.path)} = ${valueStr} ${chalk.dim(typeStr)}`);
214
239
  }
215
240
  console.log("");
216
241
  }
217
242
  }
218
243
 
219
- function handleGet(settingsManager: SettingsManager, key: string | undefined, flags: { json?: boolean }): void {
244
+ function handleGet(key: string | undefined, flags: { json?: boolean }): void {
220
245
  if (!key) {
221
246
  console.error(chalk.red(`Usage: ${APP_NAME} config get <key>`));
222
247
  console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
@@ -230,22 +255,17 @@ function handleGet(settingsManager: SettingsManager, key: string | undefined, fl
230
255
  process.exit(1);
231
256
  }
232
257
 
233
- const value = def.get(settingsManager);
258
+ const value = settings.get(def.path as SettingPath);
234
259
 
235
260
  if (flags.json) {
236
- console.log(JSON.stringify({ key: def.id, value, type: def.type, description: def.description }, null, 2));
261
+ console.log(JSON.stringify({ key: def.path, value, type: def.type, description: def.description }, null, 2));
237
262
  return;
238
263
  }
239
264
 
240
265
  console.log(formatValue(value));
241
266
  }
242
267
 
243
- async function handleSet(
244
- settingsManager: SettingsManager,
245
- key: string | undefined,
246
- value: string | undefined,
247
- flags: { json?: boolean },
248
- ): Promise<void> {
268
+ async function handleSet(key: string | undefined, value: string | undefined, flags: { json?: boolean }): Promise<void> {
249
269
  if (!key || value === undefined) {
250
270
  console.error(chalk.red(`Usage: ${APP_NAME} config set <key> <value>`));
251
271
  console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
@@ -259,28 +279,23 @@ async function handleSet(
259
279
  process.exit(1);
260
280
  }
261
281
 
262
- let parsedValue: unknown;
263
282
  try {
264
- parsedValue = parseValue(value, def, settingsManager);
283
+ parseAndSetValue(def.path as SettingPath, value);
265
284
  } catch (err) {
266
285
  console.error(chalk.red(String(err)));
267
286
  process.exit(1);
268
287
  }
269
288
 
270
- def.set(settingsManager, parsedValue as never);
289
+ const newValue = settings.get(def.path as SettingPath);
271
290
 
272
291
  if (flags.json) {
273
- console.log(JSON.stringify({ key: def.id, value: parsedValue }));
292
+ console.log(JSON.stringify({ key: def.path, value: newValue }));
274
293
  } else {
275
- console.log(chalk.green(`${theme.status.success} Set ${def.id} = ${formatValue(parsedValue)}`));
294
+ console.log(chalk.green(`${theme.status.success} Set ${def.path} = ${formatValue(newValue)}`));
276
295
  }
277
296
  }
278
297
 
279
- async function handleReset(
280
- settingsManager: SettingsManager,
281
- key: string | undefined,
282
- flags: { json?: boolean },
283
- ): Promise<void> {
298
+ async function handleReset(key: string | undefined, flags: { json?: boolean }): Promise<void> {
284
299
  if (!key) {
285
300
  console.error(chalk.red(`Usage: ${APP_NAME} config reset <key>`));
286
301
  console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
@@ -294,16 +309,14 @@ async function handleReset(
294
309
  process.exit(1);
295
310
  }
296
311
 
297
- // Get default value from a fresh in-memory settings manager
298
- const defaults = SettingsManager.inMemory();
299
- const defaultValue = def.get(defaults);
300
-
301
- def.set(settingsManager, defaultValue as never);
312
+ const path = def.path as SettingPath;
313
+ const defaultValue = getDefault(path);
314
+ settings.set(path, defaultValue as SettingValue<typeof path>);
302
315
 
303
316
  if (flags.json) {
304
- console.log(JSON.stringify({ key: def.id, value: defaultValue }));
317
+ console.log(JSON.stringify({ key: def.path, value: defaultValue }));
305
318
  } else {
306
- console.log(chalk.green(`${theme.status.success} Reset ${def.id} to ${formatValue(defaultValue)}`));
319
+ console.log(chalk.green(`${theme.status.success} Reset ${def.path} to ${formatValue(defaultValue)}`));
307
320
  }
308
321
  }
309
322
 
@@ -332,8 +345,8 @@ ${chalk.bold("Examples:")}
332
345
  ${APP_NAME} config list
333
346
  ${APP_NAME} config get theme
334
347
  ${APP_NAME} config set theme catppuccin-mocha
335
- ${APP_NAME} config set autoCompact false
336
- ${APP_NAME} config set thinkingLevel medium
348
+ ${APP_NAME} config set compaction.enabled false
349
+ ${APP_NAME} config set defaultThinkingLevel medium
337
350
  ${APP_NAME} config reset steeringMode
338
351
  ${APP_NAME} config list --json
339
352
 
@@ -135,6 +135,24 @@ function getBinaryName(): string {
135
135
  return `${APP_NAME}-${os}-${archName}`;
136
136
  }
137
137
 
138
+ /**
139
+ * Get the appropriate native addon name for this platform.
140
+ * Uses process.platform directly (linux, darwin, win32).
141
+ */
142
+ function getNativeAddonName(): string {
143
+ const platform = process.platform;
144
+ const arch = process.arch;
145
+
146
+ if (!["linux", "darwin", "win32"].includes(platform)) {
147
+ throw new Error(`Unsupported platform: ${platform}`);
148
+ }
149
+ if (!["x64", "arm64"].includes(arch)) {
150
+ throw new Error(`Unsupported architecture: ${arch}`);
151
+ }
152
+
153
+ return `pi_natives.${platform}-${arch}.node`;
154
+ }
155
+
138
156
  /**
139
157
  * Update via bun package manager.
140
158
  */
@@ -154,16 +172,24 @@ async function updateViaBun(): Promise<void> {
154
172
  */
155
173
  async function updateViaBinary(release: ReleaseInfo): Promise<void> {
156
174
  const binaryName = getBinaryName();
175
+ const nativeAddonName = getNativeAddonName();
176
+
157
177
  const asset = release.assets.find(a => a.name === binaryName);
178
+ const nativeAsset = release.assets.find(a => a.name === nativeAddonName);
158
179
 
159
180
  if (!asset) {
160
181
  throw new Error(`No binary found for ${binaryName}`);
161
182
  }
183
+ if (!nativeAsset) {
184
+ throw new Error(`No native addon found for ${nativeAddonName}`);
185
+ }
162
186
 
163
187
  const execPath = process.execPath;
164
- const _execDir = path.dirname(execPath);
188
+ const execDir = path.dirname(execPath);
165
189
  const tempPath = `${execPath}.new`;
166
190
  const backupPath = `${execPath}.bak`;
191
+ const nativePath = path.join(execDir, nativeAddonName);
192
+ const nativeTempPath = `${nativePath}.new`;
167
193
 
168
194
  console.log(chalk.dim(`Downloading ${binaryName}...`));
169
195
 
@@ -177,6 +203,18 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
177
203
  const nodeStream = Readable.fromWeb(response.body as import("stream/web").ReadableStream);
178
204
  await pipeline(nodeStream, fileStream);
179
205
 
206
+ // Download native addon
207
+ console.log(chalk.dim(`Downloading ${nativeAddonName}...`));
208
+
209
+ const nativeResponse = await fetch(nativeAsset.url, { redirect: "follow" });
210
+ if (!nativeResponse.ok || !nativeResponse.body) {
211
+ throw new Error(`Native addon download failed: ${nativeResponse.statusText}`);
212
+ }
213
+
214
+ const nativeFileStream = fs.createWriteStream(nativeTempPath, { mode: 0o755 });
215
+ const nativeNodeStream = Readable.fromWeb(nativeResponse.body as import("stream/web").ReadableStream);
216
+ await pipeline(nativeNodeStream, nativeFileStream);
217
+
180
218
  // Replace current binary
181
219
  console.log(chalk.dim("Installing update..."));
182
220
 
@@ -187,11 +225,12 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
187
225
  if (!isEnoent(err)) throw err;
188
226
  }
189
227
  await fs.promises.rename(execPath, backupPath);
190
-
191
228
  await fs.promises.rename(tempPath, execPath);
192
-
193
229
  await fs.promises.unlink(backupPath);
194
230
 
231
+ // Replace native addon (no backup needed, just overwrite)
232
+ await fs.promises.rename(nativeTempPath, nativePath);
233
+
195
234
  console.log(chalk.green(`\n${theme.status.success} Updated to ${release.version}`));
196
235
  console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
197
236
  } catch (err) {
@@ -201,6 +240,9 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
201
240
  if (fs.existsSync(tempPath)) {
202
241
  await fs.promises.unlink(tempPath);
203
242
  }
243
+ if (fs.existsSync(nativeTempPath)) {
244
+ await fs.promises.unlink(nativeTempPath);
245
+ }
204
246
  throw err;
205
247
  }
206
248
  }
@@ -5,7 +5,7 @@ import type { ControlledGit } from "../../commit/git";
5
5
  import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
6
6
  import type { ModelRegistry } from "../../config/model-registry";
7
7
  import { renderPromptTemplate } from "../../config/prompt-templates";
8
- import type { SettingsManager } from "../../config/settings-manager";
8
+ import type { Settings } from "../../config/settings";
9
9
  import { getMarkdownTheme } from "../../modes/theme/theme";
10
10
  import { createAgentSession } from "../../sdk";
11
11
  import type { AgentSessionEvent } from "../../session/agent-session";
@@ -19,7 +19,7 @@ export interface CommitAgentInput {
19
19
  cwd: string;
20
20
  git: ControlledGit;
21
21
  model: Model<Api>;
22
- settingsManager: SettingsManager;
22
+ settings: Settings;
23
23
  modelRegistry: ModelRegistry;
24
24
  authStorage: AuthStorage;
25
25
  userContext?: string;
@@ -47,7 +47,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
47
47
  git: input.git,
48
48
  authStorage: input.authStorage,
49
49
  modelRegistry: input.modelRegistry,
50
- settingsManager: input.settingsManager,
50
+ settings: input.settings,
51
51
  spawns,
52
52
  state,
53
53
  changelogTargets: input.changelogTargets,
@@ -58,7 +58,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
58
58
  cwd: input.cwd,
59
59
  authStorage: input.authStorage,
60
60
  modelRegistry: input.modelRegistry,
61
- settingsManager: input.settingsManager,
61
+ settingsInstance: input.settings,
62
62
  model: input.model,
63
63
  systemPrompt,
64
64
  customTools: tools,
@@ -8,7 +8,7 @@ import { formatCommitMessage } from "../../commit/message";
8
8
  import { resolvePrimaryModel, resolveSmolModel } from "../../commit/model-selection";
9
9
  import type { CommitCommandArgs, ConventionalAnalysis } from "../../commit/types";
10
10
  import { renderPromptTemplate } from "../../config/prompt-templates";
11
- import { SettingsManager } from "../../config/settings-manager";
11
+ import { Settings } from "../../config/settings";
12
12
  import { discoverAuthStorage, discoverContextFiles, discoverModels } from "../../sdk";
13
13
  import { type ExistingChangelogEntries, runCommitAgentSession } from "./agent";
14
14
  import { generateFallbackProposal } from "./fallback";
@@ -26,7 +26,8 @@ interface CommitExecutionContext {
26
26
  export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
27
27
  const cwd = process.cwd();
28
28
  const git = new ControlledGit(cwd);
29
- const [settingsManager, authStorage] = await Promise.all([SettingsManager.create(cwd), discoverAuthStorage()]);
29
+ const [settingsInstance, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
30
+ const settings = settingsInstance;
30
31
 
31
32
  writeStdout("● Resolving model...");
32
33
  const modelRegistry = discoverModels(authStorage);
@@ -40,12 +41,12 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
40
41
  return stagedFiles;
41
42
  })();
42
43
 
43
- const primaryModelPromise = resolvePrimaryModel(args.model, settingsManager, modelRegistry);
44
+ const primaryModelPromise = resolvePrimaryModel(args.model, settings, modelRegistry);
44
45
  const [primaryModelResult, stagedFiles] = await Promise.all([primaryModelPromise, stagedFilesPromise]);
45
46
  const { model: primaryModel, apiKey: primaryApiKey } = primaryModelResult;
46
47
  writeStdout(` └─ ${primaryModel.name}`);
47
48
 
48
- const { model: agentModel } = await resolveSmolModel(settingsManager, modelRegistry, primaryModel, primaryApiKey);
49
+ const { model: agentModel } = await resolveSmolModel(settings, modelRegistry, primaryModel, primaryApiKey);
49
50
 
50
51
  if (stagedFiles.length === 0) {
51
52
  writeStderr("No changes to commit.");
@@ -123,7 +124,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
123
124
  cwd,
124
125
  git,
125
126
  model: agentModel,
126
- settingsManager,
127
+ settings,
127
128
  modelRegistry,
128
129
  authStorage,
129
130
  userContext: args.context,