@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -1,33 +1,85 @@
1
1
  if (!globalThis.__omp_js_prelude_loaded__) {
2
2
  globalThis.__omp_js_prelude_loaded__ = true;
3
3
 
4
+ const isNil = value => value === undefined || value === null;
4
5
  const isPlainObject = value => value !== null && typeof value === "object" && !Array.isArray(value);
5
- const optionsArg = (name, value, rest, example) => {
6
- if (rest.length > 0) {
7
- throw new TypeError(
8
- `${name}() takes options as a single trailing object literal, not positional arguments (got ${rest.length + 1} extra args). Pass them as ${name}(..., ${example}).`,
9
- );
6
+ const positionalOptions = (name, args, keys, example) => {
7
+ for (let index = keys.length; index < args.length; index++) {
8
+ if (!isNil(args[index])) {
9
+ throw new TypeError(
10
+ `${name}() accepts at most ${keys.length} positional optional args; got ${args.length}. Pass ${name}(..., ${example}) for named options.`,
11
+ );
12
+ }
13
+ }
14
+ const options = {};
15
+ for (let index = 0; index < keys.length && index < args.length; index++) {
16
+ const value = args[index];
17
+ if (!isNil(value)) options[keys[index]] = value;
18
+ }
19
+ return options;
20
+ };
21
+ const optionsArg = (name, value, rest, keys, example) => {
22
+ if (isNil(value)) return positionalOptions(name, [value, ...rest], keys, example);
23
+ if (isPlainObject(value)) {
24
+ if (rest.some(arg => !isNil(arg))) {
25
+ throw new TypeError(
26
+ `${name}() takes either a single trailing options object like ${example} or positional optional args; do not mix both forms.`,
27
+ );
28
+ }
29
+ return value;
10
30
  }
11
- if (value === undefined || value === null) return {};
12
- if (!isPlainObject(value)) {
13
- const kind = Array.isArray(value) ? "an array" : typeof value;
31
+ if (typeof value === "object") {
32
+ const kind = Array.isArray(value) ? "an array" : value.constructor?.name ?? "object";
14
33
  throw new TypeError(
15
- `${name}() options must be a trailing object literal like ${example}, not ${kind}. JS helpers never take positional options.`,
34
+ `${name}() options must be a plain object like ${example}, null/undefined, or positional optional args, not ${kind}.`,
16
35
  );
17
36
  }
18
- return value;
37
+ return positionalOptions(name, [value, ...rest], keys, example);
19
38
  };
20
39
  const callHelper = (name, ...args) => globalThis.__omp_helpers__[name](...args);
40
+ const hasScheme = path => typeof path === "string" && /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(path);
41
+ const shouldDelegateRead = path => hasScheme(path) && !path.toLowerCase().startsWith("local://");
42
+ const withReadLineSelector = (path, options) => {
43
+ const offset = typeof options.offset === "number" ? options.offset : 1;
44
+ const limit = typeof options.limit === "number" ? options.limit : undefined;
45
+ if (offset <= 1 && limit === undefined) return path;
46
+ if (limit !== undefined && limit <= 0) return null;
47
+ const start = Math.max(1, offset);
48
+ if (limit === undefined) return `${path}:${start}-`;
49
+ return `${path}:${start}-${start + limit - 1}`;
50
+ };
51
+ const readToolText = async path => {
52
+ const res = await globalThis.__omp_call_tool__("read", { path });
53
+ return res && typeof res === "object" && "text" in res ? res.text : res;
54
+ };
21
55
 
22
- const read = (path, opts, ...rest) => callHelper("read", path, optionsArg("read", opts, rest, "{ offset, limit }"));
56
+
57
+ const read = async (path, opts, ...rest) => {
58
+ const options = optionsArg("read", opts, rest, ["offset", "limit"], "{ offset, limit }");
59
+ if (shouldDelegateRead(path)) {
60
+ const toolPath = withReadLineSelector(path, options);
61
+ return toolPath === null ? "" : readToolText(toolPath);
62
+ }
63
+ return callHelper("read", path, options);
64
+ };
23
65
  const write = async (path, data) => callHelper("writeFile", path, data);
24
66
  const append = (path, content) => callHelper("append", path, content);
25
- const sort = (text, opts, ...rest) => callHelper("sortText", text, optionsArg("sort", opts, rest, "{ reverse, unique }"));
26
- const uniq = (text, opts, ...rest) => callHelper("uniqText", text, optionsArg("uniq", opts, rest, "{ count }"));
67
+ const sort = (text, opts, ...rest) =>
68
+ callHelper("sortText", text, optionsArg("sort", opts, rest, ["reverse", "unique"], "{ reverse, unique }"));
69
+ const uniq = (text, opts, ...rest) => callHelper("uniqText", text, optionsArg("uniq", opts, rest, ["count"], "{ count }"));
27
70
  const counter = (items, opts, ...rest) =>
28
- callHelper("counter", items, optionsArg("counter", opts, rest, "{ limit, reverse }"));
71
+ callHelper("counter", items, optionsArg("counter", opts, rest, ["limit", "reverse"], "{ limit, reverse }"));
29
72
  const diff = (a, b) => callHelper("diff", a, b);
30
- const tree = (path = ".", opts, ...rest) => callHelper("tree", path, optionsArg("tree", opts, rest, "{ maxDepth, showHidden }"));
73
+ const tree = (path = ".", opts, ...rest) => {
74
+ if (isPlainObject(path) && opts === undefined && rest.length === 0) {
75
+ return callHelper("tree", ".", path);
76
+ }
77
+ return callHelper(
78
+ "tree",
79
+ isNil(path) ? "." : path,
80
+ optionsArg("tree", opts, rest, ["maxDepth", "showHidden"], "{ maxDepth, showHidden }"),
81
+ );
82
+ };
31
83
  const env = (key, value) => callHelper("env", key, value);
32
84
 
33
85
  const tool = new Proxy(
@@ -58,14 +110,14 @@ if (!globalThis.__omp_js_prelude_loaded__) {
58
110
  const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
59
111
 
60
112
  const completion = async (prompt, opts, ...rest) => {
61
- const o = optionsArg("completion", opts, rest, "{ model, system, schema }");
113
+ const o = optionsArg("completion", opts, rest, ["model", "system", "schema"], "{ model, system, schema }");
62
114
  const res = await globalThis.__omp_call_tool__("__completion__", { prompt, ...o });
63
115
  const text = res && typeof res === "object" ? res.text : res;
64
116
  return hasOwn(o, "schema") ? JSON.parse(text) : text;
65
117
  };
66
118
 
67
119
  const agent = async (prompt, opts, ...rest) => {
68
- const o = optionsArg("agent", opts, rest, "{ agentType, model, label, schema }");
120
+ const o = optionsArg("agent", opts, rest, ["agentType", "model", "label", "schema"], "{ agentType, model, label, schema }");
69
121
  const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...o });
70
122
  const text = res && typeof res === "object" ? res.text : res;
71
123
  return hasOwn(o, "schema") ? JSON.parse(text) : text;
@@ -3,12 +3,9 @@ import * as path from "node:path";
3
3
  import type { AgentState } from "@oh-my-pi/pi-agent-core";
4
4
  import { APP_NAME, isEnoent } from "@oh-my-pi/pi-utils";
5
5
  import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme";
6
- import {
7
- loadEntriesFromFile,
8
- type SessionEntry,
9
- type SessionHeader,
10
- SessionManager,
11
- } from "../../session/session-manager";
6
+ import type { SessionEntry, SessionHeader } from "../../session/session-entries";
7
+ import { loadEntriesFromFile } from "../../session/session-loader";
8
+ import { SessionManager } from "../../session/session-manager";
12
9
  import templateCss from "./template.css" with { type: "text" };
13
10
  import templateHtml from "./template.html" with { type: "text" };
14
11
  import templateJs from "./template.js" with { type: "text" };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Model query facade exposed to extensions as `ctx.models`.
3
+ *
4
+ * Read-only: lets an extension select a model the same way core does — list
5
+ * authenticated models, read the session model, resolve a model string or role
6
+ * alias, and compare model families — without touching the mutable registry or
7
+ * duplicating resolution/family heuristics.
8
+ */
9
+ import type { Api, Model } from "@oh-my-pi/pi-ai";
10
+ import { modelFamilyToken } from "@oh-my-pi/pi-catalog/identity";
11
+ import type { ModelRegistry } from "../../config/model-registry";
12
+ import { getModelMatchPreferences, resolveModelRoleValue } from "../../config/model-resolver";
13
+ import type { Settings } from "../../config/settings";
14
+ import type { ExtensionModelQuery } from "./types";
15
+
16
+ /**
17
+ * Build the `ctx.models` facade. `getModel` is read lazily so `current()` always
18
+ * reflects the live session model (it can change mid-session via `/model`).
19
+ */
20
+ export function createExtensionModelQuery(
21
+ modelRegistry: ModelRegistry,
22
+ settings: Settings | undefined,
23
+ getModel: () => Model | undefined,
24
+ ): ExtensionModelQuery {
25
+ return {
26
+ list: () => modelRegistry.getAvailable(),
27
+ current: () => getModel(),
28
+ // resolveModelRoleValue expands a role alias (`pi/slow`) to its full configured
29
+ // priority list and tries each pattern — the same path core selection uses — so a
30
+ // fallback model lower in the list still resolves. Plain model strings pass through
31
+ // as a single pattern.
32
+ resolve: (spec: string): Model<Api> | undefined =>
33
+ resolveModelRoleValue(spec, modelRegistry.getAvailable(), {
34
+ settings,
35
+ matchPreferences: getModelMatchPreferences(settings),
36
+ modelRegistry,
37
+ }).model,
38
+ family: (model: Model<Api>): string =>
39
+ modelFamilyToken(modelRegistry.getCanonicalId(model) ?? model.id) || model.provider.toLowerCase(),
40
+ };
41
+ }
@@ -6,9 +6,11 @@ import type { CredentialDisabledEvent, ImageContent, Model, ProviderResponseMeta
6
6
  import type { KeyId } from "@oh-my-pi/pi-tui";
7
7
  import { logger } from "@oh-my-pi/pi-utils";
8
8
  import type { ModelRegistry } from "../../config/model-registry";
9
+ import type { Settings } from "../../config/settings";
9
10
  import type { MemoryRuntimeContext } from "../../memory-backend";
10
11
  import { type Theme, theme } from "../../modes/theme/theme";
11
12
  import type { SessionManager } from "../../session/session-manager";
13
+ import { createExtensionModelQuery } from "./model-api";
12
14
  import type {
13
15
  AfterProviderResponseEvent,
14
16
  AssistantThinkingRenderer,
@@ -207,6 +209,7 @@ export class ExtensionRunner {
207
209
  private readonly sessionManager: SessionManager,
208
210
  private readonly modelRegistry: ModelRegistry,
209
211
  getMemory?: () => MemoryRuntimeContext | undefined,
212
+ private readonly settings?: Settings,
210
213
  ) {
211
214
  this.#uiContext = noOpUIContext;
212
215
  this.#getMemoryFn = getMemory;
@@ -479,6 +482,7 @@ export class ExtensionRunner {
479
482
  get model() {
480
483
  return getModel();
481
484
  },
485
+ models: createExtensionModelQuery(this.modelRegistry, this.settings, getModel),
482
486
  isIdle: () => this.#isIdleFn(),
483
487
  abort: () => this.#abortFn(),
484
488
  hasPendingMessages: () => this.#hasPendingMessagesFn(),
@@ -56,6 +56,7 @@ import type {
56
56
  SearchToolInput,
57
57
  WriteToolInput,
58
58
  } from "../../tools";
59
+ import type { ApprovalMode } from "../../tools/approval";
59
60
  import type { EventBus } from "../../utils/event-bus";
60
61
  import type {
61
62
  AgentEndEvent,
@@ -293,6 +294,32 @@ export interface CompactOptions {
293
294
  // surface (model registry, system prompt, shutdown, full session manager
294
295
  // access). Field overlap is incidental; merging into a base would require
295
296
  // hooks to widen their public contract.
297
+ /**
298
+ * Read-only model query facade exposed at `ctx.models`. Lets an extension select a
299
+ * model the same way core does — list authenticated models, read the session model,
300
+ * resolve a model string or role alias, and compare model families — without reaching
301
+ * into the mutable registry or re-implementing matching/family heuristics.
302
+ */
303
+ export interface ExtensionModelQuery {
304
+ /** Authenticated models available this session (the same set `--model` selection sees). */
305
+ list(): Model[];
306
+ /** The current session model, if one is set. */
307
+ current(): Model | undefined;
308
+ /**
309
+ * Resolve a model string (`provider/id`, bare id) or role alias (`pi/slow`, a
310
+ * configured role) to a Model, using the same settings-backed aliases and match
311
+ * preferences as core selection. Thinking/routing suffixes are accepted and resolved
312
+ * to the base model (pass effort separately). Returns undefined when nothing matches.
313
+ */
314
+ resolve(spec: string): Model | undefined;
315
+ /**
316
+ * Opaque lineage token for "are these the same family?" comparisons — every Claude
317
+ * point release shares a token, Claude and GPT differ. Backed by catalog canonical
318
+ * identity. Compare it; do not persist it (the vocabulary tracks new releases).
319
+ */
320
+ family(model: Model): string;
321
+ }
322
+
296
323
  export interface ExtensionContext {
297
324
  /** UI methods for user interaction */
298
325
  ui: ExtensionUIContext;
@@ -310,6 +337,8 @@ export interface ExtensionContext {
310
337
  modelRegistry: ModelRegistry;
311
338
  /** Current model (may be undefined) */
312
339
  model: Model | undefined;
340
+ /** Read-only model query facade: list / current / resolve / family. */
341
+ models: ExtensionModelQuery;
313
342
  /** Whether the agent is idle (not streaming) */
314
343
  isIdle(): boolean;
315
344
  /** Abort the current agent operation */
@@ -608,6 +637,24 @@ export interface InputEvent {
608
637
  // Tool Events
609
638
  // ============================================================================
610
639
 
640
+ export interface ToolApprovalRequestedEvent {
641
+ type: "tool_approval_requested";
642
+ sessionId: string;
643
+ toolCallId: string;
644
+ toolName: string;
645
+ reason?: string;
646
+ approvalMode: ApprovalMode;
647
+ }
648
+
649
+ export interface ToolApprovalResolvedEvent {
650
+ type: "tool_approval_resolved";
651
+ sessionId: string;
652
+ toolCallId: string;
653
+ toolName: string;
654
+ approved: boolean;
655
+ reason?: string;
656
+ }
657
+
611
658
  interface ToolCallEventBase {
612
659
  type: "tool_call";
613
660
  toolCallId: string;
@@ -775,7 +822,9 @@ export type ExtensionEvent =
775
822
  | UserPythonEvent
776
823
  | InputEvent
777
824
  | ToolCallEvent
778
- | ToolResultEvent;
825
+ | ToolResultEvent
826
+ | ToolApprovalRequestedEvent
827
+ | ToolApprovalResolvedEvent;
779
828
 
780
829
  // ============================================================================
781
830
  // Event Results
@@ -946,6 +995,8 @@ export interface ExtensionAPI {
946
995
  on(event: "goal_updated", handler: ExtensionHandler<GoalUpdatedEvent>): void;
947
996
  on(event: "credential_disabled", handler: ExtensionHandler<CredentialDisabledEvent>): void;
948
997
  on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
998
+ on(event: "tool_approval_requested", handler: ExtensionHandler<ToolApprovalRequestedEvent>): void;
999
+ on(event: "tool_approval_resolved", handler: ExtensionHandler<ToolApprovalResolvedEvent>): void;
949
1000
  on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
950
1001
  on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
951
1002
  on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
@@ -121,8 +121,36 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
121
121
  const approvalCheck = requiresApproval(this.tool, params, approvalMode, userPolicies);
122
122
 
123
123
  if (approvalCheck.required) {
124
+ const hasApprovalHandlers =
125
+ this.runner.hasHandlers("tool_approval_requested") || this.runner.hasHandlers("tool_approval_resolved");
126
+ const sessionId = context?.sessionManager?.getSessionId() ?? "";
127
+ if (hasApprovalHandlers) {
128
+ await this.runner.emit({
129
+ type: "tool_approval_requested",
130
+ sessionId,
131
+ toolName: this.tool.name,
132
+ toolCallId,
133
+ ...(approvalCheck.reason ? { reason: approvalCheck.reason } : {}),
134
+ approvalMode,
135
+ });
136
+ }
137
+
138
+ const resolveApproval = async (approved: boolean, reason?: string) => {
139
+ if (!hasApprovalHandlers) return;
140
+ await this.runner.emit({
141
+ type: "tool_approval_resolved",
142
+ sessionId,
143
+ toolName: this.tool.name,
144
+ toolCallId,
145
+ approved,
146
+ ...(reason ? { reason } : {}),
147
+ });
148
+ };
149
+
124
150
  // Check if UI is available
125
151
  if (!this.runner.hasUI()) {
152
+ const reason = "no interactive UI available";
153
+ await resolveApproval(false, reason);
126
154
  throw new Error(
127
155
  `Tool "${this.tool.name}" requires approval but no interactive UI available.\n` +
128
156
  `Options:\n` +
@@ -133,11 +161,19 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
133
161
  }
134
162
 
135
163
  const uiContext = this.runner.getUIContext();
136
- const choice = await uiContext.select(formatApprovalPrompt(this.tool, params, approvalCheck.reason), [
137
- "Approve",
138
- "Deny",
139
- ]);
140
- if (choice !== "Approve") {
164
+ let choice: string | undefined;
165
+ try {
166
+ choice = await uiContext.select(formatApprovalPrompt(this.tool, params, approvalCheck.reason), [
167
+ "Approve",
168
+ "Deny",
169
+ ]);
170
+ } catch (err) {
171
+ await resolveApproval(false, err instanceof Error ? err.message : "approval aborted");
172
+ throw err;
173
+ }
174
+ const approved = choice === "Approve";
175
+ await resolveApproval(approved, approved ? undefined : "denied by user");
176
+ if (!approved) {
141
177
  throw new Error(`Tool call denied by user: ${this.tool.name}`);
142
178
  }
143
179
  }
@@ -1,4 +1,5 @@
1
- export type { ReadonlySessionManager, UsageStatistics } from "../../session/session-manager";
1
+ export type { UsageStatistics } from "../../session/session-entries";
2
+ export type { ReadonlySessionManager } from "../../session/session-manager";
2
3
  export * from "./loader";
3
4
  export * from "./runner";
4
5
  export * from "./tool-wrapper";
@@ -30,16 +30,28 @@ const PI_PACKAGE_ALTERNATION = PI_PACKAGE_NAMES.join("|");
30
30
  // root that we relocated under a different folder. Each entry rewrites
31
31
  // `<pkg>/<from>` → `<pkg>/<to>` after the scope has been canonicalised, so
32
32
  // plugins importing the upstream layout still resolve to a real file in our
33
- // bundled copy. Add new entries as `pkg/from -> pkg/to` whenever a plugin
34
- // surfaces another upstream-only subpath that breaks resolution.
33
+ // bundled copy. Entries ending in `/` rewrite the whole subtree; add new
34
+ // `pkg/from -> pkg/to` pairs whenever an upstream-only subpath breaks resolution.
35
35
  const PI_SUBPATH_REMAPS: ReadonlyMap<string, string> = new Map<string, string>([
36
- // (currently empty) Upstream `@mariozechner/pi-ai/oauth` re-exported
37
- // `./utils/oauth/index.js`. Our pi-ai now exposes the same surface at the
38
- // real `@oh-my-pi/pi-ai/oauth` export, so the legacy subpath canonicalizes
39
- // straight to it with no rewrite. Add `from -> to` entries here whenever a
40
- // future upstream-only subpath surfaces that breaks resolution.
36
+ ["pi-ai/utils/oauth", "pi-ai/oauth"],
37
+ ["pi-ai/utils/oauth/", "pi-ai/oauth/"],
41
38
  ]);
42
39
 
40
+ function remapLegacyPiSubpath(rest: string): string {
41
+ const exact = PI_SUBPATH_REMAPS.get(rest);
42
+ if (exact) {
43
+ return exact;
44
+ }
45
+
46
+ for (const [from, to] of PI_SUBPATH_REMAPS) {
47
+ if (from.endsWith("/") && rest.startsWith(from)) {
48
+ return `${to}${rest.slice(from.length)}`;
49
+ }
50
+ }
51
+
52
+ return rest;
53
+ }
54
+
43
55
  const LEGACY_PI_SPECIFIER_FILTER = new RegExp(`^@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/.*)?$`);
44
56
  const LEGACY_PI_IMPORT_SPECIFIER_REGEX = new RegExp(
45
57
  `((?:from\\s+|import\\s+|import\\s*\\(\\s*)["'])(@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/[^"'()\\s]+)?)(["'])`,
@@ -101,6 +113,28 @@ export function __computeBunfsPackageRoot(metaDir: string, pathImpl: typeof path
101
113
  return pathImpl.join(metaDir, "packages");
102
114
  }
103
115
 
116
+ /**
117
+ * Compute the package root for the npm prebuilt `dist/cli.js` bundle.
118
+ *
119
+ * `bundle-dist.ts` defines `process.env.PI_BUNDLED="true"`; after bundling,
120
+ * `import.meta.dir` points at `<package>/dist`. Do not resolve the package via
121
+ * bare `@oh-my-pi/pi-coding-agent` here: from a global install Bun can pick an
122
+ * older cache entry, recreating mixed-runtime plugin loading.
123
+ */
124
+ export function __computeBundledSelfPackageRoot(metaDir: string, pathImpl: typeof path = path): string {
125
+ const normalizedMetaDir = pathImpl.normalize(metaDir);
126
+ if (pathImpl.basename(normalizedMetaDir) === "dist") {
127
+ return pathImpl.resolve(metaDir, "..");
128
+ }
129
+
130
+ const pluginsDirSuffix = pathImpl.join("src", "extensibility", "plugins");
131
+ if (normalizedMetaDir.endsWith(pluginsDirSuffix)) {
132
+ return pathImpl.resolve(metaDir, "..", "..", "..");
133
+ }
134
+
135
+ return pathImpl.resolve(metaDir);
136
+ }
137
+
104
138
  const BUNFS_PACKAGE_ROOT = IS_COMPILED_BINARY ? __computeBunfsPackageRoot(import.meta.dir) : null;
105
139
 
106
140
  function bunfsPath(...segments: string[]): string {
@@ -112,11 +146,7 @@ function bunfsPath(...segments: string[]): string {
112
146
 
113
147
  function resolveBundledSelfPackageRoot(): string | undefined {
114
148
  if (!process.env.PI_BUNDLED) return undefined;
115
- try {
116
- return path.dirname(Bun.resolveSync("@oh-my-pi/pi-coding-agent/package.json", import.meta.dir));
117
- } catch {
118
- return undefined;
119
- }
149
+ return __computeBundledSelfPackageRoot(import.meta.dir);
120
150
  }
121
151
 
122
152
  const BUNDLED_SELF_PACKAGE_ROOT = resolveBundledSelfPackageRoot();
@@ -208,7 +238,7 @@ function remapLegacyPiSpecifier(specifier: string): string | null {
208
238
  return null;
209
239
  }
210
240
  const rest = specifier.slice(slashIdx + 1);
211
- const remappedSubpath = PI_SUBPATH_REMAPS.get(rest) ?? rest;
241
+ const remappedSubpath = remapLegacyPiSubpath(rest);
212
242
  return `${CANONICAL_PI_SCOPE}/${remappedSubpath}`;
213
243
  }
214
244
 
@@ -172,34 +172,49 @@ function resolveManifestEntryFile(joined: string): string | null {
172
172
  * Handles both single-string and string[] base entries, plus feature-specific entries.
173
173
  */
174
174
  function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "commands" | "extensions"): string[] {
175
- const paths: string[] = [];
175
+ const resolved: string[] = [];
176
+ for (const entry of resolvePluginManifestEntries(plugin, key)) {
177
+ if (entry.resolvedPath) {
178
+ resolved.push(entry.resolvedPath);
179
+ }
180
+ }
181
+ return resolved;
182
+ }
183
+
184
+ /**
185
+ * Declared manifest entries paired with their resolved file path. Returns one
186
+ * record per declared entry — base entries first, then enabled-feature entries
187
+ * — so callers (e.g. install-time validation) can detect manifest entries that
188
+ * point at missing files instead of silently skipping them like
189
+ * {@link resolvePluginPaths} does.
190
+ */
191
+ export function resolvePluginManifestEntries(
192
+ plugin: InstalledPlugin,
193
+ key: "tools" | "hooks" | "commands" | "extensions",
194
+ ): Array<{ entry: string; resolvedPath: string | null }> {
195
+ const declared: Array<{ entry: string; resolvedPath: string | null }> = [];
176
196
  const manifest = plugin.manifest;
177
197
 
178
- // Base entry (always included if exists)
198
+ const resolveEntry = (entry: string): { entry: string; resolvedPath: string | null } => ({
199
+ entry,
200
+ resolvedPath: resolveManifestEntryFile(path.join(plugin.path, entry)),
201
+ });
202
+
179
203
  const base = manifest[key];
180
204
  if (base) {
181
205
  const entries = Array.isArray(base) ? base : [base];
182
206
  for (const entry of entries) {
183
- const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
184
- if (resolved) {
185
- paths.push(resolved);
186
- }
207
+ declared.push(resolveEntry(entry));
187
208
  }
188
209
  }
189
210
 
190
- // Feature-specific entries
191
211
  if (manifest.features && plugin.enabledFeatures) {
192
212
  const enabledSet = new Set(plugin.enabledFeatures);
193
-
194
213
  for (const [featName, feat] of Object.entries(manifest.features)) {
195
214
  if (!enabledSet.has(featName)) continue;
196
-
197
215
  if (feat[key]) {
198
216
  for (const entry of feat[key]) {
199
- const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
200
- if (resolved) {
201
- paths.push(resolved);
202
- }
217
+ declared.push(resolveEntry(entry));
203
218
  }
204
219
  }
205
220
  }
@@ -207,19 +222,15 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
207
222
  // null means use defaults - enable features with default: true
208
223
  for (const [_featName, feat] of Object.entries(manifest.features)) {
209
224
  if (!feat.default) continue;
210
-
211
225
  if (feat[key]) {
212
226
  for (const entry of feat[key]) {
213
- const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
214
- if (resolved) {
215
- paths.push(resolved);
216
- }
227
+ declared.push(resolveEntry(entry));
217
228
  }
218
229
  }
219
230
  }
220
231
  }
221
232
 
222
- return paths;
233
+ return declared;
223
234
  }
224
235
 
225
236
  export function resolvePluginToolPaths(plugin: InstalledPlugin): string[] {