@oh-my-pi/pi-coding-agent 3.37.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +44 -3
  3. package/docs/extensions.md +29 -4
  4. package/docs/sdk.md +3 -3
  5. package/package.json +5 -5
  6. package/src/cli/args.ts +8 -0
  7. package/src/config.ts +5 -15
  8. package/src/core/agent-session.ts +217 -51
  9. package/src/core/auth-storage.ts +456 -47
  10. package/src/core/bash-executor.ts +79 -14
  11. package/src/core/custom-commands/types.ts +1 -1
  12. package/src/core/custom-tools/types.ts +1 -1
  13. package/src/core/export-html/index.ts +33 -1
  14. package/src/core/export-html/template.css +99 -0
  15. package/src/core/export-html/template.generated.ts +1 -1
  16. package/src/core/export-html/template.js +133 -8
  17. package/src/core/extensions/index.ts +22 -4
  18. package/src/core/extensions/loader.ts +152 -214
  19. package/src/core/extensions/runner.ts +139 -79
  20. package/src/core/extensions/types.ts +143 -19
  21. package/src/core/extensions/wrapper.ts +5 -8
  22. package/src/core/hooks/types.ts +1 -1
  23. package/src/core/index.ts +2 -1
  24. package/src/core/keybindings.ts +4 -1
  25. package/src/core/model-registry.ts +4 -4
  26. package/src/core/model-resolver.ts +35 -26
  27. package/src/core/sdk.ts +96 -76
  28. package/src/core/settings-manager.ts +45 -14
  29. package/src/core/system-prompt.ts +5 -15
  30. package/src/core/tools/bash.ts +115 -54
  31. package/src/core/tools/find.ts +86 -7
  32. package/src/core/tools/grep.ts +27 -6
  33. package/src/core/tools/index.ts +15 -6
  34. package/src/core/tools/ls.ts +49 -18
  35. package/src/core/tools/render-utils.ts +2 -1
  36. package/src/core/tools/task/worker.ts +35 -12
  37. package/src/core/tools/web-search/auth.ts +37 -32
  38. package/src/core/tools/web-search/providers/anthropic.ts +35 -22
  39. package/src/index.ts +101 -9
  40. package/src/main.ts +60 -20
  41. package/src/migrations.ts +47 -2
  42. package/src/modes/index.ts +2 -2
  43. package/src/modes/interactive/components/assistant-message.ts +25 -7
  44. package/src/modes/interactive/components/bash-execution.ts +5 -0
  45. package/src/modes/interactive/components/branch-summary-message.ts +5 -0
  46. package/src/modes/interactive/components/compaction-summary-message.ts +5 -0
  47. package/src/modes/interactive/components/countdown-timer.ts +38 -0
  48. package/src/modes/interactive/components/custom-editor.ts +8 -0
  49. package/src/modes/interactive/components/custom-message.ts +5 -0
  50. package/src/modes/interactive/components/footer.ts +2 -5
  51. package/src/modes/interactive/components/hook-input.ts +29 -20
  52. package/src/modes/interactive/components/hook-selector.ts +52 -38
  53. package/src/modes/interactive/components/index.ts +39 -0
  54. package/src/modes/interactive/components/login-dialog.ts +160 -0
  55. package/src/modes/interactive/components/model-selector.ts +10 -2
  56. package/src/modes/interactive/components/session-selector.ts +5 -1
  57. package/src/modes/interactive/components/settings-defs.ts +9 -0
  58. package/src/modes/interactive/components/status-line/segments.ts +3 -3
  59. package/src/modes/interactive/components/tool-execution.ts +9 -16
  60. package/src/modes/interactive/components/tree-selector.ts +1 -6
  61. package/src/modes/interactive/interactive-mode.ts +466 -215
  62. package/src/modes/interactive/theme/theme.ts +50 -2
  63. package/src/modes/print-mode.ts +78 -31
  64. package/src/modes/rpc/rpc-mode.ts +186 -78
  65. package/src/modes/rpc/rpc-types.ts +10 -3
  66. package/src/prompts/system-prompt.md +36 -28
  67. package/src/utils/clipboard.ts +90 -50
  68. package/src/utils/image-convert.ts +1 -1
  69. package/src/utils/image-resize.ts +1 -1
  70. package/src/utils/tools-manager.ts +2 -2
