@pencil-agent/nano-pencil 1.13.0 → 1.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/builtin-extensions.js +20 -0
  2. package/dist/cli.js +24 -1
  3. package/dist/core/config/settings-manager.d.ts +17 -0
  4. package/dist/core/config/settings-manager.js +176 -0
  5. package/dist/core/extensions/loader.js +10 -3
  6. package/dist/core/mcp/mcp-client.js +18 -7
  7. package/dist/core/messages.js +1 -1
  8. package/dist/core/package-manager.d.ts +2 -1
  9. package/dist/core/package-manager.js +17 -22
  10. package/dist/core/runtime/agent-session.js +8 -3
  11. package/dist/extensions/defaults/CLAUDE.md +10 -5
  12. package/dist/extensions/defaults/btw/index.d.ts +8 -0
  13. package/dist/extensions/defaults/btw/index.js +110 -0
  14. package/dist/extensions/defaults/debug/collectors.d.ts +85 -0
  15. package/dist/extensions/defaults/debug/collectors.js +248 -0
  16. package/dist/extensions/defaults/debug/index.d.ts +8 -0
  17. package/dist/extensions/defaults/debug/index.js +189 -0
  18. package/dist/extensions/defaults/grub/README.md +85 -14
  19. package/dist/extensions/defaults/grub/grub-controller.d.ts +25 -4
  20. package/dist/extensions/defaults/grub/grub-controller.js +120 -17
  21. package/dist/extensions/defaults/grub/grub-feature-list.d.ts +35 -0
  22. package/dist/extensions/defaults/grub/grub-feature-list.js +208 -0
  23. package/dist/extensions/defaults/grub/grub-parser.d.ts +1 -1
  24. package/dist/extensions/defaults/grub/grub-parser.js +87 -8
  25. package/dist/extensions/defaults/grub/grub-persistence.d.ts +17 -0
  26. package/dist/extensions/defaults/grub/grub-persistence.js +166 -0
  27. package/dist/extensions/defaults/grub/grub-types.d.ts +50 -3
  28. package/dist/extensions/defaults/grub/grub-types.js +2 -1
  29. package/dist/extensions/defaults/grub/index.d.ts +3 -3
  30. package/dist/extensions/defaults/grub/index.js +257 -21
  31. package/dist/extensions/defaults/loop/cron/cron-scheduler.js +7 -3
  32. package/dist/extensions/defaults/plan/exit-plan-mode-tool.js +11 -20
  33. package/dist/extensions/defaults/plan/index.js +21 -5
  34. package/dist/extensions/defaults/plan/plan-file-manager.d.ts +3 -3
  35. package/dist/extensions/defaults/plan/plan-file-manager.js +12 -7
  36. package/dist/extensions/defaults/plan/teammate-approval.d.ts +4 -1
  37. package/dist/extensions/defaults/plan/teammate-approval.js +4 -1
  38. package/dist/extensions/defaults/presence/index.d.ts +1 -1
  39. package/dist/extensions/defaults/presence/index.js +18 -6
  40. package/dist/extensions/defaults/sal/index.js +66 -24
  41. package/dist/extensions/defaults/sal/terrain.d.ts +13 -3
  42. package/dist/extensions/defaults/sal/terrain.js +65 -37
  43. package/dist/extensions/defaults/security-audit/engine/detector.d.ts +0 -7
  44. package/dist/extensions/defaults/security-audit/engine/detector.js +0 -7
  45. package/dist/extensions/defaults/security-audit/engine/interceptor.d.ts +0 -7
  46. package/dist/extensions/defaults/security-audit/engine/interceptor.js +0 -7
  47. package/dist/extensions/defaults/security-audit/engine/logger.d.ts +0 -7
  48. package/dist/extensions/defaults/security-audit/engine/logger.js +0 -7
  49. package/dist/extensions/defaults/security-audit/interface.d.ts +0 -8
  50. package/dist/extensions/defaults/security-audit/interface.js +0 -8
  51. package/dist/extensions/optional/export-html/index.js +8 -1
  52. package/dist/main.js +9 -2
  53. package/dist/modes/acp/acp-mode.js +4 -1
  54. package/dist/modes/interactive/components/memory-stats.d.ts +0 -5
  55. package/dist/modes/interactive/components/memory-stats.js +0 -5
  56. package/dist/modes/interactive/components/pencil-loader.d.ts +27 -3
  57. package/dist/modes/interactive/components/pencil-loader.js +73 -6
  58. package/dist/modes/interactive/components/soul-stats.d.ts +0 -5
  59. package/dist/modes/interactive/components/soul-stats.js +0 -5
  60. package/dist/modes/interactive/interactive-mode.d.ts +9 -1
  61. package/dist/modes/interactive/interactive-mode.js +87 -16
  62. package/dist/modes/interactive/services/tips.d.ts +20 -0
  63. package/dist/modes/interactive/services/tips.js +96 -0
  64. package/dist/nanopencil-defaults.js +4 -6
  65. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +162 -71
  66. package/dist/node_modules/@pencil-agent/ai/models.generated.js +220 -134
  67. package/dist/node_modules/@pencil-agent/tui/autocomplete.js +10 -3
  68. package/dist/node_modules/@pencil-agent/tui/components/editor.d.ts +2 -0
  69. package/dist/node_modules/@pencil-agent/tui/components/editor.js +6 -2
  70. package/dist/node_modules/@pencil-agent/tui/fuzzy.d.ts +11 -0
  71. package/dist/node_modules/@pencil-agent/tui/fuzzy.js +40 -0
  72. package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
  73. package/dist/node_modules/@pencil-agent/tui/index.js +1 -1
  74. package/dist/packages/mem-core/consolidation.js +2 -1
  75. package/dist/packages/mem-core/engine-scoring-v2.js +33 -9
  76. package/dist/packages/mem-core/extraction.js +2 -1
  77. package/dist/packages/soul-core/config.d.ts +0 -5
  78. package/dist/packages/soul-core/config.js +0 -5
  79. package/dist/packages/soul-core/evolution.d.ts +0 -5
  80. package/dist/packages/soul-core/evolution.js +0 -5
  81. package/dist/packages/soul-core/index.d.ts +0 -5
  82. package/dist/packages/soul-core/index.js +0 -5
  83. package/dist/packages/soul-core/injection.d.ts +0 -5
  84. package/dist/packages/soul-core/injection.js +0 -5
  85. package/dist/packages/soul-core/manager.d.ts +0 -5
  86. package/dist/packages/soul-core/manager.js +0 -5
  87. package/dist/packages/soul-core/src/config.d.ts +0 -5
  88. package/dist/packages/soul-core/src/config.js +0 -5
  89. package/dist/packages/soul-core/src/evolution.d.ts +0 -5
  90. package/dist/packages/soul-core/src/evolution.js +0 -5
  91. package/dist/packages/soul-core/src/index.d.ts +0 -5
  92. package/dist/packages/soul-core/src/index.js +0 -5
  93. package/dist/packages/soul-core/src/injection.d.ts +0 -5
  94. package/dist/packages/soul-core/src/injection.js +0 -5
  95. package/dist/packages/soul-core/src/manager.d.ts +0 -5
  96. package/dist/packages/soul-core/src/manager.js +0 -5
  97. package/dist/packages/soul-core/src/store.d.ts +0 -5
  98. package/dist/packages/soul-core/src/store.js +0 -5
  99. package/dist/packages/soul-core/src/types.d.ts +0 -5
  100. package/dist/packages/soul-core/src/types.js +0 -5
  101. package/dist/packages/soul-core/store.d.ts +0 -5
  102. package/dist/packages/soul-core/store.js +0 -5
  103. package/dist/packages/soul-core/types.d.ts +0 -5
  104. package/dist/packages/soul-core/types.js +0 -5
  105. package/dist/utils/startup-profiler.d.ts +31 -0
  106. package/dist/utils/startup-profiler.js +48 -0
  107. package/docs/SAL/345/256/236/351/252/214/350/257/204/344/274/260/346/226/271/345/274/217/357/274/210/344/273/243/347/240/201/345/257/271/346/257/224/344/270/216/345/244/232worktree/357/274/211.md +158 -0
  108. package/docs/SAL/346/200/273/344/275/223/350/267/257/347/272/277/344/270/216/345/256/236/351/252/214/345/244/247/347/272/262.md +213 -0
  109. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +251 -0
  110. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +123 -0
  111. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -0
  112. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +158 -0
  113. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +128 -0
  114. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +321 -0
  115. package/docs/loop-usage-examples.md +215 -0
  116. package/docs/planmode.md +1987 -0
  117. package/package.json +5 -3
  118. package/docs/SAL/345/256/236/351/252/214/350/276/271/347/225/214/344/270/216/345/220/216/347/273/255/346/234/272/345/210/266/350/267/257/347/272/277.md +0 -305
  119. package/docs/SAL/345/257/271/346/257/224/350/257/225/351/252/214/350/256/276/350/256/241.md +0 -667
  120. package/docs/SAL/347/273/223/346/236/204/351/224/232/347/202/271/345/256/232/344/275/215/346/226/271/346/241/210.md +0 -851
