@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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 (193) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/gemini-image.ts +361 -0
  62. package/src/core/tools/git.ts +216 -0
  63. package/src/core/tools/index.ts +28 -15
  64. package/src/core/tools/lsp/config.ts +5 -4
  65. package/src/core/tools/lsp/index.ts +17 -12
  66. package/src/core/tools/lsp/render.ts +39 -47
  67. package/src/core/tools/read.ts +66 -29
  68. package/src/core/tools/render-utils.ts +268 -0
  69. package/src/core/tools/renderers.ts +243 -225
  70. package/src/core/tools/task/discovery.ts +2 -2
  71. package/src/core/tools/task/executor.ts +66 -58
  72. package/src/core/tools/task/index.ts +29 -10
  73. package/src/core/tools/task/model-resolver.ts +8 -13
  74. package/src/core/tools/task/omp-command.ts +24 -0
  75. package/src/core/tools/task/render.ts +35 -60
  76. package/src/core/tools/task/types.ts +3 -0
  77. package/src/core/tools/web-fetch.ts +29 -28
  78. package/src/core/tools/web-search/index.ts +6 -5
  79. package/src/core/tools/web-search/providers/exa.ts +6 -5
  80. package/src/core/tools/web-search/render.ts +66 -111
  81. package/src/core/voice-controller.ts +135 -0
  82. package/src/core/voice-supervisor.ts +1003 -0
  83. package/src/core/voice.ts +308 -0
  84. package/src/discovery/builtin.ts +75 -1
  85. package/src/discovery/claude.ts +47 -1
  86. package/src/discovery/codex.ts +54 -2
  87. package/src/discovery/gemini.ts +55 -2
  88. package/src/discovery/helpers.ts +100 -1
  89. package/src/discovery/index.ts +2 -0
  90. package/src/index.ts +14 -9
  91. package/src/lib/worktree/collapse.ts +179 -0
  92. package/src/lib/worktree/constants.ts +14 -0
  93. package/src/lib/worktree/errors.ts +23 -0
  94. package/src/lib/worktree/git.ts +110 -0
  95. package/src/lib/worktree/index.ts +23 -0
  96. package/src/lib/worktree/operations.ts +216 -0
  97. package/src/lib/worktree/session.ts +114 -0
  98. package/src/lib/worktree/stats.ts +67 -0
  99. package/src/main.ts +61 -37
  100. package/src/migrations.ts +37 -7
  101. package/src/modes/interactive/components/bash-execution.ts +6 -4
  102. package/src/modes/interactive/components/custom-editor.ts +55 -0
  103. package/src/modes/interactive/components/custom-message.ts +95 -0
  104. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  105. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  106. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  107. package/src/modes/interactive/components/extensions/types.ts +1 -0
  108. package/src/modes/interactive/components/footer.ts +324 -0
  109. package/src/modes/interactive/components/hook-editor.ts +1 -0
  110. package/src/modes/interactive/components/hook-selector.ts +3 -3
  111. package/src/modes/interactive/components/model-selector.ts +7 -6
  112. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  113. package/src/modes/interactive/components/settings-defs.ts +55 -6
  114. package/src/modes/interactive/components/status-line/separators.ts +4 -4
  115. package/src/modes/interactive/components/status-line.ts +45 -35
  116. package/src/modes/interactive/components/tool-execution.ts +95 -23
  117. package/src/modes/interactive/interactive-mode.ts +644 -113
  118. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  119. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  120. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  121. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  122. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  123. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  124. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  125. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  126. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  127. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  128. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  129. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  130. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  131. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  132. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  133. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  134. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  135. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  136. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  137. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  138. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  139. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  140. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  141. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  142. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  143. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  144. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  145. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  146. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  147. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  148. package/src/modes/interactive/theme/defaults/index.ts +128 -0
  149. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  150. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  151. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  152. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  154. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  155. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  156. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  157. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  158. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  159. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  160. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  161. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  162. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  163. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  166. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  167. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  168. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  169. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  170. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  171. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  172. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  173. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  174. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  175. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  176. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  177. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  178. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  179. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  180. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  181. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  182. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  183. package/src/modes/print-mode.ts +14 -72
  184. package/src/modes/rpc/rpc-client.ts +23 -9
  185. package/src/modes/rpc/rpc-mode.ts +137 -125
  186. package/src/modes/rpc/rpc-types.ts +46 -24
  187. package/src/prompts/task.md +1 -0
  188. package/src/prompts/tools/gemini-image.md +4 -0
  189. package/src/prompts/tools/git.md +9 -0
  190. package/src/prompts/voice-summary.md +12 -0
  191. package/src/utils/image-convert.ts +26 -0
  192. package/src/utils/image-resize.ts +215 -0
  193. package/src/utils/shell-snapshot.ts +22 -20
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Extension loader - loads TypeScript extension modules using native Bun import.
3
+ */
4
+
5
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import * as path from "node:path";
8
+ import type { KeyId } from "@oh-my-pi/pi-tui";
9
+ import * as TypeBox from "@sinclair/typebox";
10
+ import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
11
+ import { loadSync } from "../../discovery";
12
+ import { getExtensionNameFromPath } from "../../discovery/helpers";
13
+ import * as piCodingAgent from "../../index";
14
+ import { theme } from "../../modes/interactive/theme/theme";
15
+ import { createEventBus, type EventBus } from "../event-bus";
16
+ import type { ExecOptions } from "../exec";
17
+ import { execCommand } from "../exec";
18
+ import { logger } from "../logger";
19
+ import type {
20
+ AppendEntryHandler,
21
+ ExtensionAPI,
22
+ ExtensionContext,
23
+ ExtensionFactory,
24
+ ExtensionFlag,
25
+ ExtensionShortcut,
26
+ ExtensionUIContext,
27
+ GetActiveToolsHandler,
28
+ GetAllToolsHandler,
29
+ LoadExtensionsResult,
30
+ LoadedExtension,
31
+ MessageRenderer,
32
+ RegisteredCommand,
33
+ RegisteredTool,
34
+ SendMessageHandler,
35
+ SetActiveToolsHandler,
36
+ ToolDefinition,
37
+ } from "./types";
38
+
39
+ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
40
+
41
+ function normalizeUnicodeSpaces(str: string): string {
42
+ return str.replace(UNICODE_SPACES, " ");
43
+ }
44
+
45
+ function expandPath(p: string): string {
46
+ const normalized = normalizeUnicodeSpaces(p);
47
+ if (normalized.startsWith("~/")) {
48
+ return path.join(homedir(), normalized.slice(2));
49
+ }
50
+ if (normalized.startsWith("~")) {
51
+ return path.join(homedir(), normalized.slice(1));
52
+ }
53
+ return normalized;
54
+ }
55
+
56
+ function resolvePath(extPath: string, cwd: string): string {
57
+ const expanded = expandPath(extPath);
58
+ if (path.isAbsolute(expanded)) {
59
+ return expanded;
60
+ }
61
+ return path.resolve(cwd, expanded);
62
+ }
63
+
64
+ function createNoOpUIContext(): ExtensionUIContext {
65
+ 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
+ },
80
+ };
81
+ }
82
+
83
+ type HandlerFn = (...args: unknown[]) => Promise<unknown>;
84
+
85
+ function createExtensionAPI(
86
+ handlers: Map<string, HandlerFn[]>,
87
+ tools: Map<string, RegisteredTool>,
88
+ cwd: string,
89
+ extensionPath: string,
90
+ 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
+
118
+ const api = {
119
+ logger,
120
+ typebox: TypeBox,
121
+ pi: piCodingAgent,
122
+
123
+ on(event: string, handler: HandlerFn): void {
124
+ const list = handlers.get(event) ?? [];
125
+ list.push(handler);
126
+ handlers.set(event, list);
127
+ },
128
+
129
+ registerTool(tool: ToolDefinition): void {
130
+ tools.set(tool.name, {
131
+ definition: tool,
132
+ extensionPath,
133
+ });
134
+ },
135
+
136
+ registerCommand(name: string, options: { description?: string; handler: RegisteredCommand["handler"] }): void {
137
+ commands.set(name, { name, ...options });
138
+ },
139
+
140
+ registerShortcut(
141
+ shortcut: KeyId,
142
+ options: {
143
+ description?: string;
144
+ handler: (ctx: ExtensionContext) => Promise<void> | void;
145
+ },
146
+ ): void {
147
+ shortcuts.set(shortcut, { shortcut, extensionPath, ...options });
148
+ },
149
+
150
+ registerFlag(
151
+ name: string,
152
+ options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
153
+ ): void {
154
+ flags.set(name, { name, extensionPath, ...options });
155
+ if (options.default !== undefined) {
156
+ flagValues.set(name, options.default);
157
+ }
158
+ },
159
+
160
+ getFlag(name: string): boolean | string | undefined {
161
+ return flagValues.get(name);
162
+ },
163
+
164
+ registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {
165
+ messageRenderers.set(customType, renderer as MessageRenderer);
166
+ },
167
+
168
+ sendMessage(message, options): void {
169
+ sendMessageHandler(message, options);
170
+ },
171
+
172
+ appendEntry(customType: string, data?: unknown): void {
173
+ appendEntryHandler(customType, data);
174
+ },
175
+
176
+ exec(command: string, args: string[], options?: ExecOptions) {
177
+ return execCommand(command, args, options?.cwd ?? cwd, options);
178
+ },
179
+
180
+ getActiveTools(): string[] {
181
+ return getActiveToolsHandler();
182
+ },
183
+
184
+ getAllTools(): string[] {
185
+ return getAllToolsHandler();
186
+ },
187
+
188
+ setActiveTools(toolNames: string[]): void {
189
+ setActiveToolsHandler(toolNames);
190
+ },
191
+
192
+ events: eventBus,
193
+ } as ExtensionAPI;
194
+
195
+ 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
+ },
220
+ };
221
+ }
222
+
223
+ async function loadExtension(
224
+ extensionPath: string,
225
+ cwd: string,
226
+ eventBus: EventBus,
227
+ sharedUI: { ui: ExtensionUIContext; hasUI: boolean },
228
+ ): Promise<{ extension: LoadedExtension | null; error: string | null }> {
229
+ const resolvedPath = resolvePath(extensionPath, cwd);
230
+
231
+ try {
232
+ const module = await import(resolvedPath);
233
+ const factory = (module.default ?? module) as ExtensionFactory;
234
+
235
+ if (typeof factory !== "function") {
236
+ return { extension: null, error: "Extension must export a default function" };
237
+ }
238
+
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
+ };
278
+ } catch (err) {
279
+ const message = err instanceof Error ? err.message : String(err);
280
+ return { extension: null, error: `Failed to load extension: ${message}` };
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Create a LoadedExtension from an inline factory function.
286
+ */
287
+ export function loadExtensionFromFactory(
288
+ factory: ExtensionFactory,
289
+ cwd: string,
290
+ eventBus: EventBus,
291
+ sharedUI: { ui: ExtensionUIContext; hasUI: boolean },
292
+ 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
+ };
330
+ }
331
+
332
+ /**
333
+ * Load extensions from paths.
334
+ */
335
+ export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
336
+ const extensions: LoadedExtension[] = [];
337
+ const errors: Array<{ path: string; error: string }> = [];
338
+ const resolvedEventBus = eventBus ?? createEventBus();
339
+ const sharedUI = { ui: createNoOpUIContext(), hasUI: false };
340
+
341
+ for (const extPath of paths) {
342
+ const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, sharedUI);
343
+
344
+ if (error) {
345
+ errors.push({ path: extPath, error });
346
+ continue;
347
+ }
348
+
349
+ if (extension) {
350
+ extensions.push(extension);
351
+ }
352
+ }
353
+
354
+ return {
355
+ extensions,
356
+ errors,
357
+ setUIContext(uiContext, hasUI) {
358
+ sharedUI.ui = uiContext;
359
+ sharedUI.hasUI = hasUI;
360
+ },
361
+ };
362
+ }
363
+
364
+ interface ExtensionManifest {
365
+ extensions?: string[];
366
+ themes?: string[];
367
+ skills?: string[];
368
+ }
369
+
370
+ function readExtensionManifest(packageJsonPath: string): ExtensionManifest | null {
371
+ try {
372
+ const content = readFileSync(packageJsonPath, "utf-8");
373
+ const pkg = JSON.parse(content) as { omp?: ExtensionManifest; pi?: ExtensionManifest };
374
+ const manifest = pkg.omp ?? pkg.pi;
375
+ if (manifest && typeof manifest === "object") {
376
+ return manifest;
377
+ }
378
+ return null;
379
+ } catch {
380
+ return null;
381
+ }
382
+ }
383
+
384
+ function isExtensionFile(name: string): boolean {
385
+ return name.endsWith(".ts") || name.endsWith(".js");
386
+ }
387
+
388
+ /**
389
+ * Discover extensions in a directory.
390
+ *
391
+ * Discovery rules:
392
+ * 1. Direct files: `extensions/*.ts` or `*.js` → load
393
+ * 2. Subdirectory with index: `extensions/<ext>/index.ts` or `index.js` → load
394
+ * 3. Subdirectory with package.json: `extensions/<ext>/package.json` with "omp"/"pi" field → load declared paths
395
+ *
396
+ * No recursion beyond one level. Complex packages must use package.json manifest.
397
+ */
398
+ function discoverExtensionsInDir(dir: string): string[] {
399
+ if (!existsSync(dir)) {
400
+ return [];
401
+ }
402
+
403
+ const discovered: string[] = [];
404
+
405
+ try {
406
+ const entries = readdirSync(dir, { withFileTypes: true });
407
+
408
+ for (const entry of entries) {
409
+ const entryPath = path.join(dir, entry.name);
410
+
411
+ // 1. Direct files: *.ts or *.js
412
+ if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
413
+ discovered.push(entryPath);
414
+ continue;
415
+ }
416
+
417
+ // 2 & 3. Subdirectories
418
+ 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);
442
+ }
443
+ }
444
+ }
445
+ } catch {
446
+ return [];
447
+ }
448
+
449
+ return discovered;
450
+ }
451
+
452
+ /**
453
+ * Discover and load extensions from standard locations.
454
+ */
455
+ export async function discoverAndLoadExtensions(
456
+ configuredPaths: string[],
457
+ cwd: string,
458
+ eventBus?: EventBus,
459
+ disabledExtensionIds: string[] = [],
460
+ ): Promise<LoadExtensionsResult> {
461
+ const allPaths: string[] = [];
462
+ const seen = new Set<string>();
463
+ const disabled = new Set(disabledExtensionIds);
464
+
465
+ const isDisabledName = (name: string): boolean => disabled.has(`extension-module:${name}`);
466
+
467
+ const addPath = (extPath: string): void => {
468
+ const resolved = path.resolve(extPath);
469
+ if (!seen.has(resolved)) {
470
+ seen.add(resolved);
471
+ allPaths.push(extPath);
472
+ }
473
+ };
474
+
475
+ const addPaths = (paths: string[]) => {
476
+ for (const extPath of paths) {
477
+ if (isDisabledName(getExtensionNameFromPath(extPath))) continue;
478
+ addPath(extPath);
479
+ }
480
+ };
481
+
482
+ // 1. Discover extension modules via capability API (native .omp/.pi only)
483
+ const discovered = loadSync<ExtensionModule>(extensionModuleCapability.id, { cwd });
484
+ for (const ext of discovered.items) {
485
+ if (ext._source.provider !== "native") continue;
486
+ if (isDisabledName(ext.name)) continue;
487
+ addPath(ext.path);
488
+ }
489
+
490
+ // 2. Explicitly configured paths
491
+ for (const configuredPath of configuredPaths) {
492
+ const resolved = resolvePath(configuredPath, cwd);
493
+ if (existsSync(resolved) && statSync(resolved).isDirectory()) {
494
+ addPaths(discoverExtensionsInDir(resolved));
495
+ } else {
496
+ addPath(resolved);
497
+ }
498
+ }
499
+
500
+ return loadExtensions(allPaths, cwd, eventBus);
501
+ }