@@ -2,8 +2,14 @@
2
2
  * Extension system for lifecycle events and custom tools.
3
3
  */
4
4
 
5
- export { discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions } from "./loader";
6
- export type { BranchHandler, ExtensionErrorListener, NavigateTreeHandler, NewSessionHandler } from "./runner";
5
+ export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions } from "./loader";
6
+ export type {
7
+ BranchHandler,
8
+ ExtensionErrorListener,
9
+ NavigateTreeHandler,
10
+ NewSessionHandler,
11
+ ShutdownHandler,
12
+ } from "./runner";
7
13
  export { ExtensionRunner } from "./runner";
8
14
  export type {
9
15
  AgentEndEvent,
@@ -11,6 +17,7 @@ export type {
11
17
  // Re-exports
12
18
  AgentToolResult,
13
19
  AgentToolUpdateCallback,
20
+ AppAction,
14
21
  AppendEntryHandler,
15
22
  BashToolResultEvent,
16
23
  BeforeAgentStartEvent,
@@ -23,26 +30,32 @@ export type {
23
30
  EditToolResultEvent,
24
31
  ExecOptions,
25
32
  ExecResult,
33
+ Extension,
34
+ ExtensionActions,
26
35
  // API
27
36
  ExtensionAPI,
28
37
  ExtensionCommandContext,
38
+ ExtensionCommandContextActions,
29
39
  // Context
30
40
  ExtensionContext,
41
+ ExtensionContextActions,
31
42
  // Errors
32
43
  ExtensionError,
33
44
  ExtensionEvent,
34
45
  ExtensionFactory,
35
46
  ExtensionFlag,
36
47
  ExtensionHandler,
48
+ ExtensionRuntime,
37
49
  ExtensionShortcut,
38
50
  ExtensionUIContext,
51
+ ExtensionUIDialogOptions,
39
52
  FindToolResultEvent,
40
53
  GetActiveToolsHandler,
41
54
  GetAllToolsHandler,
55
+ GetThinkingLevelHandler,
42
56
  GrepToolResultEvent,
57
+ KeybindingsManager,
43
58
  LoadExtensionsResult,
44
- // Loaded Extension
45
- LoadedExtension,
46
59
  LsToolResultEvent,
47
60
  // Message Rendering
48
61
  MessageRenderer,
@@ -52,6 +65,7 @@ export type {
52
65
  RegisteredCommand,
53
66
  RegisteredTool,
54
67
  SendMessageHandler,
68
+ SendUserMessageHandler,
55
69
  SessionBeforeBranchEvent,
56
70
  SessionBeforeBranchResult,
57
71
  SessionBeforeCompactEvent,
@@ -69,6 +83,8 @@ export type {
69
83
  SessionSwitchEvent,
70
84
  SessionTreeEvent,
71
85
  SetActiveToolsHandler,
86
+ SetModelHandler,
87
+ SetThinkingLevelHandler,
72
88
  // Events - Tool
73
89
  ToolCallEvent,
74
90
  ToolCallEventResult,
@@ -80,6 +96,8 @@ export type {
80
96
  TreePreparation,
81
97
  TurnEndEvent,
82
98
  TurnStartEvent,
99
+ UserBashEvent,
100
+ UserBashEventResult,
83
101
  WriteToolResultEvent,
84
102
  } from "./types";
85
103
  // Type guards
@@ -11,28 +11,19 @@ import { type ExtensionModule, extensionModuleCapability } from "../../capabilit
11
11
  import { loadSync } from "../../discovery";
12
12
  import { getExtensionNameFromPath } from "../../discovery/helpers";
13
13
  import * as piCodingAgent from "../../index";
14
- import { theme } from "../../modes/interactive/theme/theme";
15
14
  import { createEventBus, type EventBus } from "../event-bus";
16
15
  import type { ExecOptions } from "../exec";
17
16
  import { execCommand } from "../exec";
18
17
  import { logger } from "../logger";
19
18
  import type {
20
- AppendEntryHandler,
19
+ Extension,
21
20
  ExtensionAPI,
22
21
  ExtensionContext,
23
22
  ExtensionFactory,
24
- ExtensionFlag,
25
- ExtensionShortcut,
26
- ExtensionUIContext,
27
- GetActiveToolsHandler,
28
- GetAllToolsHandler,
23
+ ExtensionRuntime,
29
24
  LoadExtensionsResult,
30
- LoadedExtension,
31
25
  MessageRenderer,
32
26
  RegisteredCommand,
33
- RegisteredTool,
34
- SendMessageHandler,
35
- SetActiveToolsHandler,
36
27
  ToolDefinition,
37
28
  } from "./types";
38
29
 
@@ -61,80 +52,62 @@ function resolvePath(extPath: string, cwd: string): string {
61
52
  return path.resolve(cwd, expanded);
62
53
  }
63
54
 
64
- function createNoOpUIContext(): ExtensionUIContext {
55
+ type HandlerFn = (...args: unknown[]) => Promise<unknown>;
56
+
57
+ /**
58
+ * Create a runtime with throwing stubs for action methods.
59
+ * Runner.initialize() replaces these with real implementations.
60
+ */
61
+ export function createExtensionRuntime(): ExtensionRuntime {
62
+ const notInitialized = () => {
63
+ throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
64
+ };
65
+
65
66
  return {
66
- select: async () => undefined,
67
- confirm: async () => false,
68
- input: async () => undefined,
69
- notify: () => {},
70
- setStatus: () => {},
71
- setWidget: () => {},
72
- setTitle: () => {},
73
- custom: async () => undefined as never,
74
- setEditorText: () => {},
75
- getEditorText: () => "",
76
- editor: async () => undefined,
77
- get theme() {
78
- return theme;
79
- },
67
+ sendMessage: notInitialized,
68
+ sendUserMessage: notInitialized,
69
+ appendEntry: notInitialized,
70
+ getActiveTools: notInitialized,
71
+ getAllTools: notInitialized,
72
+ setActiveTools: notInitialized,
73
+ setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
74
+ getThinkingLevel: notInitialized,
75
+ setThinkingLevel: notInitialized,
76
+ flagValues: new Map(),
80
77
  };
81
78
  }
82
79
 
83
- type HandlerFn = (...args: unknown[]) => Promise<unknown>;
84
-
80
+ /**
81
+ * Create the ExtensionAPI for an extension.
82
+ * Registration methods write to the extension object.
83
+ * Action methods delegate to the shared runtime.
84
+ */
85
85
  function createExtensionAPI(
86
- handlers: Map<string, HandlerFn[]>,
87
- tools: Map<string, RegisteredTool>,
86
+ extension: Extension,
87
+ runtime: ExtensionRuntime,
88
88
  cwd: string,
89
- extensionPath: string,
90
89
  eventBus: EventBus,
91
- _sharedUI: { ui: ExtensionUIContext; hasUI: boolean },
92
- ): {
93
- api: ExtensionAPI;
94
- messageRenderers: Map<string, MessageRenderer>;
95
- commands: Map<string, RegisteredCommand>;
96
- flags: Map<string, ExtensionFlag>;
97
- flagValues: Map<string, boolean | string>;
98
- shortcuts: Map<KeyId, ExtensionShortcut>;
99
- setSendMessageHandler: (handler: SendMessageHandler) => void;
100
- setAppendEntryHandler: (handler: AppendEntryHandler) => void;
101
- setGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;
102
- setGetAllToolsHandler: (handler: GetAllToolsHandler) => void;
103
- setSetActiveToolsHandler: (handler: SetActiveToolsHandler) => void;
104
- setFlagValue: (name: string, value: boolean | string) => void;
105
- } {
106
- let sendMessageHandler: SendMessageHandler = () => {};
107
- let appendEntryHandler: AppendEntryHandler = () => {};
108
- let getActiveToolsHandler: GetActiveToolsHandler = () => [];
109
- let getAllToolsHandler: GetAllToolsHandler = () => [];
110
- let setActiveToolsHandler: SetActiveToolsHandler = () => {};
111
-
112
- const messageRenderers = new Map<string, MessageRenderer>();
113
- const commands = new Map<string, RegisteredCommand>();
114
- const flags = new Map<string, ExtensionFlag>();
115
- const flagValues = new Map<string, boolean | string>();
116
- const shortcuts = new Map<KeyId, ExtensionShortcut>();
117
-
90
+ ): ExtensionAPI {
118
91
  const api = {
119
92
  logger,
120
93
  typebox: TypeBox,
121
94
  pi: piCodingAgent,
122
95
 
123
96
  on(event: string, handler: HandlerFn): void {
124
- const list = handlers.get(event) ?? [];
97
+ const list = extension.handlers.get(event) ?? [];
125
98
  list.push(handler);
126
- handlers.set(event, list);
99
+ extension.handlers.set(event, list);
127
100
  },
128
101
 
129
102
  registerTool(tool: ToolDefinition): void {
130
- tools.set(tool.name, {
103
+ extension.tools.set(tool.name, {
131
104
  definition: tool,
132
- extensionPath,
105
+ extensionPath: extension.path,
133
106
  });
134
107
  },
135
108
 
136
109
  registerCommand(name: string, options: { description?: string; handler: RegisteredCommand["handler"] }): void {
137
- commands.set(name, { name, ...options });
110
+ extension.commands.set(name, { name, ...options });
138
111
  },
139
112
 
140
113
  registerShortcut(
@@ -144,33 +117,38 @@ function createExtensionAPI(
144
117
  handler: (ctx: ExtensionContext) => Promise<void> | void;
145
118
  },
146
119
  ): void {
147
- shortcuts.set(shortcut, { shortcut, extensionPath, ...options });
120
+ extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
148
121
  },
149
122
 
150
123
  registerFlag(
151
124
  name: string,
152
125
  options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
153
126
  ): void {
154
- flags.set(name, { name, extensionPath, ...options });
127
+ extension.flags.set(name, { name, extensionPath: extension.path, ...options });
155
128
  if (options.default !== undefined) {
156
- flagValues.set(name, options.default);
129
+ runtime.flagValues.set(name, options.default);
157
130
  }
158
131
  },
159
132
 
160
- getFlag(name: string): boolean | string | undefined {
161
- return flagValues.get(name);
133
+ registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {
134
+ extension.messageRenderers.set(customType, renderer as MessageRenderer);
162
135
  },
163
136
 
164
- registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {
165
- messageRenderers.set(customType, renderer as MessageRenderer);
137
+ getFlag(name: string): boolean | string | undefined {
138
+ if (!extension.flags.has(name)) return undefined;
139
+ return runtime.flagValues.get(name);
166
140
  },
167
141
 
168
142
  sendMessage(message, options): void {
169
- sendMessageHandler(message, options);
143
+ runtime.sendMessage(message, options);
144
+ },
145
+
146
+ sendUserMessage(content, options): void {
147
+ runtime.sendUserMessage(content, options);
170
148
  },
171
149
 
172
150
  appendEntry(customType: string, data?: unknown): void {
173
- appendEntryHandler(customType, data);
151
+ runtime.appendEntry(customType, data);
174
152
  },
175
153
 
176
154
  exec(command: string, args: string[], options?: ExecOptions) {
@@ -178,45 +156,48 @@ function createExtensionAPI(
178
156
  },
179
157
 
180
158
  getActiveTools(): string[] {
181
- return getActiveToolsHandler();
159
+ return runtime.getActiveTools();
182
160
  },
183
161
 
184
162
  getAllTools(): string[] {
185
- return getAllToolsHandler();
163
+ return runtime.getAllTools();
186
164
  },
187
165
 
188
166
  setActiveTools(toolNames: string[]): void {
189
- setActiveToolsHandler(toolNames);
167
+ runtime.setActiveTools(toolNames);
168
+ },
169
+
170
+ setModel(model) {
171
+ return runtime.setModel(model);
172
+ },
173
+
174
+ getThinkingLevel() {
175
+ return runtime.getThinkingLevel();
176
+ },
177
+
178
+ setThinkingLevel(level) {
179
+ runtime.setThinkingLevel(level);
190
180
  },
191
181
 
192
182
  events: eventBus,
193
183
  } as ExtensionAPI;
194
184
 
185
+ return api;
186
+ }
187
+
188
+ /**
189
+ * Create an Extension object with empty collections.
190
+ */
191
+ function createExtension(extensionPath: string, resolvedPath: string): Extension {
195
192
  return {
196
- api,
197
- messageRenderers,
198
- commands,
199
- flags,
200
- flagValues,
201
- shortcuts,
202
- setSendMessageHandler: (handler: SendMessageHandler) => {
203
- sendMessageHandler = handler;
204
- },
205
- setAppendEntryHandler: (handler: AppendEntryHandler) => {
206
- appendEntryHandler = handler;
207
- },
208
- setGetActiveToolsHandler: (handler: GetActiveToolsHandler) => {
209
- getActiveToolsHandler = handler;
210
- },
211
- setGetAllToolsHandler: (handler: GetAllToolsHandler) => {
212
- getAllToolsHandler = handler;
213
- },
214
- setSetActiveToolsHandler: (handler: SetActiveToolsHandler) => {
215
- setActiveToolsHandler = handler;
216
- },
217
- setFlagValue: (name: string, value: boolean | string) => {
218
- flagValues.set(name, value);
219
- },
193
+ path: extensionPath,
194
+ resolvedPath,
195
+ handlers: new Map(),
196
+ tools: new Map(),
197
+ messageRenderers: new Map(),
198
+ commands: new Map(),
199
+ flags: new Map(),
200
+ shortcuts: new Map(),
220
201
  };
221
202
  }
222
203
 
@@ -224,8 +205,8 @@ async function loadExtension(
224
205
  extensionPath: string,
225
206
  cwd: string,
226
207
  eventBus: EventBus,
227
- sharedUI: { ui: ExtensionUIContext; hasUI: boolean },
228
- ): Promise<{ extension: LoadedExtension | null; error: string | null }> {
208
+ runtime: ExtensionRuntime,
209
+ ): Promise<{ extension: Extension | null; error: string | null }> {
229
210
  const resolvedPath = resolvePath(extensionPath, cwd);
230
211
 
231
212
  try {
@@ -233,48 +214,17 @@ async function loadExtension(
233
214
  const factory = (module.default ?? module) as ExtensionFactory;
234
215
 
235
216
  if (typeof factory !== "function") {
236
- return { extension: null, error: "Extension must export a default function" };
217
+ return {
218
+ extension: null,
219
+ error: `Extension does not export a valid factory function: ${extensionPath}`,
220
+ };
237
221
  }
238
222
 
239
- const handlers = new Map<string, HandlerFn[]>();
240
- const tools = new Map<string, RegisteredTool>();
241
- const {
242
- api,
243
- messageRenderers,
244
- commands,
245
- flags,
246
- flagValues,
247
- shortcuts,
248
- setSendMessageHandler,
249
- setAppendEntryHandler,
250
- setGetActiveToolsHandler,
251
- setGetAllToolsHandler,
252
- setSetActiveToolsHandler,
253
- setFlagValue,
254
- } = createExtensionAPI(handlers, tools, cwd, extensionPath, eventBus, sharedUI);
255
-
256
- factory(api);
257
-
258
- return {
259
- extension: {
260
- path: extensionPath,
261
- resolvedPath,
262
- handlers,
263
- tools,
264
- messageRenderers,
265
- commands,
266
- flags,
267
- flagValues,
268
- shortcuts,
269
- setSendMessageHandler,
270
- setAppendEntryHandler,
271
- setGetActiveToolsHandler,
272
- setGetAllToolsHandler,
273
- setSetActiveToolsHandler,
274
- setFlagValue,
275
- },
276
- error: null,
277
- };
223
+ const extension = createExtension(extensionPath, resolvedPath);
224
+ const api = createExtensionAPI(extension, runtime, cwd, eventBus);
225
+ await factory(api);
226
+
227
+ return { extension, error: null };
278
228
  } catch (err) {
279
229
  const message = err instanceof Error ? err.message : String(err);
280
230
  return { extension: null, error: `Failed to load extension: ${message}` };
@@ -282,64 +232,32 @@ async function loadExtension(
282
232
  }
283
233
 
284
234
  /**
285
- * Create a LoadedExtension from an inline factory function.
235
+ * Create an Extension from an inline factory function.
286
236
  */
287
- export function loadExtensionFromFactory(
237
+ export async function loadExtensionFromFactory(
288
238
  factory: ExtensionFactory,
289
239
  cwd: string,
290
240
  eventBus: EventBus,
291
- sharedUI: { ui: ExtensionUIContext; hasUI: boolean },
241
+ runtime: ExtensionRuntime,
292
242
  name = "<inline>",
293
- ): LoadedExtension {
294
- const handlers = new Map<string, HandlerFn[]>();
295
- const tools = new Map<string, RegisteredTool>();
296
- const {
297
- api,
298
- messageRenderers,
299
- commands,
300
- flags,
301
- flagValues,
302
- shortcuts,
303
- setSendMessageHandler,
304
- setAppendEntryHandler,
305
- setGetActiveToolsHandler,
306
- setGetAllToolsHandler,
307
- setSetActiveToolsHandler,
308
- setFlagValue,
309
- } = createExtensionAPI(handlers, tools, cwd, name, eventBus, sharedUI);
310
-
311
- factory(api);
312
-
313
- return {
314
- path: name,
315
- resolvedPath: name,
316
- handlers,
317
- tools,
318
- messageRenderers,
319
- commands,
320
- flags,
321
- flagValues,
322
- shortcuts,
323
- setSendMessageHandler,
324
- setAppendEntryHandler,
325
- setGetActiveToolsHandler,
326
- setGetAllToolsHandler,
327
- setSetActiveToolsHandler,
328
- setFlagValue,
329
- };
243
+ ): Promise<Extension> {
244
+ const extension = createExtension(name, name);
245
+ const api = createExtensionAPI(extension, runtime, cwd, eventBus);
246
+ await factory(api);
247
+ return extension;
330
248
  }
331
249
 
332
250
  /**
333
251
  * Load extensions from paths.
334
252
  */
335
253
  export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
336
- const extensions: LoadedExtension[] = [];
254
+ const extensions: Extension[] = [];
337
255
  const errors: Array<{ path: string; error: string }> = [];
338
256
  const resolvedEventBus = eventBus ?? createEventBus();
339
- const sharedUI = { ui: createNoOpUIContext(), hasUI: false };
257
+ const runtime = createExtensionRuntime();
340
258
 
341
259
  for (const extPath of paths) {
342
- const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, sharedUI);
260
+ const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);
343
261
 
344
262
  if (error) {
345
263
  errors.push({ path: extPath, error });
@@ -354,10 +272,7 @@ export async function loadExtensions(paths: string[], cwd: string, eventBus?: Ev
354
272
  return {
355
273
  extensions,
356
274
  errors,
357
- setUIContext(uiContext, hasUI) {
358
- sharedUI.ui = uiContext;
359
- sharedUI.hasUI = hasUI;
360
- },
275
+ runtime,
361
276
  };
362
277
  }
363
278
 
@@ -385,6 +300,39 @@ function isExtensionFile(name: string): boolean {
385
300
  return name.endsWith(".ts") || name.endsWith(".js");
386
301
  }
387
302
 
303
+ /**
304
+ * Resolve extension entry points from a directory.
305
+ */
306
+ function resolveExtensionEntries(dir: string): string[] | null {
307
+ const packageJsonPath = path.join(dir, "package.json");
308
+ if (existsSync(packageJsonPath)) {
309
+ const manifest = readExtensionManifest(packageJsonPath);
310
+ if (manifest?.extensions?.length) {
311
+ const entries: string[] = [];
312
+ for (const extPath of manifest.extensions) {
313
+ const resolvedExtPath = path.resolve(dir, extPath);
314
+ if (existsSync(resolvedExtPath)) {
315
+ entries.push(resolvedExtPath);
316
+ }
317
+ }
318
+ if (entries.length > 0) {
319
+ return entries;
320
+ }
321
+ }
322
+ }
323
+
324
+ const indexTs = path.join(dir, "index.ts");
325
+ const indexJs = path.join(dir, "index.js");
326
+ if (existsSync(indexTs)) {
327
+ return [indexTs];
328
+ }
329
+ if (existsSync(indexJs)) {
330
+ return [indexJs];
331
+ }
332
+
333
+ return null;
334
+ }
335
+
388
336
  /**
389
337
  * Discover extensions in a directory.
390
338
  *
@@ -416,29 +364,9 @@ function discoverExtensionsInDir(dir: string): string[] {
416
364
 
417
365
  // 2 & 3. Subdirectories
418
366
  if (entry.isDirectory() || entry.isSymbolicLink()) {
419
- // Check for package.json with "omp"/"pi" field first
420
- const packageJsonPath = path.join(entryPath, "package.json");
421
- if (existsSync(packageJsonPath)) {
422
- const manifest = readExtensionManifest(packageJsonPath);
423
- if (manifest?.extensions) {
424
- // Load paths declared in manifest (relative to package.json dir)
425
- for (const extPath of manifest.extensions) {
426
- const resolvedExtPath = path.resolve(entryPath, extPath);
427
- if (existsSync(resolvedExtPath)) {
428
- discovered.push(resolvedExtPath);
429
- }
430
- }
431
- continue;
432
- }
433
- }
434
-
435
- // Check for index.ts or index.js
436
- const indexTs = path.join(entryPath, "index.ts");
437
- const indexJs = path.join(entryPath, "index.js");
438
- if (existsSync(indexTs)) {
439
- discovered.push(indexTs);
440
- } else if (existsSync(indexJs)) {
441
- discovered.push(indexJs);
367
+ const entries = resolveExtensionEntries(entryPath);
368
+ if (entries) {
369
+ discovered.push(...entries);
442
370
  }
443
371
  }
444
372
  }
@@ -491,10 +419,20 @@ export async function discoverAndLoadExtensions(
491
419
  for (const configuredPath of configuredPaths) {
492
420
  const resolved = resolvePath(configuredPath, cwd);
493
421
  if (existsSync(resolved) && statSync(resolved).isDirectory()) {
494
- addPaths(discoverExtensionsInDir(resolved));
495
- } else {
496
- addPath(resolved);
422
+ const entries = resolveExtensionEntries(resolved);
423
+ if (entries) {
424
+ addPaths(entries);
425
+ continue;
426
+ }
427
+
428
+ const discovered = discoverExtensionsInDir(resolved);
429
+ if (discovered.length > 0) {
430
+ addPaths(discovered);
431
+ }
432
+ continue;
497
433
  }
434
+
435
+ addPath(resolved);
498
436
  }
499
437
 
500
438
  return loadExtensions(allPaths, cwd, eventBus);