@@ -24,6 +24,8 @@ const BUNDLED_SAL_EXTENSION = join(__dirname, "extensions", "defaults", "sal", "
24
24
  const BUNDLED_GRUB_EXTENSION = join(__dirname, "extensions", "defaults", "grub", "index.js");
25
25
  const BUNDLED_SUBAGENT_EXTENSION = join(__dirname, "extensions", "defaults", "subagent", "index.js");
26
26
  const BUNDLED_TEAM_EXTENSION = join(__dirname, "extensions", "defaults", "team", "index.js");
27
+ const BUNDLED_BTW_EXTENSION = join(__dirname, "extensions", "defaults", "btw", "index.js");
28
+ const BUNDLED_DEBUG_EXTENSION = join(__dirname, "extensions", "defaults", "debug", "index.js");
27
29
  const BUNDLED_MCP_EXTENSION = join(__dirname, "extensions", "defaults", "mcp", "index.js");
28
30
  const BUNDLED_EXPORT_HTML_EXTENSION = join(__dirname, "extensions", "optional", "export-html", "index.js");
29
31
  /** Find package root from current module location (containing package.json with nano-pencil related name) */
@@ -209,6 +211,24 @@ export function getBuiltinExtensionPaths() {
209
211
  if (existsSync(teamTs))
210
212
  paths.push(teamTs);
211
213
  }
214
+ // === BTW extension (quick side question without interrupting) ===
215
+ if (existsSync(BUNDLED_BTW_EXTENSION)) {
216
+ paths.push(BUNDLED_BTW_EXTENSION);
217
+ }
218
+ else {
219
+ const btwTs = join(__dirname, "extensions", "defaults", "btw", "index.ts");
220
+ if (existsSync(btwTs))
221
+ paths.push(btwTs);
222
+ }
223
+ // === Debug extension (system diagnostics with three-layer analysis) ===
224
+ if (existsSync(BUNDLED_DEBUG_EXTENSION)) {
225
+ paths.push(BUNDLED_DEBUG_EXTENSION);
226
+ }
227
+ else {
228
+ const debugTs = join(__dirname, "extensions", "defaults", "debug", "index.ts");
229
+ if (existsSync(debugTs))
230
+ paths.push(debugTs);
231
+ }
212
232
  // Built-in MCP extension
213
233
  if (existsSync(BUNDLED_MCP_EXTENSION)) {
214
234
  paths.push(BUNDLED_MCP_EXTENSION);
package/dist/cli.js CHANGED
@@ -6,5 +6,28 @@
6
6
  * [HERE]: Entry point; orchestrates argument parsing and mode selection
7
7
  */
8
8
  process.title = "nanopencil";
9
+ import { readFile } from "node:fs/promises";
10
+ import { fileURLToPath } from "node:url";
11
+ import { dirname, join } from "node:path";
12
+ const args = process.argv.slice(2);
13
+ // Fast path: --version, --help don't need full module loading
14
+ if (args.includes("--version")) {
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ // In dev, package.json is in project root; in bundle, it's two levels up from dist/cli.js
18
+ const pkgPath = __dirname.endsWith("dist") ? join(__dirname, "..", "package.json") : join(__dirname, "package.json");
19
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
20
+ console.log(pkg.version);
21
+ process.exit(0);
22
+ }
23
+ if (args.includes("--help") || args.includes("-h")) {
24
+ console.log(`nanoPencil AI coding agent`);
25
+ console.log(`Usage: nanopencil [options]`);
26
+ console.log(` nanopencil [command] [options]`);
27
+ console.log(`Options:`);
28
+ console.log(` --version Show version`);
29
+ console.log(` --help, -h Show this help`);
30
+ process.exit(0);
31
+ }
9
32
  import { main } from "./main.js";
10
- main(process.argv.slice(2));
33
+ main(args);
@@ -113,6 +113,8 @@ export interface Settings {
113
113
  export type SettingsScope = "global" | "project";
114
114
  export interface SettingsStorage {
115
115
  withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void;
116
+ withLockAsync(scope: SettingsScope, fn: (current: string | undefined) => Promise<string | undefined>): Promise<void>;
117
+ readStorageAsync(scope: SettingsScope): Promise<string | undefined>;
116
118
  }
117
119
  export interface SettingsError {
118
120
  scope: SettingsScope;
@@ -123,11 +125,15 @@ export declare class FileSettingsStorage implements SettingsStorage {
123
125
  private projectSettingsPath;
124
126
  constructor(cwd?: string, agentDir?: string);
125
127
  withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void;
128
+ withLockAsync(scope: SettingsScope, fn: (current: string | undefined) => Promise<string | undefined>): Promise<void>;
129
+ readStorageAsync(scope: SettingsScope): Promise<string | undefined>;
126
130
  }
127
131
  export declare class InMemorySettingsStorage implements SettingsStorage {
128
132
  private global;
129
133
  private project;
130
134
  withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void;
135
+ withLockAsync(scope: SettingsScope, fn: (current: string | undefined) => Promise<string | undefined>): Promise<void>;
136
+ readStorageAsync(scope: SettingsScope): Promise<string | undefined>;
131
137
  }
132
138
  export declare class SettingsManager {
133
139
  private storage;
@@ -145,12 +151,18 @@ export declare class SettingsManager {
145
151
  private constructor();
146
152
  /** Create a SettingsManager that loads from files */
147
153
  static create(cwd?: string, agentDir?: string): SettingsManager;
154
+ /** Create a SettingsManager that loads from files async */
155
+ static createAsync(cwd?: string, agentDir?: string): Promise<SettingsManager>;
148
156
  /** Create a SettingsManager from an arbitrary storage backend */
149
157
  static fromStorage(storage: SettingsStorage): SettingsManager;
158
+ /** Create a SettingsManager from an arbitrary storage backend async */
159
+ static fromStorageAsync(storage: SettingsStorage): Promise<SettingsManager>;
150
160
  /** Create an in-memory SettingsManager (no file I/O) */
151
161
  static inMemory(settings?: Partial<Settings>): SettingsManager;
152
162
  private static loadFromStorage;
163
+ private static loadFromStorageAsync;
153
164
  private static tryLoadFromStorage;
165
+ private static tryLoadFromStorageAsync;
154
166
  /** Migrate old settings format to new format */
155
167
  private static migrateSettings;
156
168
  getGlobalSettings(): Settings;
@@ -158,6 +170,7 @@ export declare class SettingsManager {
158
170
  /** Get merged effective settings (project overrides global). */
159
171
  getSettings(): Settings;
160
172
  reload(): void;
173
+ reloadAsync(): Promise<void>;
161
174
  /** Apply additional overrides on top of current settings */
162
175
  applyOverrides(overrides: Partial<Settings>): void;
163
176
  /** Mark a global field as modified during this session */
@@ -167,10 +180,14 @@ export declare class SettingsManager {
167
180
  private recordError;
168
181
  private clearModifiedScope;
169
182
  private enqueueWrite;
183
+ private enqueueWriteAsync;
170
184
  private cloneModifiedNestedFields;
171
185
  private persistScopedSettings;
186
+ private persistScopedSettingsAsync;
172
187
  private save;
188
+ private saveAsync;
173
189
  private saveProjectSettings;
190
+ private saveProjectSettingsAsync;
174
191
  flush(): Promise<void>;
175
192
  drainErrors(): SettingsError[];
176
193
  getLastChangelogVersion(): string | undefined;
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
2
  import { dirname, join } from "path";
3
3
  import lockfile from "proper-lockfile";
4
4
  import { APP_NAME, CONFIG_DIR_NAME, getAgentDir } from "../../config.js";
5
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
5
6
  /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
6
7
  function deepMergeSettings(base, overrides) {
7
8
  const result = { ...base };
@@ -63,6 +64,42 @@ export class FileSettingsStorage {
63
64
  }
64
65
  }
65
66
  }
67
+ async withLockAsync(scope, fn) {
68
+ const filePath = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath;
69
+ const dir = dirname(filePath);
70
+ let release;
71
+ try {
72
+ const fileExists = existsSync(filePath);
73
+ if (fileExists) {
74
+ release = lockfile.lockSync(filePath, { realpath: false });
75
+ }
76
+ // Read inside the lock so we capture a consistent snapshot before writing.
77
+ const current = fileExists ? await readFile(filePath, "utf-8") : undefined;
78
+ const next = await fn(current);
79
+ if (next !== undefined) {
80
+ if (!existsSync(dir)) {
81
+ await mkdir(dir, { recursive: true });
82
+ }
83
+ // Ensure we hold the lock for the write even if we only read above.
84
+ if (!release) {
85
+ release = lockfile.lockSync(filePath, { realpath: false });
86
+ }
87
+ await writeFile(filePath, next, "utf-8");
88
+ }
89
+ }
90
+ finally {
91
+ if (release) {
92
+ release();
93
+ }
94
+ }
95
+ }
96
+ async readStorageAsync(scope) {
97
+ const filePath = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath;
98
+ if (!existsSync(filePath)) {
99
+ return undefined;
100
+ }
101
+ return readFile(filePath, "utf-8");
102
+ }
66
103
  }
67
104
  export class InMemorySettingsStorage {
68
105
  global;
@@ -79,6 +116,21 @@ export class InMemorySettingsStorage {
79
116
  }
80
117
  }
81
118
  }
119
+ async withLockAsync(scope, fn) {
120
+ const current = scope === "global" ? this.global : this.project;
121
+ const next = await fn(current);
122
+ if (next !== undefined) {
123
+ if (scope === "global") {
124
+ this.global = next;
125
+ }
126
+ else {
127
+ this.project = next;
128
+ }
129
+ }
130
+ }
131
+ async readStorageAsync(scope) {
132
+ return scope === "global" ? this.global : this.project;
133
+ }
82
134
  }
83
135
  export class SettingsManager {
84
136
  storage;
@@ -107,6 +159,11 @@ export class SettingsManager {
107
159
  const storage = new FileSettingsStorage(cwd, agentDir);
108
160
  return SettingsManager.fromStorage(storage);
109
161
  }
162
+ /** Create a SettingsManager that loads from files async */
163
+ static async createAsync(cwd = process.cwd(), agentDir = getAgentDir()) {
164
+ const storage = new FileSettingsStorage(cwd, agentDir);
165
+ return SettingsManager.fromStorageAsync(storage);
166
+ }
110
167
  /** Create a SettingsManager from an arbitrary storage backend */
111
168
  static fromStorage(storage) {
112
169
  const globalLoad = SettingsManager.tryLoadFromStorage(storage, "global");
@@ -120,6 +177,21 @@ export class SettingsManager {
120
177
  }
121
178
  return new SettingsManager(storage, globalLoad.settings, projectLoad.settings, globalLoad.error, projectLoad.error, initialErrors);
122
179
  }
180
+ /** Create a SettingsManager from an arbitrary storage backend async */
181
+ static async fromStorageAsync(storage) {
182
+ const [globalLoadResult, projectLoadResult] = await Promise.all([
183
+ SettingsManager.tryLoadFromStorageAsync(storage, "global"),
184
+ SettingsManager.tryLoadFromStorageAsync(storage, "project"),
185
+ ]);
186
+ const initialErrors = [];
187
+ if (globalLoadResult.error) {
188
+ initialErrors.push({ scope: "global", error: globalLoadResult.error });
189
+ }
190
+ if (projectLoadResult.error) {
191
+ initialErrors.push({ scope: "project", error: projectLoadResult.error });
192
+ }
193
+ return new SettingsManager(storage, globalLoadResult.settings, projectLoadResult.settings, globalLoadResult.error, projectLoadResult.error, initialErrors);
194
+ }
123
195
  /** Create an in-memory SettingsManager (no file I/O) */
124
196
  static inMemory(settings = {}) {
125
197
  const storage = new InMemorySettingsStorage();
@@ -137,6 +209,14 @@ export class SettingsManager {
137
209
  const settings = JSON.parse(content);
138
210
  return SettingsManager.migrateSettings(settings);
139
211
  }
212
+ static async loadFromStorageAsync(storage, scope) {
213
+ const content = await storage.readStorageAsync(scope);
214
+ if (!content) {
215
+ return {};
216
+ }
217
+ const settings = JSON.parse(content);
218
+ return SettingsManager.migrateSettings(settings);
219
+ }
140
220
  static tryLoadFromStorage(storage, scope) {
141
221
  try {
142
222
  return { settings: SettingsManager.loadFromStorage(storage, scope), error: null };
@@ -145,6 +225,14 @@ export class SettingsManager {
145
225
  return { settings: {}, error: error };
146
226
  }
147
227
  }
228
+ static async tryLoadFromStorageAsync(storage, scope) {
229
+ try {
230
+ return { settings: await SettingsManager.loadFromStorageAsync(storage, scope), error: null };
231
+ }
232
+ catch (error) {
233
+ return { settings: {}, error: error };
234
+ }
235
+ }
148
236
  /** Migrate old settings format to new format */
149
237
  static migrateSettings(settings) {
150
238
  // Migrate queueMode -> steeringMode
@@ -210,6 +298,33 @@ export class SettingsManager {
210
298
  }
211
299
  this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
212
300
  }
301
+ async reloadAsync() {
302
+ const [globalLoadResult, projectLoadResult] = await Promise.all([
303
+ SettingsManager.tryLoadFromStorageAsync(this.storage, "global"),
304
+ SettingsManager.tryLoadFromStorageAsync(this.storage, "project"),
305
+ ]);
306
+ if (!globalLoadResult.error) {
307
+ this.globalSettings = globalLoadResult.settings;
308
+ this.globalSettingsLoadError = null;
309
+ }
310
+ else {
311
+ this.globalSettingsLoadError = globalLoadResult.error;
312
+ this.recordError("global", globalLoadResult.error);
313
+ }
314
+ this.modifiedFields.clear();
315
+ this.modifiedNestedFields.clear();
316
+ this.modifiedProjectFields.clear();
317
+ this.modifiedProjectNestedFields.clear();
318
+ if (!projectLoadResult.error) {
319
+ this.projectSettings = projectLoadResult.settings;
320
+ this.projectSettingsLoadError = null;
321
+ }
322
+ else {
323
+ this.projectSettingsLoadError = projectLoadResult.error;
324
+ this.recordError("project", projectLoadResult.error);
325
+ }
326
+ this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
327
+ }
213
328
  /** Apply additional overrides on top of current settings */
214
329
  applyOverrides(overrides) {
215
330
  this.settings = deepMergeSettings(this.settings, overrides);
@@ -257,6 +372,17 @@ export class SettingsManager {
257
372
  this.recordError(scope, error);
258
373
  });
259
374
  }
375
+ enqueueWriteAsync(scope, task) {
376
+ this.writeQueue = this.writeQueue
377
+ .then(async () => {
378
+ await task();
379
+ this.clearModifiedScope(scope);
380
+ })
381
+ .catch((error) => {
382
+ this.recordError(scope, error);
383
+ });
384
+ return this.writeQueue;
385
+ }
260
386
  cloneModifiedNestedFields(source) {
261
387
  const snapshot = new Map();
262
388
  for (const [key, value] of source.entries()) {
@@ -289,6 +415,31 @@ export class SettingsManager {
289
415
  return JSON.stringify(mergedSettings, null, 2);
290
416
  });
291
417
  }
418
+ async persistScopedSettingsAsync(scope, snapshotSettings, modifiedFields, modifiedNestedFields) {
419
+ await this.storage.withLockAsync(scope, async (current) => {
420
+ const currentFileSettings = current
421
+ ? SettingsManager.migrateSettings(JSON.parse(current))
422
+ : {};
423
+ const mergedSettings = { ...currentFileSettings };
424
+ for (const field of modifiedFields) {
425
+ const value = snapshotSettings[field];
426
+ if (modifiedNestedFields.has(field) && typeof value === "object" && value !== null) {
427
+ const nestedModified = modifiedNestedFields.get(field);
428
+ const baseNested = currentFileSettings[field] ?? {};
429
+ const inMemoryNested = value;
430
+ const mergedNested = { ...baseNested };
431
+ for (const nestedKey of nestedModified) {
432
+ mergedNested[nestedKey] = inMemoryNested[nestedKey];
433
+ }
434
+ mergedSettings[field] = mergedNested;
435
+ }
436
+ else {
437
+ mergedSettings[field] = value;
438
+ }
439
+ }
440
+ return JSON.stringify(mergedSettings, null, 2);
441
+ });
442
+ }
292
443
  save() {
293
444
  this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
294
445
  if (this.globalSettingsLoadError) {
@@ -301,6 +452,18 @@ export class SettingsManager {
301
452
  this.persistScopedSettings("global", snapshotGlobalSettings, modifiedFields, modifiedNestedFields);
302
453
  });
303
454
  }
455
+ async saveAsync() {
456
+ this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
457
+ if (this.globalSettingsLoadError) {
458
+ return;
459
+ }
460
+ const snapshotGlobalSettings = structuredClone(this.globalSettings);
461
+ const modifiedFields = new Set(this.modifiedFields);
462
+ const modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedNestedFields);
463
+ await this.enqueueWriteAsync("global", async () => {
464
+ await this.persistScopedSettingsAsync("global", snapshotGlobalSettings, modifiedFields, modifiedNestedFields);
465
+ });
466
+ }
304
467
  saveProjectSettings(settings) {
305
468
  this.projectSettings = structuredClone(settings);
306
469
  this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
@@ -314,6 +477,19 @@ export class SettingsManager {
314
477
  this.persistScopedSettings("project", snapshotProjectSettings, modifiedFields, modifiedNestedFields);
315
478
  });
316
479
  }
480
+ async saveProjectSettingsAsync(settings) {
481
+ this.projectSettings = structuredClone(settings);
482
+ this.settings = deepMergeSettings(this.globalSettings, this.projectSettings);
483
+ if (this.projectSettingsLoadError) {
484
+ return;
485
+ }
486
+ const snapshotProjectSettings = structuredClone(this.projectSettings);
487
+ const modifiedFields = new Set(this.modifiedProjectFields);
488
+ const modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedProjectNestedFields);
489
+ await this.enqueueWriteAsync("project", async () => {
490
+ await this.persistScopedSettingsAsync("project", snapshotProjectSettings, modifiedFields, modifiedNestedFields);
491
+ });
492
+ }
317
493
  async flush() {
318
494
  await this.writeQueue;
319
495
  }
@@ -260,10 +260,17 @@ export async function loadExtensions(paths, cwd, eventBus) {
260
260
  const errors = [];
261
261
  const resolvedEventBus = eventBus ?? createEventBus();
262
262
  const runtime = createExtensionRuntime();
263
- for (const extPath of paths) {
264
- const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);
263
+ // Parallel loading for faster startup.
264
+ // Note: unlike the old sequential loop, this collects all errors before surfacing
265
+ // them — a crashing extension no longer aborts early. This is intentional: we want
266
+ // all extensions to attempt loading so error reports are comprehensive, not truncated.
267
+ const results = await Promise.all(paths.map((extPath) => loadExtension(extPath, cwd, resolvedEventBus, runtime).then((result) => ({
268
+ ...result,
269
+ path: extPath,
270
+ }))));
271
+ for (const { extension, error, path } of results) {
265
272
  if (error) {
266
- errors.push({ path: extPath, error });
273
+ errors.push({ path, error });
267
274
  continue;
268
275
  }
269
276
  if (extension) {
@@ -174,13 +174,24 @@ export class MCPClient {
174
174
  }
175
175
  async spawnProcess(spec, env, cwd) {
176
176
  return await new Promise((resolve, reject) => {
177
- const child = spawn(spec.command, spec.args, {
178
- env,
179
- cwd,
180
- stdio: ["pipe", "pipe", "pipe"],
181
- windowsHide: true,
182
- shell: process.platform === "win32",
183
- });
177
+ // On Windows, use shell mode but pass command as a single string to avoid DEP0190 warning
178
+ // On Unix, use non-shell mode for better security
179
+ const useShell = process.platform === "win32";
180
+ const child = useShell
181
+ ? spawn([spec.command, ...spec.args].map(arg => arg.includes(" ") ? `"${arg}"` : arg).join(" "), {
182
+ env,
183
+ cwd,
184
+ stdio: ["pipe", "pipe", "pipe"],
185
+ windowsHide: true,
186
+ shell: true,
187
+ })
188
+ : spawn(spec.command, spec.args, {
189
+ env,
190
+ cwd,
191
+ stdio: ["pipe", "pipe", "pipe"],
192
+ windowsHide: true,
193
+ shell: false,
194
+ });
184
195
  const onError = (err) => {
185
196
  child.removeListener("spawn", onSpawn);
186
197
  reject(err);
@@ -9,7 +9,7 @@ export const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch tha
9
9
  <summary>
10
10
  `;
11
11
  export const BRANCH_SUMMARY_SUFFIX = `</summary>`;
12
- export const CUSTOM_MESSAGE_TYPES_EXCLUDED_FROM_CONTEXT = new Set(["presence"]);
12
+ export const CUSTOM_MESSAGE_TYPES_EXCLUDED_FROM_CONTEXT = new Set(["presence", "btw"]);
13
13
  /**
14
14
  * Convert a BashExecutionMessage to user message text for LLM context.
15
15
  */
@@ -93,8 +93,9 @@ export declare class DefaultPackageManager implements PackageManager {
93
93
  private parseSource;
94
94
  /**
95
95
  * Check if an npm package needs to be updated.
96
- * - For unpinned packages: check if registry has a newer version
97
96
  * - For pinned packages: check if installed version matches the pinned version
97
+ * - For unpinned packages: skip registry check at startup (returns false); registry
98
+ * check is deferred to explicit /update or auto-update runs to avoid startup latency
98
99
  */
99
100
  private npmNeedsUpdate;
100
101
  private getInstalledNpmVersion;
@@ -129,8 +129,8 @@ function collectFiles(dir, filePattern, skipNodeModules = true, ignoreMatcher, r
129
129
  }
130
130
  }
131
131
  }
132
- catch {
133
- // Ignore errors
132
+ catch (err) {
133
+ console.error("[package-manager] collectFiles failed:", err);
134
134
  }
135
135
  return files;
136
136
  }
@@ -177,8 +177,8 @@ function collectSkillEntries(dir, includeRootFiles = true, ignoreMatcher, rootDi
177
177
  }
178
178
  }
179
179
  }
180
- catch {
181
- // Ignore errors
180
+ catch (err) {
181
+ console.error("[package-manager] collectSkillEntries failed:", err);
182
182
  }
183
183
  return entries;
184
184
  }
@@ -247,8 +247,8 @@ function collectAutoPromptEntries(dir) {
247
247
  }
248
248
  }
249
249
  }
250
- catch {
251
- // Ignore errors
250
+ catch (err) {
251
+ console.error("[package-manager] collectAutoPromptEntries failed:", err);
252
252
  }
253
253
  return entries;
254
254
  }
@@ -283,8 +283,8 @@ function collectAutoThemeEntries(dir) {
283
283
  }
284
284
  }
285
285
  }
286
- catch {
287
- // Ignore errors
286
+ catch (err) {
287
+ console.error("[package-manager] collectAutoThemeEntries failed:", err);
288
288
  }
289
289
  return entries;
290
290
  }
@@ -372,8 +372,8 @@ function collectAutoExtensionEntries(dir) {
372
372
  }
373
373
  }
374
374
  }
375
- catch {
376
- // Ignore errors
375
+ catch (err) {
376
+ console.error("[package-manager] collectAutoExtensionEntries failed:", err);
377
377
  }
378
378
  return entries;
379
379
  }
@@ -862,8 +862,9 @@ export class DefaultPackageManager {
862
862
  }
863
863
  /**
864
864
  * Check if an npm package needs to be updated.
865
- * - For unpinned packages: check if registry has a newer version
866
865
  * - For pinned packages: check if installed version matches the pinned version
866
+ * - For unpinned packages: skip registry check at startup (returns false); registry
867
+ * check is deferred to explicit /update or auto-update runs to avoid startup latency
867
868
  */
868
869
  async npmNeedsUpdate(source, installedPath) {
869
870
  if (isOfflineModeEnabled()) {
@@ -877,15 +878,9 @@ export class DefaultPackageManager {
877
878
  // Pinned: check if installed matches pinned (exact match for now)
878
879
  return installedVersion !== pinnedVersion;
879
880
  }
880
- // Unpinned: check registry for latest version
881
- try {
882
- const latestVersion = await this.getLatestNpmVersion(source.name);
883
- return latestVersion !== installedVersion;
884
- }
885
- catch {
886
- // If we can't check registry, assume it's fine
887
- return false;
888
- }
881
+ // For unpinned packages on startup, skip registry check - assume installed version is fine
882
+ // Registry check will happen only when user explicitly runs /update or auto-update triggers
883
+ return false;
889
884
  }
890
885
  getInstalledNpmVersion(installedPath) {
891
886
  const packageJsonPath = join(installedPath, "package.json");
@@ -1372,8 +1367,8 @@ export class DefaultPackageManager {
1372
1367
  files.push(...collectResourceFiles(p, resourceType));
1373
1368
  }
1374
1369
  }
1375
- catch {
1376
- // Ignore errors
1370
+ catch (err) {
1371
+ console.error("[package-manager] collectFilesFromPaths failed:", err);
1377
1372
  }
1378
1373
  }
1379
1374
  return files;
@@ -339,7 +339,9 @@ export class AgentSession {
339
339
  if (event.type === "message_update") {
340
340
  // Streaming updates: emit to UI immediately, don't await extensions
341
341
  this._emit(event);
342
- this._emitExtensionEvent(event).catch(() => { });
342
+ this._emitExtensionEvent(event).catch((err) => {
343
+ this._logger.error("[extension] message_update event error", { error: err });
344
+ });
343
345
  }
344
346
  else {
345
347
  // All other events: extensions run concurrently with UI notification
@@ -394,8 +396,9 @@ export class AgentSession {
394
396
  await this._soulManager.recordInteraction(context, outcome, "turn");
395
397
  await this._soulManager.updateExpertise(expertiseDomain, tags, outcome === "success");
396
398
  }
397
- catch {
399
+ catch (err) {
398
400
  // Keep Soul failures non-blocking for the main session lifecycle.
401
+ this._logger.warn("[soul] recordInteraction/updateExpertise failed", { error: err });
399
402
  }
400
403
  })();
401
404
  }
@@ -408,7 +411,9 @@ export class AgentSession {
408
411
  type: "agent_end",
409
412
  messages: event.messages,
410
413
  })
411
- .catch(() => { });
414
+ .catch((err) => {
415
+ this._logger.error("[extension] agent_end event error", { error: err });
416
+ });
412
417
  }
413
418
  };
414
419
  /** Extract text content from a message */
@@ -17,16 +17,21 @@ security-audit/engine/interceptor.ts: Request/response interception, Interceptor
17
17
  security-audit/engine/logger.ts: Security event logging, JSON file audit trail
18
18
  security-audit/engine/detector.ts: Vulnerability detection, pattern matching for dangerous commands
19
19
  soul/index.ts: AI personality evolution extension, persistent personality across sessions
20
- grub/index.ts: Grub extension entry - autonomous iterative task runner extracted from legacy loop, /grub command + GRUB_MESSAGE_TYPE renderer
21
- grub/grub-controller.ts: GrubController - drives autonomous grub iterations, state machine for LoopTaskState
22
- grub/grub-parser.ts: Grub command parsing, parseGrubCommand/buildGrubHelp
23
- grub/grub-types.ts: Grub types, GrubStatus/GrubDecisionStatus/GrubDecision/GrubTaskState/GrubTaskSnapshot
24
- grub/README.md: Grub extension documentation - autonomous "keep digging until done" runner
20
+ grub/index.ts: Grub extension entry - long-running autonomous harness, dual-phase system prompts (initializer/coding), /grub command (start/status/resume/stop) + grub renderer, session_start auto-adopt, git harness commit, pruneStale cleanup
21
+ grub/grub-controller.ts: GrubController - state machine for /grub iterations, durable persistState on every transition, adoptResumedTask for cross-session resume, validateCompletion downgrades premature complete when feature-list still has pending entries
22
+ grub/grub-parser.ts: Grub command parsing - parseGrubCommand/buildGrubHelp with resume subcommand, status --json, --max-iter/--max-fail flags
23
+ grub/grub-types.ts: Grub types - GrubStatus/GrubDecisionStatus/GrubDecision/GrubPhase/GrubTaskState/GrubTaskSnapshot/ParsedGrubCommand + FeatureItem/FeatureList (version 1 schema) + PersistedGrubState envelope
24
+ grub/grub-feature-list.ts: feature-list.json IO - readFeatureList/writeFeatureList atomic write, validateFeatureListDiff enforces passes/evidence-only mutations, createInitialFeatureList placeholder, migrateChecklistToFeatureList legacy converter, countPassing/allPassing/firstPending helpers
25
+ grub/grub-persistence.ts: Cross-session persistence - persistState atomic JSON write to .grub/<id>/state.json, loadState shape-validated read, discoverActiveTasks scans .grub/ for running records, pruneStale removes terminal harnesses older than 30 days by default
26
+ grub/README.md: Grub extension documentation - long-running harness contract, feature-list.json schema, completion guard, cross-session resume, legacy migration
25
27
  loop/index.ts: Loop extension entry - session-scoped recurring prompt/command scheduler with pause/resume/run-now/max-runs/quiet, /loop command + LOOP_MESSAGE_TYPE renderer
26
28
  loop/scheduler-controller.ts: SchedulerController - in-memory recurring task store with pause/resume/run-now/max-runs, MAX_SCHEDULED_TASKS=50
27
29
  loop/scheduler-parser.ts: Loop command parsing with flags/subcommands, parseSchedulerCommand/parseDurationSpec/buildSchedulerHelp, --name/--max/--quiet
28
30
  loop/scheduler-types.ts: Scheduled loop types, LoopPayloadKind/ScheduledLoopTask/LoopStartSpec/ParsedSchedulerCommand
29
31
  loop/README.md: Loop extension documentation - recurring scheduler usage and flags
32
+ btw/index.ts: BTW extension entry - /btw command for quick side questions without interrupting main task, uses completeSimple() for lightweight response, BTW_MESSAGE_TYPE renderer
33
+ debug/index.ts: Debug extension entry - /debug command for system diagnostics with three-layer analysis (Phenomenon/Essence/Philosophy), supports /debug env|session|model subcommands, uses completeSimple() for LLM analysis, DEBUG_MESSAGE_TYPE renderer
34
+ debug/collectors.ts: Diagnostic data collectors for /debug command, collectSystemInfo/collectModelInfo/collectSessionInfo/collectConfigInfo/collectGitInfo/collectAgentState, sanitizeForLLM, formatDiagnosticData
30
35
  plan/index.ts: Plan Mode extension entry - registers /plan command, EnterPlanMode/ExitPlanMode tools, permission gating, workflow prompt injection
31
36
  plan/types.ts: PlanModeState, PlanModeAttachment types, PlanModeConfig, PlanApprovalRequest/Response
32
37
  plan/plan-file-manager.ts: PlanFileManager - plan file path management and I/O, slug generation, plans directory
@@ -0,0 +1,8 @@
1
+ /**
2
+ * [WHO]: btwExtension - registers /btw command and BTW message renderer
3
+ * [FROM]: Depends on core/extensions/types
4
+ * [TO]: Auto-loaded by builtin-extensions.ts as a default extension
5
+ * [HERE]: extensions/defaults/btw/index.ts - quick side question without interrupting main task
6
+ */
7
+ import type { ExtensionAPI } from "../../../core/extensions/types.js";
8
+ export default function btwExtension(api: ExtensionAPI): Promise<void>;