@ashx-j/lunr 0.1.6 → 0.2.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 (149) hide show
  1. package/dist/builtin-extensions/index.d.ts.map +1 -1
  2. package/dist/builtin-extensions/index.js +3 -6
  3. package/dist/builtin-extensions/index.js.map +1 -1
  4. package/dist/builtin-extensions/lunr-todos.d.ts +7 -6
  5. package/dist/builtin-extensions/lunr-todos.d.ts.map +1 -1
  6. package/dist/builtin-extensions/lunr-todos.js +24 -11
  7. package/dist/builtin-extensions/lunr-todos.js.map +1 -1
  8. package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.d.ts.map +1 -1
  9. package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.js +9 -5
  10. package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.js.map +1 -1
  11. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.d.ts.map +1 -1
  12. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js +31 -3
  13. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js.map +1 -1
  14. package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.d.ts +1 -0
  15. package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.d.ts.map +1 -1
  16. package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.js +14 -4
  17. package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.js.map +1 -1
  18. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
  19. package/dist/builtin-extensions/pi-subagents/src/extension/index.js +4 -3
  20. package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
  21. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
  22. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +12 -1
  23. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
  24. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +3 -0
  25. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
  26. package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
  27. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +8 -0
  28. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
  29. package/dist/builtin-extensions/pi-subagents/src/tui/render.js +22 -2
  30. package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
  31. package/dist/builtin-extensions/pi-web-access/index.d.ts.map +1 -1
  32. package/dist/builtin-extensions/pi-web-access/index.js +38 -230
  33. package/dist/builtin-extensions/pi-web-access/index.js.map +1 -1
  34. package/dist/builtin-extensions/pi-web-access/render-search-chrome.d.ts +35 -0
  35. package/dist/builtin-extensions/pi-web-access/render-search-chrome.d.ts.map +1 -0
  36. package/dist/builtin-extensions/pi-web-access/render-search-chrome.js +93 -0
  37. package/dist/builtin-extensions/pi-web-access/render-search-chrome.js.map +1 -0
  38. package/dist/bun/cli.d.ts.map +1 -1
  39. package/dist/bun/cli.js +1 -0
  40. package/dist/bun/cli.js.map +1 -1
  41. package/dist/catalog/models.json +1 -1
  42. package/dist/catalog/providers/cerebras.json +1 -1
  43. package/dist/catalog/providers/cloudflare-ai-gateway.json +1 -1
  44. package/dist/catalog/providers/cloudflare-workers-ai.json +1 -1
  45. package/dist/catalog/providers/huggingface.json +1 -1
  46. package/dist/catalog/providers/opencode-go.json +1 -1
  47. package/dist/catalog/providers/openrouter.json +1 -1
  48. package/dist/catalog/providers/vercel-ai-gateway.json +1 -1
  49. package/dist/catalog/publication.json +3 -3
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +4 -1
  52. package/dist/cli.js.map +1 -1
  53. package/dist/core/context-breakdown.d.ts +21 -1
  54. package/dist/core/context-breakdown.d.ts.map +1 -1
  55. package/dist/core/context-breakdown.js +58 -2
  56. package/dist/core/context-breakdown.js.map +1 -1
  57. package/dist/core/extensions/host-static.d.ts +2 -0
  58. package/dist/core/extensions/host-static.d.ts.map +1 -0
  59. package/dist/core/extensions/host-static.js +15 -0
  60. package/dist/core/extensions/host-static.js.map +1 -0
  61. package/dist/core/extensions/loader.d.ts +10 -0
  62. package/dist/core/extensions/loader.d.ts.map +1 -1
  63. package/dist/core/extensions/loader.js +75 -34
  64. package/dist/core/extensions/loader.js.map +1 -1
  65. package/dist/core/official-catalog.d.ts.map +1 -1
  66. package/dist/core/official-catalog.js +11 -4
  67. package/dist/core/official-catalog.js.map +1 -1
  68. package/dist/core/package-manager.d.ts +3 -0
  69. package/dist/core/package-manager.d.ts.map +1 -1
  70. package/dist/core/package-manager.js +11 -5
  71. package/dist/core/package-manager.js.map +1 -1
  72. package/dist/core/permission-mode-control.d.ts +27 -0
  73. package/dist/core/permission-mode-control.d.ts.map +1 -0
  74. package/dist/core/permission-mode-control.js +39 -0
  75. package/dist/core/permission-mode-control.js.map +1 -0
  76. package/dist/core/resource-loader.d.ts +4 -1
  77. package/dist/core/resource-loader.d.ts.map +1 -1
  78. package/dist/core/resource-loader.js +13 -5
  79. package/dist/core/resource-loader.js.map +1 -1
  80. package/dist/core/settings-manager.d.ts.map +1 -1
  81. package/dist/core/settings-manager.js.map +1 -1
  82. package/dist/core/slash-commands.d.ts.map +1 -1
  83. package/dist/core/slash-commands.js +0 -1
  84. package/dist/core/slash-commands.js.map +1 -1
  85. package/dist/core/timings.d.ts +1 -0
  86. package/dist/core/timings.d.ts.map +1 -1
  87. package/dist/core/timings.js +6 -1
  88. package/dist/core/timings.js.map +1 -1
  89. package/dist/core/tools/read.d.ts.map +1 -1
  90. package/dist/core/tools/read.js +14 -20
  91. package/dist/core/tools/read.js.map +1 -1
  92. package/dist/core/tools/render-utils.d.ts +2 -0
  93. package/dist/core/tools/render-utils.d.ts.map +1 -1
  94. package/dist/core/tools/render-utils.js +10 -0
  95. package/dist/core/tools/render-utils.js.map +1 -1
  96. package/dist/main.d.ts +8 -0
  97. package/dist/main.d.ts.map +1 -1
  98. package/dist/main.js +63 -16
  99. package/dist/main.js.map +1 -1
  100. package/dist/migrations.d.ts.map +1 -1
  101. package/dist/migrations.js +3 -0
  102. package/dist/migrations.js.map +1 -1
  103. package/dist/modes/interactive/components/assistant-message.d.ts +1 -0
  104. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/assistant-message.js +1 -0
  106. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  107. package/dist/modes/interactive/components/context-view.d.ts.map +1 -1
  108. package/dist/modes/interactive/components/context-view.js +3 -1
  109. package/dist/modes/interactive/components/context-view.js.map +1 -1
  110. package/dist/modes/interactive/components/plan-message.d.ts +17 -0
  111. package/dist/modes/interactive/components/plan-message.d.ts.map +1 -0
  112. package/dist/modes/interactive/components/plan-message.js +43 -0
  113. package/dist/modes/interactive/components/plan-message.js.map +1 -0
  114. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/settings-selector.js +1 -1
  116. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  117. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  118. package/dist/modes/interactive/components/tool-execution.js +3 -1
  119. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  120. package/dist/modes/interactive/components/usage-view.d.ts +4 -25
  121. package/dist/modes/interactive/components/usage-view.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/usage-view.js +17 -69
  123. package/dist/modes/interactive/components/usage-view.js.map +1 -1
  124. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  125. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/user-message.js +1 -0
  127. package/dist/modes/interactive/components/user-message.js.map +1 -1
  128. package/dist/modes/interactive/interactive-mode.d.ts +23 -6
  129. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  130. package/dist/modes/interactive/interactive-mode.js +267 -148
  131. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  132. package/dist/modes/interactive/smooth-streaming.d.ts +39 -6
  133. package/dist/modes/interactive/smooth-streaming.d.ts.map +1 -1
  134. package/dist/modes/interactive/smooth-streaming.js +160 -73
  135. package/dist/modes/interactive/smooth-streaming.js.map +1 -1
  136. package/dist/utils/clipboard-image.d.ts.map +1 -1
  137. package/dist/utils/clipboard-image.js +7 -3
  138. package/dist/utils/clipboard-image.js.map +1 -1
  139. package/docs/terminal-setup.md +13 -0
  140. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  141. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  142. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  143. package/examples/extensions/gondolin/package-lock.json +2 -2
  144. package/examples/extensions/gondolin/package.json +1 -1
  145. package/examples/extensions/sandbox/package-lock.json +2 -2
  146. package/examples/extensions/sandbox/package.json +1 -1
  147. package/examples/extensions/with-deps/package-lock.json +2 -2
  148. package/examples/extensions/with-deps/package.json +1 -1
  149. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -18,11 +18,14 @@ try {
18
18
  catch {
19
19
  // Node < 22.8 or unwritable cache dir.
20
20
  }
21
- const [{ APP_NAME }, { configureHttpDispatcher }, { main }] = await Promise.all([
21
+ const importStarted = Date.now();
22
+ const [{ APP_NAME }, { configureHttpDispatcher }, { main }, { noteImportMain }] = await Promise.all([
22
23
  import("./config.js"),
23
24
  import("./core/http-dispatcher.js"),
24
25
  import("./main.js"),
26
+ import("./core/timings.js"),
25
27
  ]);
28
+ noteImportMain(Date.now() - importStarted);
26
29
  process.title = APP_NAME;
27
30
  process.env.PI_CODING_AGENT = "true";
28
31
  process.emitWarning = (() => { });
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,IAAI,CAAC;IACJ,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9F,kBAAkB,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC;AACrD,CAAC;AAAC,MAAM,CAAC;IACR,uCAAuC;AACxC,CAAC;AAED,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,uBAAuB,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;IAC/E,MAAM,CAAC,aAAa,CAAC;IACrB,MAAM,CAAC,2BAA2B,CAAC;IACnC,MAAM,CAAC,WAAW,CAAC;CACnB,CAAC,CAAC;AAEH,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC;AACzB,OAAO,CAAC,GAAG,CAAC,eAAe,GAAG,MAAM,CAAC;AACrC,OAAO,CAAC,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAA+B,CAAC;AAE/D,4EAA4E;AAC5E,wFAAwF;AACxF,uBAAuB,EAAE,CAAC;AAE1B,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n/**\n * CLI entry point for the refactored coding agent.\n * Uses main.ts with AgentSession and new mode modules.\n *\n * Test with: npx tsx src/cli-new.ts [args...]\n */\nimport { enableCompileCache } from \"node:module\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n// Enable before importing the rest of the graph so subsequent processes can\n// reuse V8 code cache. Helps the first start after a reboot when the OS page\n// cache is cold. Static imports are hoisted, so main is loaded dynamically.\ntry {\n\tconst agentDir = process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), \".lunr\", \"agent\");\n\tenableCompileCache(join(agentDir, \"compile-cache\"));\n} catch {\n\t// Node < 22.8 or unwritable cache dir.\n}\n\nconst [{ APP_NAME }, { configureHttpDispatcher }, { main }] = await Promise.all([\n\timport(\"./config.ts\"),\n\timport(\"./core/http-dispatcher.ts\"),\n\timport(\"./main.ts\"),\n]);\n\nprocess.title = APP_NAME;\nprocess.env.PI_CODING_AGENT = \"true\";\nprocess.emitWarning = (() => {}) as typeof process.emitWarning;\n\n// Configure undici's global dispatcher before provider SDKs issue requests.\n// Runtime settings are applied once SettingsManager has loaded global/project settings.\nconfigureHttpDispatcher();\n\nawait main(process.argv.slice(2));\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,IAAI,CAAC;IACJ,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9F,kBAAkB,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC;AACrD,CAAC;AAAC,MAAM,CAAC;IACR,uCAAuC;AACxC,CAAC;AAED,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;AACjC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,uBAAuB,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;IACnG,MAAM,CAAC,aAAa,CAAC;IACrB,MAAM,CAAC,2BAA2B,CAAC;IACnC,MAAM,CAAC,WAAW,CAAC;IACnB,MAAM,CAAC,mBAAmB,CAAC;CAC3B,CAAC,CAAC;AACH,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,CAAC;AAE3C,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC;AACzB,OAAO,CAAC,GAAG,CAAC,eAAe,GAAG,MAAM,CAAC;AACrC,OAAO,CAAC,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAA+B,CAAC;AAE/D,4EAA4E;AAC5E,wFAAwF;AACxF,uBAAuB,EAAE,CAAC;AAE1B,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n/**\n * CLI entry point for the refactored coding agent.\n * Uses main.ts with AgentSession and new mode modules.\n *\n * Test with: npx tsx src/cli-new.ts [args...]\n */\nimport { enableCompileCache } from \"node:module\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n// Enable before importing the rest of the graph so subsequent processes can\n// reuse V8 code cache. Helps the first start after a reboot when the OS page\n// cache is cold. Static imports are hoisted, so main is loaded dynamically.\ntry {\n\tconst agentDir = process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), \".lunr\", \"agent\");\n\tenableCompileCache(join(agentDir, \"compile-cache\"));\n} catch {\n\t// Node < 22.8 or unwritable cache dir.\n}\n\nconst importStarted = Date.now();\nconst [{ APP_NAME }, { configureHttpDispatcher }, { main }, { noteImportMain }] = await Promise.all([\n\timport(\"./config.ts\"),\n\timport(\"./core/http-dispatcher.ts\"),\n\timport(\"./main.ts\"),\n\timport(\"./core/timings.ts\"),\n]);\nnoteImportMain(Date.now() - importStarted);\n\nprocess.title = APP_NAME;\nprocess.env.PI_CODING_AGENT = \"true\";\nprocess.emitWarning = (() => {}) as typeof process.emitWarning;\n\n// Configure undici's global dispatcher before provider SDKs issue requests.\n// Runtime settings are applied once SettingsManager has loaded global/project settings.\nconfigureHttpDispatcher();\n\nawait main(process.argv.slice(2));\n"]}
@@ -22,9 +22,20 @@ export interface ContextBreakdownInput {
22
22
  messages: ReadonlyArray<AgentMessage>;
23
23
  contextWindow: number;
24
24
  }
25
+ export interface ContextFileBreakdown {
26
+ /** Basename of the context file (AGENTS.md, CLAUDE.md, …). */
27
+ label: string;
28
+ /** Path from the <project_instructions path="…"> wrapper, when present. */
29
+ path?: string;
30
+ tokens: number;
31
+ }
25
32
  export interface ContextBreakdown {
26
- /** System prompt, including appended project context files (AGENTS.md etc.). */
33
+ /** System prompt minus <project_context> and the skills block. */
27
34
  systemPrompt: number;
35
+ /** Each AGENTS.md / context file from <project_instructions>. */
36
+ contextFiles: ContextFileBreakdown[];
37
+ /** formatSkillsForPrompt section, if present. */
38
+ skills: number;
28
39
  /** Serialized tool schemas (name + description + JSON parameter schema). */
29
40
  toolDefinitions: number;
30
41
  /** User and extension ("custom") messages. */
@@ -54,6 +65,15 @@ export interface ContextBreakdown {
54
65
  summaries: number;
55
66
  };
56
67
  }
68
+ /** Split a live system prompt into base / context-file / skills parts. */
69
+ export declare function splitSystemPromptSections(systemPrompt: string): {
70
+ base: string;
71
+ contextFiles: Array<{
72
+ path?: string;
73
+ content: string;
74
+ }>;
75
+ skills: string;
76
+ };
57
77
  /**
58
78
  * Estimate the token cost of the tool definitions sent to the model.
59
79
  * The wire format is roughly `{ name, description, parameters(JSON schema) }` per tool.
@@ -1 +1 @@
1
- {"version":3,"file":"context-breakdown.d.ts","sourceRoot":"","sources":["../../src/core/context-breakdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAIlE,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAC;IAC3C,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAChC,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,MAAM,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;KAClB,CAAC;CACF;AAID;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,aAAa,CAAC,oBAAoB,CAAC,GAAG,MAAM,CAY/F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,qBAAqB,GAAG,gBAAgB,CA4EtF","sourcesContent":["/**\n * lunr: Context window breakdown estimation for the /context command.\n *\n * Pure functions that estimate (chars/4, same heuristic as `estimateTokens` in\n * compaction.ts) what consumes the model's context window: the system prompt\n * (project context files are baked into it), the registered tool definitions,\n * and the live session messages split into user / assistant text / thinking /\n * tool calls / tool results / summaries.\n *\n * Pass the same message list the context actually contains (post-compaction,\n * that is `AgentSession.messages`) so the breakdown matches reality.\n */\n\nimport type { AgentMessage } from \"@ashx-j/lunr-agent\";\nimport type { AssistantMessage } from \"@ashx-j/lunr-ai/compat\";\nimport { estimateTokens } from \"./compaction/index.ts\";\n\nexport interface ContextBreakdownTool {\n\tname: string;\n\tdescription: string;\n\tparameters: unknown;\n}\n\nexport interface ContextBreakdownInput {\n\tsystemPrompt: string;\n\ttools: ReadonlyArray<ContextBreakdownTool>;\n\tmessages: ReadonlyArray<AgentMessage>;\n\tcontextWindow: number;\n}\n\nexport interface ContextBreakdown {\n\t/** System prompt, including appended project context files (AGENTS.md etc.). */\n\tsystemPrompt: number;\n\t/** Serialized tool schemas (name + description + JSON parameter schema). */\n\ttoolDefinitions: number;\n\t/** User and extension (\"custom\") messages. */\n\tuser: number;\n\t/** Assistant text blocks. */\n\tassistantText: number;\n\t/** Assistant thinking blocks. */\n\tthinking: number;\n\t/** Assistant tool call blocks (name + serialized arguments). */\n\ttoolCalls: number;\n\t/** Tool result messages and bash execution output. */\n\ttoolResults: number;\n\t/** Compaction and branch summaries. */\n\tsummaries: number;\n\t/** Sum of all categories above. */\n\ttotal: number;\n\tcontextWindow: number;\n\t/** max(0, contextWindow - total) */\n\tfree: number;\n\t/** Per-category message counts (not token counts). */\n\tcounts: {\n\t\tuser: number;\n\t\tassistantText: number;\n\t\tthinking: number;\n\t\ttoolCalls: number;\n\t\ttoolResults: number;\n\t\tsummaries: number;\n\t};\n}\n\nconst CHARS_PER_TOKEN = 4;\n\n/**\n * Estimate the token cost of the tool definitions sent to the model.\n * The wire format is roughly `{ name, description, parameters(JSON schema) }` per tool.\n */\nexport function estimateToolDefinitionTokens(tools: ReadonlyArray<ContextBreakdownTool>): number {\n\tlet chars = 0;\n\tfor (const tool of tools) {\n\t\tchars += tool.name.length + tool.description.length;\n\t\ttry {\n\t\t\tconst schema = JSON.stringify(tool.parameters);\n\t\t\tif (schema) chars += schema.length;\n\t\t} catch {\n\t\t\t// Unserializable schema — skip rather than fail the whole breakdown.\n\t\t}\n\t}\n\treturn Math.ceil(chars / CHARS_PER_TOKEN);\n}\n\n/**\n * Compute the estimated context window breakdown.\n * All numbers are chars/4 estimates, matching `estimateTokens`.\n */\nexport function computeContextBreakdown(input: ContextBreakdownInput): ContextBreakdown {\n\tconst systemPrompt = Math.ceil(input.systemPrompt.length / CHARS_PER_TOKEN);\n\tconst toolDefinitions = estimateToolDefinitionTokens(input.tools);\n\n\tlet user = 0;\n\tlet assistantText = 0;\n\tlet thinking = 0;\n\tlet toolCalls = 0;\n\tlet toolResults = 0;\n\tlet summaries = 0;\n\n\tconst counts = {\n\t\tuser: 0,\n\t\tassistantText: 0,\n\t\tthinking: 0,\n\t\ttoolCalls: 0,\n\t\ttoolResults: 0,\n\t\tsummaries: 0,\n\t};\n\n\tfor (const message of input.messages) {\n\t\tswitch (message.role) {\n\t\t\tcase \"assistant\": {\n\t\t\t\tlet textChars = 0;\n\t\t\t\tlet thinkingChars = 0;\n\t\t\t\tlet toolCallChars = 0;\n\t\t\t\tfor (const block of (message as AssistantMessage).content) {\n\t\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\t\ttextChars += block.text.length;\n\t\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t\tthinkingChars += block.thinking.length;\n\t\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\t\ttoolCallChars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassistantText += Math.ceil(textChars / CHARS_PER_TOKEN);\n\t\t\t\tthinking += Math.ceil(thinkingChars / CHARS_PER_TOKEN);\n\t\t\t\ttoolCalls += Math.ceil(toolCallChars / CHARS_PER_TOKEN);\n\t\t\t\tif (textChars > 0) counts.assistantText++;\n\t\t\t\tif (thinkingChars > 0) counts.thinking++;\n\t\t\t\tif (toolCallChars > 0) counts.toolCalls++;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"user\":\n\t\t\tcase \"custom\":\n\t\t\t\tuser += estimateTokens(message);\n\t\t\t\tcounts.user++;\n\t\t\t\tbreak;\n\t\t\tcase \"toolResult\":\n\t\t\tcase \"bashExecution\":\n\t\t\t\ttoolResults += estimateTokens(message);\n\t\t\t\tcounts.toolResults++;\n\t\t\t\tbreak;\n\t\t\tcase \"branchSummary\":\n\t\t\tcase \"compactionSummary\":\n\t\t\t\tsummaries += estimateTokens(message);\n\t\t\t\tcounts.summaries++;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst total = systemPrompt + toolDefinitions + user + assistantText + thinking + toolCalls + toolResults + summaries;\n\treturn {\n\t\tsystemPrompt,\n\t\ttoolDefinitions,\n\t\tuser,\n\t\tassistantText,\n\t\tthinking,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\tsummaries,\n\t\ttotal,\n\t\tcontextWindow: input.contextWindow,\n\t\tfree: Math.max(0, input.contextWindow - total),\n\t\tcounts,\n\t};\n}\n"]}
1
+ {"version":3,"file":"context-breakdown.d.ts","sourceRoot":"","sources":["../../src/core/context-breakdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAIlE,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAC;IAC3C,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACpC,gEAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,6EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAChC,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,YAAY,EAAE,oBAAoB,EAAE,CAAC;IACrC,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,eAAe,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,MAAM,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;KAClB,CAAC;CACF;AAmBD,0EAA0E;AAC1E,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,MAAM,EAAE,MAAM,CAAC;CACf,CAsBA;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,aAAa,CAAC,oBAAoB,CAAC,GAAG,MAAM,CAY/F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,qBAAqB,GAAG,gBAAgB,CAgGtF","sourcesContent":["/**\n * lunr: Context window breakdown estimation for the /context command.\n *\n * Pure functions that estimate (chars/4, same heuristic as `estimateTokens` in\n * compaction.ts) what consumes the model's context window: the system prompt\n * (project context files are baked into it), the registered tool definitions,\n * and the live session messages split into user / assistant text / thinking /\n * tool calls / tool results / summaries.\n *\n * Pass the same message list the context actually contains (post-compaction,\n * that is `AgentSession.messages`) so the breakdown matches reality.\n */\n\nimport type { AgentMessage } from \"@ashx-j/lunr-agent\";\nimport type { AssistantMessage } from \"@ashx-j/lunr-ai/compat\";\nimport { estimateTokens } from \"./compaction/index.ts\";\n\nexport interface ContextBreakdownTool {\n\tname: string;\n\tdescription: string;\n\tparameters: unknown;\n}\n\nexport interface ContextBreakdownInput {\n\tsystemPrompt: string;\n\ttools: ReadonlyArray<ContextBreakdownTool>;\n\tmessages: ReadonlyArray<AgentMessage>;\n\tcontextWindow: number;\n}\n\nexport interface ContextFileBreakdown {\n\t/** Basename of the context file (AGENTS.md, CLAUDE.md, …). */\n\tlabel: string;\n\t/** Path from the <project_instructions path=\"…\"> wrapper, when present. */\n\tpath?: string;\n\ttokens: number;\n}\n\nexport interface ContextBreakdown {\n\t/** System prompt minus <project_context> and the skills block. */\n\tsystemPrompt: number;\n\t/** Each AGENTS.md / context file from <project_instructions>. */\n\tcontextFiles: ContextFileBreakdown[];\n\t/** formatSkillsForPrompt section, if present. */\n\tskills: number;\n\t/** Serialized tool schemas (name + description + JSON parameter schema). */\n\ttoolDefinitions: number;\n\t/** User and extension (\"custom\") messages. */\n\tuser: number;\n\t/** Assistant text blocks. */\n\tassistantText: number;\n\t/** Assistant thinking blocks. */\n\tthinking: number;\n\t/** Assistant tool call blocks (name + serialized arguments). */\n\ttoolCalls: number;\n\t/** Tool result messages and bash execution output. */\n\ttoolResults: number;\n\t/** Compaction and branch summaries. */\n\tsummaries: number;\n\t/** Sum of all categories above. */\n\ttotal: number;\n\tcontextWindow: number;\n\t/** max(0, contextWindow - total) */\n\tfree: number;\n\t/** Per-category message counts (not token counts). */\n\tcounts: {\n\t\tuser: number;\n\t\tassistantText: number;\n\t\tthinking: number;\n\t\ttoolCalls: number;\n\t\ttoolResults: number;\n\t\tsummaries: number;\n\t};\n}\n\nconst CHARS_PER_TOKEN = 4;\nconst PROJECT_CONTEXT_RE = /<project_context>[\\s\\S]*?<\\/project_context>/;\nconst PROJECT_INSTRUCTIONS_RE = /<project_instructions(?:\\s+path=\"([^\"]*)\")?>([\\s\\S]*?)<\\/project_instructions>/g;\nconst SKILLS_BLOCK_RE =\n\t/\\n\\nThe following skills provide specialized instructions for specific tasks\\.[\\s\\S]*<\\/available_skills>/;\n\nfunction estimateChars(text: string): number {\n\treturn text.length === 0 ? 0 : Math.ceil(text.length / CHARS_PER_TOKEN);\n}\n\nfunction contextFileLabel(filePath: string | undefined): string {\n\tif (!filePath) return \"AGENTS.md\";\n\tconst normalized = filePath.replace(/\\\\/g, \"/\");\n\tconst slash = normalized.lastIndexOf(\"/\");\n\treturn slash === -1 ? normalized : normalized.slice(slash + 1);\n}\n\n/** Split a live system prompt into base / context-file / skills parts. */\nexport function splitSystemPromptSections(systemPrompt: string): {\n\tbase: string;\n\tcontextFiles: Array<{ path?: string; content: string }>;\n\tskills: string;\n} {\n\tlet remaining = systemPrompt;\n\tconst contextFiles: Array<{ path?: string; content: string }> = [];\n\tconst contextMatch = remaining.match(PROJECT_CONTEXT_RE);\n\tif (contextMatch) {\n\t\tconst block = contextMatch[0];\n\t\tPROJECT_INSTRUCTIONS_RE.lastIndex = 0;\n\t\tfor (const match of block.matchAll(PROJECT_INSTRUCTIONS_RE)) {\n\t\t\tcontextFiles.push({\n\t\t\t\tpath: match[1],\n\t\t\t\tcontent: match[2] ?? \"\",\n\t\t\t});\n\t\t}\n\t\tremaining = remaining.replace(block, \"\");\n\t}\n\tlet skills = \"\";\n\tconst skillsMatch = remaining.match(SKILLS_BLOCK_RE);\n\tif (skillsMatch) {\n\t\tskills = skillsMatch[0];\n\t\tremaining = remaining.replace(skills, \"\");\n\t}\n\treturn { base: remaining, contextFiles, skills };\n}\n\n/**\n * Estimate the token cost of the tool definitions sent to the model.\n * The wire format is roughly `{ name, description, parameters(JSON schema) }` per tool.\n */\nexport function estimateToolDefinitionTokens(tools: ReadonlyArray<ContextBreakdownTool>): number {\n\tlet chars = 0;\n\tfor (const tool of tools) {\n\t\tchars += tool.name.length + tool.description.length;\n\t\ttry {\n\t\t\tconst schema = JSON.stringify(tool.parameters);\n\t\t\tif (schema) chars += schema.length;\n\t\t} catch {\n\t\t\t// Unserializable schema — skip rather than fail the whole breakdown.\n\t\t}\n\t}\n\treturn Math.ceil(chars / CHARS_PER_TOKEN);\n}\n\n/**\n * Compute the estimated context window breakdown.\n * All numbers are chars/4 estimates, matching `estimateTokens`.\n */\nexport function computeContextBreakdown(input: ContextBreakdownInput): ContextBreakdown {\n\tconst sections = splitSystemPromptSections(input.systemPrompt);\n\tconst systemPrompt = estimateChars(sections.base);\n\tconst contextFiles = sections.contextFiles.map((file) => ({\n\t\tlabel: contextFileLabel(file.path),\n\t\tpath: file.path,\n\t\ttokens: estimateChars(file.content),\n\t}));\n\tconst skills = estimateChars(sections.skills);\n\tconst toolDefinitions = estimateToolDefinitionTokens(input.tools);\n\n\tlet user = 0;\n\tlet assistantText = 0;\n\tlet thinking = 0;\n\tlet toolCalls = 0;\n\tlet toolResults = 0;\n\tlet summaries = 0;\n\n\tconst counts = {\n\t\tuser: 0,\n\t\tassistantText: 0,\n\t\tthinking: 0,\n\t\ttoolCalls: 0,\n\t\ttoolResults: 0,\n\t\tsummaries: 0,\n\t};\n\n\tfor (const message of input.messages) {\n\t\tswitch (message.role) {\n\t\t\tcase \"assistant\": {\n\t\t\t\tlet textChars = 0;\n\t\t\t\tlet thinkingChars = 0;\n\t\t\t\tlet toolCallChars = 0;\n\t\t\t\tfor (const block of (message as AssistantMessage).content) {\n\t\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\t\ttextChars += block.text.length;\n\t\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t\tthinkingChars += block.thinking.length;\n\t\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\t\ttoolCallChars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassistantText += Math.ceil(textChars / CHARS_PER_TOKEN);\n\t\t\t\tthinking += Math.ceil(thinkingChars / CHARS_PER_TOKEN);\n\t\t\t\ttoolCalls += Math.ceil(toolCallChars / CHARS_PER_TOKEN);\n\t\t\t\tif (textChars > 0) counts.assistantText++;\n\t\t\t\tif (thinkingChars > 0) counts.thinking++;\n\t\t\t\tif (toolCallChars > 0) counts.toolCalls++;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"user\":\n\t\t\tcase \"custom\":\n\t\t\t\tuser += estimateTokens(message);\n\t\t\t\tcounts.user++;\n\t\t\t\tbreak;\n\t\t\tcase \"toolResult\":\n\t\t\tcase \"bashExecution\":\n\t\t\t\ttoolResults += estimateTokens(message);\n\t\t\t\tcounts.toolResults++;\n\t\t\t\tbreak;\n\t\t\tcase \"branchSummary\":\n\t\t\tcase \"compactionSummary\":\n\t\t\t\tsummaries += estimateTokens(message);\n\t\t\t\tcounts.summaries++;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst contextFileTokens = contextFiles.reduce((sum, file) => sum + file.tokens, 0);\n\tconst total =\n\t\tsystemPrompt +\n\t\tcontextFileTokens +\n\t\tskills +\n\t\ttoolDefinitions +\n\t\tuser +\n\t\tassistantText +\n\t\tthinking +\n\t\ttoolCalls +\n\t\ttoolResults +\n\t\tsummaries;\n\treturn {\n\t\tsystemPrompt,\n\t\tcontextFiles,\n\t\tskills,\n\t\ttoolDefinitions,\n\t\tuser,\n\t\tassistantText,\n\t\tthinking,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\tsummaries,\n\t\ttotal,\n\t\tcontextWindow: input.contextWindow,\n\t\tfree: Math.max(0, input.contextWindow - total),\n\t\tcounts,\n\t};\n}\n"]}
@@ -12,6 +12,43 @@
12
12
  */
13
13
  import { estimateTokens } from "./compaction/index.js";
14
14
  const CHARS_PER_TOKEN = 4;
15
+ const PROJECT_CONTEXT_RE = /<project_context>[\s\S]*?<\/project_context>/;
16
+ const PROJECT_INSTRUCTIONS_RE = /<project_instructions(?:\s+path="([^"]*)")?>([\s\S]*?)<\/project_instructions>/g;
17
+ const SKILLS_BLOCK_RE = /\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*<\/available_skills>/;
18
+ function estimateChars(text) {
19
+ return text.length === 0 ? 0 : Math.ceil(text.length / CHARS_PER_TOKEN);
20
+ }
21
+ function contextFileLabel(filePath) {
22
+ if (!filePath)
23
+ return "AGENTS.md";
24
+ const normalized = filePath.replace(/\\/g, "/");
25
+ const slash = normalized.lastIndexOf("/");
26
+ return slash === -1 ? normalized : normalized.slice(slash + 1);
27
+ }
28
+ /** Split a live system prompt into base / context-file / skills parts. */
29
+ export function splitSystemPromptSections(systemPrompt) {
30
+ let remaining = systemPrompt;
31
+ const contextFiles = [];
32
+ const contextMatch = remaining.match(PROJECT_CONTEXT_RE);
33
+ if (contextMatch) {
34
+ const block = contextMatch[0];
35
+ PROJECT_INSTRUCTIONS_RE.lastIndex = 0;
36
+ for (const match of block.matchAll(PROJECT_INSTRUCTIONS_RE)) {
37
+ contextFiles.push({
38
+ path: match[1],
39
+ content: match[2] ?? "",
40
+ });
41
+ }
42
+ remaining = remaining.replace(block, "");
43
+ }
44
+ let skills = "";
45
+ const skillsMatch = remaining.match(SKILLS_BLOCK_RE);
46
+ if (skillsMatch) {
47
+ skills = skillsMatch[0];
48
+ remaining = remaining.replace(skills, "");
49
+ }
50
+ return { base: remaining, contextFiles, skills };
51
+ }
15
52
  /**
16
53
  * Estimate the token cost of the tool definitions sent to the model.
17
54
  * The wire format is roughly `{ name, description, parameters(JSON schema) }` per tool.
@@ -36,7 +73,14 @@ export function estimateToolDefinitionTokens(tools) {
36
73
  * All numbers are chars/4 estimates, matching `estimateTokens`.
37
74
  */
38
75
  export function computeContextBreakdown(input) {
39
- const systemPrompt = Math.ceil(input.systemPrompt.length / CHARS_PER_TOKEN);
76
+ const sections = splitSystemPromptSections(input.systemPrompt);
77
+ const systemPrompt = estimateChars(sections.base);
78
+ const contextFiles = sections.contextFiles.map((file) => ({
79
+ label: contextFileLabel(file.path),
80
+ path: file.path,
81
+ tokens: estimateChars(file.content),
82
+ }));
83
+ const skills = estimateChars(sections.skills);
40
84
  const toolDefinitions = estimateToolDefinitionTokens(input.tools);
41
85
  let user = 0;
42
86
  let assistantText = 0;
@@ -97,9 +141,21 @@ export function computeContextBreakdown(input) {
97
141
  break;
98
142
  }
99
143
  }
100
- const total = systemPrompt + toolDefinitions + user + assistantText + thinking + toolCalls + toolResults + summaries;
144
+ const contextFileTokens = contextFiles.reduce((sum, file) => sum + file.tokens, 0);
145
+ const total = systemPrompt +
146
+ contextFileTokens +
147
+ skills +
148
+ toolDefinitions +
149
+ user +
150
+ assistantText +
151
+ thinking +
152
+ toolCalls +
153
+ toolResults +
154
+ summaries;
101
155
  return {
102
156
  systemPrompt,
157
+ contextFiles,
158
+ skills,
103
159
  toolDefinitions,
104
160
  user,
105
161
  assistantText,
@@ -1 +1 @@
1
- {"version":3,"file":"context-breakdown.js","sourceRoot":"","sources":["../../src/core/context-breakdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunr: Context window breakdown estimation for the /context command.\n *\n * Pure functions that estimate (chars/4, same heuristic as `estimateTokens` in\n * compaction.ts) what consumes the model's context window: the system prompt\n * (project context files are baked into it), the registered tool definitions,\n * and the live session messages split into user / assistant text / thinking /\n * tool calls / tool results / summaries.\n *\n * Pass the same message list the context actually contains (post-compaction,\n * that is `AgentSession.messages`) so the breakdown matches reality.\n */\n\nimport type { AgentMessage } from \"@ashx-j/lunr-agent\";\nimport type { AssistantMessage } from \"@ashx-j/lunr-ai/compat\";\nimport { estimateTokens } from \"./compaction/index.ts\";\n\nexport interface ContextBreakdownTool {\n\tname: string;\n\tdescription: string;\n\tparameters: unknown;\n}\n\nexport interface ContextBreakdownInput {\n\tsystemPrompt: string;\n\ttools: ReadonlyArray<ContextBreakdownTool>;\n\tmessages: ReadonlyArray<AgentMessage>;\n\tcontextWindow: number;\n}\n\nexport interface ContextBreakdown {\n\t/** System prompt, including appended project context files (AGENTS.md etc.). */\n\tsystemPrompt: number;\n\t/** Serialized tool schemas (name + description + JSON parameter schema). */\n\ttoolDefinitions: number;\n\t/** User and extension (\"custom\") messages. */\n\tuser: number;\n\t/** Assistant text blocks. */\n\tassistantText: number;\n\t/** Assistant thinking blocks. */\n\tthinking: number;\n\t/** Assistant tool call blocks (name + serialized arguments). */\n\ttoolCalls: number;\n\t/** Tool result messages and bash execution output. */\n\ttoolResults: number;\n\t/** Compaction and branch summaries. */\n\tsummaries: number;\n\t/** Sum of all categories above. */\n\ttotal: number;\n\tcontextWindow: number;\n\t/** max(0, contextWindow - total) */\n\tfree: number;\n\t/** Per-category message counts (not token counts). */\n\tcounts: {\n\t\tuser: number;\n\t\tassistantText: number;\n\t\tthinking: number;\n\t\ttoolCalls: number;\n\t\ttoolResults: number;\n\t\tsummaries: number;\n\t};\n}\n\nconst CHARS_PER_TOKEN = 4;\n\n/**\n * Estimate the token cost of the tool definitions sent to the model.\n * The wire format is roughly `{ name, description, parameters(JSON schema) }` per tool.\n */\nexport function estimateToolDefinitionTokens(tools: ReadonlyArray<ContextBreakdownTool>): number {\n\tlet chars = 0;\n\tfor (const tool of tools) {\n\t\tchars += tool.name.length + tool.description.length;\n\t\ttry {\n\t\t\tconst schema = JSON.stringify(tool.parameters);\n\t\t\tif (schema) chars += schema.length;\n\t\t} catch {\n\t\t\t// Unserializable schema — skip rather than fail the whole breakdown.\n\t\t}\n\t}\n\treturn Math.ceil(chars / CHARS_PER_TOKEN);\n}\n\n/**\n * Compute the estimated context window breakdown.\n * All numbers are chars/4 estimates, matching `estimateTokens`.\n */\nexport function computeContextBreakdown(input: ContextBreakdownInput): ContextBreakdown {\n\tconst systemPrompt = Math.ceil(input.systemPrompt.length / CHARS_PER_TOKEN);\n\tconst toolDefinitions = estimateToolDefinitionTokens(input.tools);\n\n\tlet user = 0;\n\tlet assistantText = 0;\n\tlet thinking = 0;\n\tlet toolCalls = 0;\n\tlet toolResults = 0;\n\tlet summaries = 0;\n\n\tconst counts = {\n\t\tuser: 0,\n\t\tassistantText: 0,\n\t\tthinking: 0,\n\t\ttoolCalls: 0,\n\t\ttoolResults: 0,\n\t\tsummaries: 0,\n\t};\n\n\tfor (const message of input.messages) {\n\t\tswitch (message.role) {\n\t\t\tcase \"assistant\": {\n\t\t\t\tlet textChars = 0;\n\t\t\t\tlet thinkingChars = 0;\n\t\t\t\tlet toolCallChars = 0;\n\t\t\t\tfor (const block of (message as AssistantMessage).content) {\n\t\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\t\ttextChars += block.text.length;\n\t\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t\tthinkingChars += block.thinking.length;\n\t\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\t\ttoolCallChars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassistantText += Math.ceil(textChars / CHARS_PER_TOKEN);\n\t\t\t\tthinking += Math.ceil(thinkingChars / CHARS_PER_TOKEN);\n\t\t\t\ttoolCalls += Math.ceil(toolCallChars / CHARS_PER_TOKEN);\n\t\t\t\tif (textChars > 0) counts.assistantText++;\n\t\t\t\tif (thinkingChars > 0) counts.thinking++;\n\t\t\t\tif (toolCallChars > 0) counts.toolCalls++;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"user\":\n\t\t\tcase \"custom\":\n\t\t\t\tuser += estimateTokens(message);\n\t\t\t\tcounts.user++;\n\t\t\t\tbreak;\n\t\t\tcase \"toolResult\":\n\t\t\tcase \"bashExecution\":\n\t\t\t\ttoolResults += estimateTokens(message);\n\t\t\t\tcounts.toolResults++;\n\t\t\t\tbreak;\n\t\t\tcase \"branchSummary\":\n\t\t\tcase \"compactionSummary\":\n\t\t\t\tsummaries += estimateTokens(message);\n\t\t\t\tcounts.summaries++;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst total = systemPrompt + toolDefinitions + user + assistantText + thinking + toolCalls + toolResults + summaries;\n\treturn {\n\t\tsystemPrompt,\n\t\ttoolDefinitions,\n\t\tuser,\n\t\tassistantText,\n\t\tthinking,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\tsummaries,\n\t\ttotal,\n\t\tcontextWindow: input.contextWindow,\n\t\tfree: Math.max(0, input.contextWindow - total),\n\t\tcounts,\n\t};\n}\n"]}
1
+ {"version":3,"file":"context-breakdown.js","sourceRoot":"","sources":["../../src/core/context-breakdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunr: Context window breakdown estimation for the /context command.\n *\n * Pure functions that estimate (chars/4, same heuristic as `estimateTokens` in\n * compaction.ts) what consumes the model's context window: the system prompt\n * (project context files are baked into it), the registered tool definitions,\n * and the live session messages split into user / assistant text / thinking /\n * tool calls / tool results / summaries.\n *\n * Pass the same message list the context actually contains (post-compaction,\n * that is `AgentSession.messages`) so the breakdown matches reality.\n */\n\nimport type { AgentMessage } from \"@ashx-j/lunr-agent\";\nimport type { AssistantMessage } from \"@ashx-j/lunr-ai/compat\";\nimport { estimateTokens } from \"./compaction/index.ts\";\n\nexport interface ContextBreakdownTool {\n\tname: string;\n\tdescription: string;\n\tparameters: unknown;\n}\n\nexport interface ContextBreakdownInput {\n\tsystemPrompt: string;\n\ttools: ReadonlyArray<ContextBreakdownTool>;\n\tmessages: ReadonlyArray<AgentMessage>;\n\tcontextWindow: number;\n}\n\nexport interface ContextFileBreakdown {\n\t/** Basename of the context file (AGENTS.md, CLAUDE.md, …). */\n\tlabel: string;\n\t/** Path from the <project_instructions path=\"…\"> wrapper, when present. */\n\tpath?: string;\n\ttokens: number;\n}\n\nexport interface ContextBreakdown {\n\t/** System prompt minus <project_context> and the skills block. */\n\tsystemPrompt: number;\n\t/** Each AGENTS.md / context file from <project_instructions>. */\n\tcontextFiles: ContextFileBreakdown[];\n\t/** formatSkillsForPrompt section, if present. */\n\tskills: number;\n\t/** Serialized tool schemas (name + description + JSON parameter schema). */\n\ttoolDefinitions: number;\n\t/** User and extension (\"custom\") messages. */\n\tuser: number;\n\t/** Assistant text blocks. */\n\tassistantText: number;\n\t/** Assistant thinking blocks. */\n\tthinking: number;\n\t/** Assistant tool call blocks (name + serialized arguments). */\n\ttoolCalls: number;\n\t/** Tool result messages and bash execution output. */\n\ttoolResults: number;\n\t/** Compaction and branch summaries. */\n\tsummaries: number;\n\t/** Sum of all categories above. */\n\ttotal: number;\n\tcontextWindow: number;\n\t/** max(0, contextWindow - total) */\n\tfree: number;\n\t/** Per-category message counts (not token counts). */\n\tcounts: {\n\t\tuser: number;\n\t\tassistantText: number;\n\t\tthinking: number;\n\t\ttoolCalls: number;\n\t\ttoolResults: number;\n\t\tsummaries: number;\n\t};\n}\n\nconst CHARS_PER_TOKEN = 4;\nconst PROJECT_CONTEXT_RE = /<project_context>[\\s\\S]*?<\\/project_context>/;\nconst PROJECT_INSTRUCTIONS_RE = /<project_instructions(?:\\s+path=\"([^\"]*)\")?>([\\s\\S]*?)<\\/project_instructions>/g;\nconst SKILLS_BLOCK_RE =\n\t/\\n\\nThe following skills provide specialized instructions for specific tasks\\.[\\s\\S]*<\\/available_skills>/;\n\nfunction estimateChars(text: string): number {\n\treturn text.length === 0 ? 0 : Math.ceil(text.length / CHARS_PER_TOKEN);\n}\n\nfunction contextFileLabel(filePath: string | undefined): string {\n\tif (!filePath) return \"AGENTS.md\";\n\tconst normalized = filePath.replace(/\\\\/g, \"/\");\n\tconst slash = normalized.lastIndexOf(\"/\");\n\treturn slash === -1 ? normalized : normalized.slice(slash + 1);\n}\n\n/** Split a live system prompt into base / context-file / skills parts. */\nexport function splitSystemPromptSections(systemPrompt: string): {\n\tbase: string;\n\tcontextFiles: Array<{ path?: string; content: string }>;\n\tskills: string;\n} {\n\tlet remaining = systemPrompt;\n\tconst contextFiles: Array<{ path?: string; content: string }> = [];\n\tconst contextMatch = remaining.match(PROJECT_CONTEXT_RE);\n\tif (contextMatch) {\n\t\tconst block = contextMatch[0];\n\t\tPROJECT_INSTRUCTIONS_RE.lastIndex = 0;\n\t\tfor (const match of block.matchAll(PROJECT_INSTRUCTIONS_RE)) {\n\t\t\tcontextFiles.push({\n\t\t\t\tpath: match[1],\n\t\t\t\tcontent: match[2] ?? \"\",\n\t\t\t});\n\t\t}\n\t\tremaining = remaining.replace(block, \"\");\n\t}\n\tlet skills = \"\";\n\tconst skillsMatch = remaining.match(SKILLS_BLOCK_RE);\n\tif (skillsMatch) {\n\t\tskills = skillsMatch[0];\n\t\tremaining = remaining.replace(skills, \"\");\n\t}\n\treturn { base: remaining, contextFiles, skills };\n}\n\n/**\n * Estimate the token cost of the tool definitions sent to the model.\n * The wire format is roughly `{ name, description, parameters(JSON schema) }` per tool.\n */\nexport function estimateToolDefinitionTokens(tools: ReadonlyArray<ContextBreakdownTool>): number {\n\tlet chars = 0;\n\tfor (const tool of tools) {\n\t\tchars += tool.name.length + tool.description.length;\n\t\ttry {\n\t\t\tconst schema = JSON.stringify(tool.parameters);\n\t\t\tif (schema) chars += schema.length;\n\t\t} catch {\n\t\t\t// Unserializable schema — skip rather than fail the whole breakdown.\n\t\t}\n\t}\n\treturn Math.ceil(chars / CHARS_PER_TOKEN);\n}\n\n/**\n * Compute the estimated context window breakdown.\n * All numbers are chars/4 estimates, matching `estimateTokens`.\n */\nexport function computeContextBreakdown(input: ContextBreakdownInput): ContextBreakdown {\n\tconst sections = splitSystemPromptSections(input.systemPrompt);\n\tconst systemPrompt = estimateChars(sections.base);\n\tconst contextFiles = sections.contextFiles.map((file) => ({\n\t\tlabel: contextFileLabel(file.path),\n\t\tpath: file.path,\n\t\ttokens: estimateChars(file.content),\n\t}));\n\tconst skills = estimateChars(sections.skills);\n\tconst toolDefinitions = estimateToolDefinitionTokens(input.tools);\n\n\tlet user = 0;\n\tlet assistantText = 0;\n\tlet thinking = 0;\n\tlet toolCalls = 0;\n\tlet toolResults = 0;\n\tlet summaries = 0;\n\n\tconst counts = {\n\t\tuser: 0,\n\t\tassistantText: 0,\n\t\tthinking: 0,\n\t\ttoolCalls: 0,\n\t\ttoolResults: 0,\n\t\tsummaries: 0,\n\t};\n\n\tfor (const message of input.messages) {\n\t\tswitch (message.role) {\n\t\t\tcase \"assistant\": {\n\t\t\t\tlet textChars = 0;\n\t\t\t\tlet thinkingChars = 0;\n\t\t\t\tlet toolCallChars = 0;\n\t\t\t\tfor (const block of (message as AssistantMessage).content) {\n\t\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\t\ttextChars += block.text.length;\n\t\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t\tthinkingChars += block.thinking.length;\n\t\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\t\ttoolCallChars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassistantText += Math.ceil(textChars / CHARS_PER_TOKEN);\n\t\t\t\tthinking += Math.ceil(thinkingChars / CHARS_PER_TOKEN);\n\t\t\t\ttoolCalls += Math.ceil(toolCallChars / CHARS_PER_TOKEN);\n\t\t\t\tif (textChars > 0) counts.assistantText++;\n\t\t\t\tif (thinkingChars > 0) counts.thinking++;\n\t\t\t\tif (toolCallChars > 0) counts.toolCalls++;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"user\":\n\t\t\tcase \"custom\":\n\t\t\t\tuser += estimateTokens(message);\n\t\t\t\tcounts.user++;\n\t\t\t\tbreak;\n\t\t\tcase \"toolResult\":\n\t\t\tcase \"bashExecution\":\n\t\t\t\ttoolResults += estimateTokens(message);\n\t\t\t\tcounts.toolResults++;\n\t\t\t\tbreak;\n\t\t\tcase \"branchSummary\":\n\t\t\tcase \"compactionSummary\":\n\t\t\t\tsummaries += estimateTokens(message);\n\t\t\t\tcounts.summaries++;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst contextFileTokens = contextFiles.reduce((sum, file) => sum + file.tokens, 0);\n\tconst total =\n\t\tsystemPrompt +\n\t\tcontextFileTokens +\n\t\tskills +\n\t\ttoolDefinitions +\n\t\tuser +\n\t\tassistantText +\n\t\tthinking +\n\t\ttoolCalls +\n\t\ttoolResults +\n\t\tsummaries;\n\treturn {\n\t\tsystemPrompt,\n\t\tcontextFiles,\n\t\tskills,\n\t\ttoolDefinitions,\n\t\tuser,\n\t\tassistantText,\n\t\tthinking,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\tsummaries,\n\t\ttotal,\n\t\tcontextWindow: input.contextWindow,\n\t\tfree: Math.max(0, input.contextWindow - total),\n\t\tcounts,\n\t};\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=host-static.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-static.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/host-static.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Static host graph for the Bun binary. bun/cli.ts imports this so VIRTUAL_MODULES\n * still contains jiti, providers/all, and the public coding-agent barrel.\n * Node CLI must not import this file.\n */\nimport * as bundledPiCodingAgent from \"../../index.ts\";\nimport * as bundledPiAiProviders from \"@ashx-j/lunr-ai/providers/all\";\nimport { createJiti } from \"jiti/static\";\nimport { registerBunExtensionHost } from \"./loader.ts\";\n\nregisterBunExtensionHost({\n\tcreateJiti,\n\tprovidersAll: bundledPiAiProviders,\n\tcodingAgent: bundledPiCodingAgent,\n});\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Static host graph for the Bun binary. bun/cli.ts imports this so VIRTUAL_MODULES
3
+ * still contains jiti, providers/all, and the public coding-agent barrel.
4
+ * Node CLI must not import this file.
5
+ */
6
+ import * as bundledPiCodingAgent from "../../index.js";
7
+ import * as bundledPiAiProviders from "@ashx-j/lunr-ai/providers/all";
8
+ import { createJiti } from "jiti/static";
9
+ import { registerBunExtensionHost } from "./loader.js";
10
+ registerBunExtensionHost({
11
+ createJiti,
12
+ providersAll: bundledPiAiProviders,
13
+ codingAgent: bundledPiCodingAgent,
14
+ });
15
+ //# sourceMappingURL=host-static.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-static.js","sourceRoot":"","sources":["../../../src/core/extensions/host-static.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,oBAAoB,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,oBAAoB,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAEvD,wBAAwB,CAAC;IACxB,UAAU;IACV,YAAY,EAAE,oBAAoB;IAClC,WAAW,EAAE,oBAAoB;CACjC,CAAC,CAAC","sourcesContent":["/**\n * Static host graph for the Bun binary. bun/cli.ts imports this so VIRTUAL_MODULES\n * still contains jiti, providers/all, and the public coding-agent barrel.\n * Node CLI must not import this file.\n */\nimport * as bundledPiCodingAgent from \"../../index.ts\";\nimport * as bundledPiAiProviders from \"@ashx-j/lunr-ai/providers/all\";\nimport { createJiti } from \"jiti/static\";\nimport { registerBunExtensionHost } from \"./loader.ts\";\n\nregisterBunExtensionHost({\n\tcreateJiti,\n\tprovidersAll: bundledPiAiProviders,\n\tcodingAgent: bundledPiCodingAgent,\n});\n"]}
@@ -4,6 +4,15 @@
4
4
  */
5
5
  import { type EventBus } from "../event-bus.ts";
6
6
  import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./types.ts";
7
+ type CreateJiti = typeof import("jiti/static").createJiti;
8
+ interface BunExtensionHost {
9
+ createJiti: CreateJiti;
10
+ providersAll: unknown;
11
+ codingAgent: unknown;
12
+ }
13
+ /** Register host modules that must stay static for the Bun binary. */
14
+ export declare function registerBunExtensionHost(host: BunExtensionHost): void;
15
+ export declare function getExtensionAliasesForTest(): Record<string, string>;
7
16
  export declare function clearExtensionCache(): void;
8
17
  /**
9
18
  * Create a runtime with throwing stubs for action methods.
@@ -20,4 +29,5 @@ export declare function loadExtensionsCached(paths: string[], cwd: string, event
20
29
  * Discover and load extensions from standard locations.
21
30
  */
22
31
  export declare function discoverAndLoadExtensions(configuredPaths: string[], cwd: string, agentDir?: string, eventBus?: EventBus): Promise<LoadExtensionsResult>;
32
+ export {};
23
33
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAwBH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAKhE,OAAO,KAAK,EAEX,SAAS,EAET,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EAKpB,MAAM,YAAY,CAAC;AA0GpB,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAWD;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CA8CzD;AA8PD;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,GACxB,OAAO,CAAC,SAAS,CAAC,CAOpB;AA6CD,wBAAsB,cAAc,CACnC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,QAAQ,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,QAAQ,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AA+GD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA2C/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension modules using jiti.\n *\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport * as _bundledPiAgentCore from \"@ashx-j/lunr-agent\";\nimport * as _bundledPiAiCompat from \"@ashx-j/lunr-ai/compat\";\nimport * as _bundledPiAiOauth from \"@ashx-j/lunr-ai/oauth\";\nimport * as _bundledPiAiProviders from \"@ashx-j/lunr-ai/providers/all\";\nimport type { KeyId } from \"@ashx-j/lunr-tui\";\nimport * as _bundledPiTui from \"@ashx-j/lunr-tui\";\nimport { createJiti } from \"jiti/static\";\n// Static imports of packages that extensions may use.\n// These MUST be static so Bun bundles them into the compiled binary.\n// The virtualModules option then makes them available to extensions.\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from \"../../config.ts\";\n// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n// avoiding a circular dependency. Extensions can import from @ashx-j/lunr.\nimport * as _bundledPiCodingAgent from \"../../index.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { createEventBus, type EventBus } from \"../event-bus.ts\";\nimport type { ExecOptions } from \"../exec.ts\";\nimport { execCommand } from \"../exec.ts\";\nimport { createSyntheticSourceInfo } from \"../source-info.ts\";\nimport { time } from \"../timings.ts\";\nimport type {\n\tEntryRenderer,\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tProviderConfig,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.ts\";\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary) */\nconst VIRTUAL_MODULES: Record<string, unknown> = {\n\ttypebox: _bundledTypebox,\n\t\"typebox/compile\": _bundledTypeboxCompile,\n\t\"typebox/value\": _bundledTypeboxValue,\n\t\"@sinclair/typebox\": _bundledTypebox,\n\t\"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n\t\"@sinclair/typebox/value\": _bundledTypeboxValue,\n\t\"@ashx-j/lunr-agent\": _bundledPiAgentCore,\n\t\"@ashx-j/lunr-tui\": _bundledPiTui,\n\t// Extensions resolve the pi-ai root to the compat entrypoint (a strict\n\t// superset of the core entrypoint): existing extensions using the old\n\t// global API keep working at runtime until compat is removed.\n\t\"@ashx-j/lunr-ai\": _bundledPiAiCompat,\n\t\"@ashx-j/lunr-ai/compat\": _bundledPiAiCompat,\n\t\"@ashx-j/lunr-ai/oauth\": _bundledPiAiOauth,\n\t\"@ashx-j/lunr-ai/providers/all\": _bundledPiAiProviders,\n\t\"@ashx-j/lunr\": _bundledPiCodingAgent,\n\t\"@mariozechner/pi-agent-core\": _bundledPiAgentCore,\n\t\"@mariozechner/pi-tui\": _bundledPiTui,\n\t\"@mariozechner/pi-ai\": _bundledPiAiCompat,\n\t\"@mariozechner/pi-ai/compat\": _bundledPiAiCompat,\n\t\"@mariozechner/pi-ai/oauth\": _bundledPiAiOauth,\n\t\"@mariozechner/pi-ai/providers/all\": _bundledPiAiProviders,\n\t\"@mariozechner/pi-coding-agent\": _bundledPiCodingAgent,\n};\n\nconst require = createRequire(import.meta.url);\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nlet _aliases: Record<string, string> | null = null;\n\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\tconst typeboxEntry = require.resolve(\"typebox\");\n\tconst typeboxCompileEntry = require.resolve(\"typebox/compile\");\n\tconst typeboxValueEntry = require.resolve(\"typebox/value\");\n\n\tconst packagesRoot = path.resolve(__dirname, \"../../../../\");\n\tconst resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {\n\t\tconst workspacePath = path.join(packagesRoot, workspaceRelativePath);\n\t\tif (fs.existsSync(workspacePath)) {\n\t\t\treturn workspacePath;\n\t\t}\n\t\treturn fileURLToPath(import.meta.resolve(specifier));\n\t};\n\n\tconst piCodingAgentEntry = packageIndex;\n\tconst piAgentCoreEntry = resolveWorkspaceOrImport(\"agent/dist/index.js\", \"@ashx-j/lunr-agent\");\n\tconst piTuiEntry = resolveWorkspaceOrImport(\"tui/dist/index.js\", \"@ashx-j/lunr-tui\");\n\t// Extensions resolve the pi-ai root to the compat entrypoint (a strict\n\t// superset of the core entrypoint): existing extensions using the old\n\t// global API keep working at runtime until compat is removed.\n\tconst piAiCompatEntry = resolveWorkspaceOrImport(\"ai/dist/compat.js\", \"@ashx-j/lunr-ai/compat\");\n\tconst piAiOauthEntry = resolveWorkspaceOrImport(\"ai/dist/oauth.js\", \"@ashx-j/lunr-ai/oauth\");\n\tconst piAiProvidersEntry = resolveWorkspaceOrImport(\n\t\t\"ai/dist/providers/all.js\",\n\t\t\"@ashx-j/lunr-ai/providers/all\",\n\t);\n\n\t_aliases = {\n\t\t\"@ashx-j/lunr\": piCodingAgentEntry,\n\t\t\"@ashx-j/lunr-agent\": piAgentCoreEntry,\n\t\t\"@ashx-j/lunr-tui\": piTuiEntry,\n\t\t\"@ashx-j/lunr-ai/providers/all\": piAiProvidersEntry,\n\t\t\"@ashx-j/lunr-ai/compat\": piAiCompatEntry,\n\t\t\"@ashx-j/lunr-ai/oauth\": piAiOauthEntry,\n\t\t\"@ashx-j/lunr-ai\": piAiCompatEntry,\n\t\t\"@mariozechner/pi-coding-agent\": piCodingAgentEntry,\n\t\t\"@mariozechner/pi-agent-core\": piAgentCoreEntry,\n\t\t\"@mariozechner/pi-tui\": piTuiEntry,\n\t\t\"@mariozechner/pi-ai/providers/all\": piAiProvidersEntry,\n\t\t\"@mariozechner/pi-ai/compat\": piAiCompatEntry,\n\t\t\"@mariozechner/pi-ai/oauth\": piAiOauthEntry,\n\t\t\"@mariozechner/pi-ai\": piAiCompatEntry,\n\t\ttypebox: typeboxEntry,\n\t\t\"typebox/compile\": typeboxCompileEntry,\n\t\t\"typebox/value\": typeboxValueEntry,\n\t\t\"@sinclair/typebox\": typeboxEntry,\n\t\t\"@sinclair/typebox/compile\": typeboxCompileEntry,\n\t\t\"@sinclair/typebox/value\": typeboxValueEntry,\n\t};\n\n\treturn _aliases;\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\nlet extensionCacheCwd: string | undefined;\nlet extensionCacheGeneration = 0;\nconst extensionCache = new Map<string, ExtensionFactory>();\n\ninterface ExtensionCacheToken {\n\tcwd: string;\n\tgeneration: number;\n}\n\nexport function clearExtensionCache(): void {\n\textensionCache.clear();\n\textensionCacheCwd = undefined;\n\textensionCacheGeneration++;\n}\n\nfunction useExtensionCacheCwd(cwd: string): ExtensionCacheToken {\n\tconst resolvedCwd = resolvePath(cwd);\n\tif (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) {\n\t\tclearExtensionCache();\n\t}\n\textensionCacheCwd = resolvedCwd;\n\treturn { cwd: resolvedCwd, generation: extensionCacheGeneration };\n}\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.bindCore() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\tconst state: { staleMessage?: string } = {};\n\tconst assertActive = () => {\n\t\tif (state.staleMessage) {\n\t\t\tthrow new Error(state.staleMessage);\n\t\t}\n\t};\n\n\tconst runtime: ExtensionRuntime = {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tsetSessionName: notInitialized,\n\t\tgetSessionName: notInitialized,\n\t\tsetLabel: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\t// registerTool() is valid during extension load; refresh is only needed post-bind.\n\t\trefreshTools: () => {},\n\t\tgetCommands: notInitialized,\n\t\tsetModel: () => Promise.reject(new Error(\"Extension runtime not initialized\")),\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\tflagValues: new Map(),\n\t\tpendingProviderRegistrations: [],\n\t\tassertActive,\n\t\tinvalidate: (message) => {\n\t\t\tstate.staleMessage ??=\n\t\t\t\tmessage ??\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\";\n\t\t},\n\t\t// Pre-bind: queue registrations so bindCore() can flush them once the\n\t\t// model registry is available. bindCore() replaces both with direct calls.\n\t\tregisterProvider: (name, config, extensionPath = \"<unknown>\") => {\n\t\t\truntime.pendingProviderRegistrations.push({ name, config, extensionPath });\n\t\t},\n\t\tunregisterProvider: (name) => {\n\t\t\truntime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);\n\t\t},\n\t};\n\n\treturn runtime;\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\truntime.assertActive();\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\truntime.assertActive();\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t\truntime.refreshTools();\n\t\t},\n\n\t\tregisterCommand(name: string, options: Omit<RegisteredCommand, \"name\" | \"sourceInfo\">): void {\n\t\t\truntime.assertActive();\n\t\t\textension.commands.set(name, {\n\t\t\t\tname,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t...options,\n\t\t\t});\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.ts\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined && !runtime.flagValues.has(name)) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\tregisterEntryRenderer<T>(customType: string, renderer: EntryRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.entryRenderers ??= new Map();\n\t\t\textension.entryRenderers.set(customType, renderer as EntryRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\truntime.assertActive();\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\tsetSessionName(name: string): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setSessionName(name);\n\t\t},\n\n\t\tgetSessionName(): string | undefined {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getSessionName();\n\t\t},\n\n\t\tsetLabel(entryId: string, label: string | undefined): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setLabel(entryId, label);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\truntime.assertActive();\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tgetCommands() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getCommands();\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.assertActive();\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tregisterProvider(name: string, config: ProviderConfig) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerProvider(name, config, extension.path);\n\t\t},\n\n\t\tunregisterProvider(name: string) {\n\t\t\truntime.assertActive();\n\t\t\truntime.unregisterProvider(name, extension.path);\n\t\t},\n\n\t\tevents: eventBus,\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nfunction isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken {\n\treturn (\n\t\tcacheToken !== undefined &&\n\t\textensionCacheCwd === cacheToken.cwd &&\n\t\textensionCacheGeneration === cacheToken.generation\n\t);\n}\n\nasync function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken) {\n\tif (isCurrentCacheToken(cacheToken)) {\n\t\tconst cachedFactory = extensionCache.get(extensionPath);\n\t\tif (cachedFactory) {\n\t\t\treturn cachedFactory;\n\t\t}\n\t}\n\n\tconst jiti = createJiti(import.meta.url, {\n\t\tmoduleCache: false,\n\t\t// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)\n\t\t// Also disable tryNative so jiti handles ALL imports (not just the entry point)\n\t\t// In Node.js/dev: use aliases to resolve to node_modules paths\n\t\t...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),\n\t});\n\n\tconst module = await jiti.import(extensionPath, { default: true });\n\tconst factory = module as ExtensionFactory;\n\tif (typeof factory !== \"function\") {\n\t\treturn undefined;\n\t}\n\tif (isCurrentCacheToken(cacheToken)) {\n\t\textensionCache.set(extensionPath, factory);\n\t}\n\treturn factory;\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\tconst source =\n\t\textensionPath.startsWith(\"<\") && extensionPath.endsWith(\">\")\n\t\t\t? extensionPath.slice(1, -1).split(\":\")[0] || \"temporary\"\n\t\t\t: \"local\";\n\tconst baseDir = extensionPath.startsWith(\"<\") ? undefined : path.dirname(resolvedPath);\n\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\tsourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tentryRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t};\n}\n\nasync function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\tcacheToken?: ExtensionCacheToken,\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });\n\n\ttry {\n\t\tconst factory = await loadExtensionModule(resolvedPath, cacheToken);\n\t\ttime(`${extensionPath} module import`, \"extensions\");\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tawait factory(api);\n\t\ttime(`${extensionPath} factory`, \"extensions\");\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n): Promise<Extension> {\n\tconst extension = createExtension(extensionPath, extensionPath);\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);\n\tawait factory(api);\n\ttime(`${extensionPath} factory`, \"extensions\");\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nasync function loadExtensionsInternal(\n\tpaths: string[],\n\tcwd: string,\n\teventBus?: EventBus,\n\truntime?: ExtensionRuntime,\n\tuseCache = false,\n): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst cacheToken = useCache ? useExtensionCacheCwd(cwd) : undefined;\n\tconst resolvedCwd = cacheToken?.cwd ?? resolvePath(cwd);\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst resolvedRuntime = runtime ?? createExtensionRuntime();\n\n\tfor (const extPath of paths) {\n\t\tconst { extension, error } = await loadExtension(\n\t\t\textPath,\n\t\t\tresolvedCwd,\n\t\t\tresolvedEventBus,\n\t\t\tresolvedRuntime,\n\t\t\tcacheToken,\n\t\t);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime: resolvedRuntime,\n\t};\n}\n\nexport async function loadExtensions(\n\tpaths: string[],\n\tcwd: string,\n\teventBus?: EventBus,\n\truntime?: ExtensionRuntime,\n): Promise<LoadExtensionsResult> {\n\treturn loadExtensionsInternal(paths, cwd, eventBus, runtime);\n}\n\nexport async function loadExtensionsCached(\n\tpaths: string[],\n\tcwd: string,\n\teventBus?: EventBus,\n\truntime?: ExtensionRuntime,\n): Promise<LoadExtensionsResult> {\n\treturn loadExtensionsInternal(paths, cwd, eventBus, runtime, true);\n}\n\ninterface PiManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n}\n\nfunction readPiManifest(packageJsonPath: string): PiManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content);\n\t\tif (pkg.pi && typeof pkg.pi === \"object\") {\n\t\t\treturn pkg.pi as PiManifest;\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"pi.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\t// Check for package.json with \"pi\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readPiManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = path.resolve(dir, extPath);\n\t\t\t\tif (fs.existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with \"pi\" field → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push(entryPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedAgentDir = resolvePath(agentDir);\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/\n\tconst localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 2. Global extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(resolvedAgentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\t// Check for package.json with pi manifest or index.ts\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// No explicit entries - discover individual files in directory\n\t\t\taddPaths(discoverExtensionsInDir(resolved));\n\t\t\tcontinue;\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, resolvedCwd, eventBus);\n}\n"]}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAmBH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAKhE,OAAO,KAAK,EAEX,SAAS,EAET,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EAKpB,MAAM,YAAY,CAAC;AAEpB,KAAK,UAAU,GAAG,cAAc,aAAa,EAAE,UAAU,CAAC;AAE1D,UAAU,gBAAgB;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACrB;AAOD,sEAAsE;AACtE,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAKrE;AAkHD,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnE;AAaD,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAWD;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CA8CzD;AA+QD;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,GACxB,OAAO,CAAC,SAAS,CAAC,CAOpB;AA6CD,wBAAsB,cAAc,CACnC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,QAAQ,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,MAAM,EAAE,EACf,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,QAAQ,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AA+GD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA2C/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension modules using jiti.\n *\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport * as _bundledPiAgentCore from \"@ashx-j/lunr-agent\";\nimport * as _bundledPiAiCompat from \"@ashx-j/lunr-ai/compat\";\nimport * as _bundledPiAiOauth from \"@ashx-j/lunr-ai/oauth\";\nimport type { KeyId } from \"@ashx-j/lunr-tui\";\nimport * as _bundledPiTui from \"@ashx-j/lunr-tui\";\n// Static imports of packages that extensions may use.\n// jiti/static, providers/all, and the public barrel stay off the Node first-paint\n// graph. Bun registers those via host-static.ts (imported from bun/cli.ts).\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { createEventBus, type EventBus } from \"../event-bus.ts\";\nimport type { ExecOptions } from \"../exec.ts\";\nimport { execCommand } from \"../exec.ts\";\nimport { createSyntheticSourceInfo } from \"../source-info.ts\";\nimport { time } from \"../timings.ts\";\nimport type {\n\tEntryRenderer,\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tProviderConfig,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.ts\";\n\ntype CreateJiti = typeof import(\"jiti/static\").createJiti;\n\ninterface BunExtensionHost {\n\tcreateJiti: CreateJiti;\n\tprovidersAll: unknown;\n\tcodingAgent: unknown;\n}\n\nlet bunExtensionHost: BunExtensionHost | undefined;\nlet createJitiFn: CreateJiti | undefined;\nlet lazyProvidersAll: unknown;\nlet lazyCodingAgent: unknown;\n\n/** Register host modules that must stay static for the Bun binary. */\nexport function registerBunExtensionHost(host: BunExtensionHost): void {\n\tbunExtensionHost = host;\n\tcreateJitiFn = host.createJiti;\n\tlazyProvidersAll = host.providersAll;\n\tlazyCodingAgent = host.codingAgent;\n}\n\nfunction scopedHostKeys(earendil: string, ashxj: string, value: unknown): Record<string, unknown> {\n\treturn {\n\t\t[earendil]: value,\n\t\t[ashxj]: value,\n\t};\n}\n\nfunction virtualModules(): Record<string, unknown> {\n\treturn {\n\t\ttypebox: _bundledTypebox,\n\t\t\"typebox/compile\": _bundledTypeboxCompile,\n\t\t\"typebox/value\": _bundledTypeboxValue,\n\t\t\"@sinclair/typebox\": _bundledTypebox,\n\t\t\"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n\t\t\"@sinclair/typebox/value\": _bundledTypeboxValue,\n\t\t...scopedHostKeys(\"@ashx-j/lunr-agent\", \"@ashx-j/lunr-agent\", _bundledPiAgentCore),\n\t\t...scopedHostKeys(\"@ashx-j/lunr-tui\", \"@ashx-j/lunr-tui\", _bundledPiTui),\n\t\t// Extensions resolve the pi-ai root to the compat entrypoint (a strict\n\t\t// superset of the core entrypoint): existing extensions using the old\n\t\t// global API keep working at runtime until compat is removed.\n\t\t...scopedHostKeys(\"@ashx-j/lunr-ai\", \"@ashx-j/lunr-ai\", _bundledPiAiCompat),\n\t\t...scopedHostKeys(\"@ashx-j/lunr-ai/compat\", \"@ashx-j/lunr-ai/compat\", _bundledPiAiCompat),\n\t\t...scopedHostKeys(\"@ashx-j/lunr-ai/oauth\", \"@ashx-j/lunr-ai/oauth\", _bundledPiAiOauth),\n\t\t...scopedHostKeys(\n\t\t\t\"@ashx-j/lunr-ai/providers/all\",\n\t\t\t\"@ashx-j/lunr-ai/providers/all\",\n\t\t\tlazyProvidersAll,\n\t\t),\n\t\t...scopedHostKeys(\"@ashx-j/lunr\", \"@ashx-j/lunr\", lazyCodingAgent),\n\t\t\"@mariozechner/pi-agent-core\": _bundledPiAgentCore,\n\t\t\"@mariozechner/pi-tui\": _bundledPiTui,\n\t\t\"@mariozechner/pi-ai\": _bundledPiAiCompat,\n\t\t\"@mariozechner/pi-ai/compat\": _bundledPiAiCompat,\n\t\t\"@mariozechner/pi-ai/oauth\": _bundledPiAiOauth,\n\t\t\"@mariozechner/pi-ai/providers/all\": lazyProvidersAll,\n\t\t\"@mariozechner/pi-coding-agent\": lazyCodingAgent,\n\t};\n}\n\nconst require = createRequire(import.meta.url);\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nlet _aliases: Record<string, string> | null = null;\n\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\tconst typeboxEntry = require.resolve(\"typebox\");\n\tconst typeboxCompileEntry = require.resolve(\"typebox/compile\");\n\tconst typeboxValueEntry = require.resolve(\"typebox/value\");\n\n\tconst packagesRoot = path.resolve(__dirname, \"../../../../\");\n\tconst resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {\n\t\tconst workspacePath = path.join(packagesRoot, workspaceRelativePath);\n\t\tif (fs.existsSync(workspacePath)) {\n\t\t\treturn workspacePath;\n\t\t}\n\t\treturn fileURLToPath(import.meta.resolve(specifier));\n\t};\n\n\tconst piCodingAgentEntry = packageIndex;\n\tconst piAgentCoreEntry = resolveWorkspaceOrImport(\"agent/dist/index.js\", \"@ashx-j/lunr-agent\");\n\tconst piTuiEntry = resolveWorkspaceOrImport(\"tui/dist/index.js\", \"@ashx-j/lunr-tui\");\n\t// Extensions resolve the pi-ai root to the compat entrypoint (a strict\n\t// superset of the core entrypoint): existing extensions using the old\n\t// global API keep working at runtime until compat is removed.\n\tconst piAiCompatEntry = resolveWorkspaceOrImport(\"ai/dist/compat.js\", \"@ashx-j/lunr-ai/compat\");\n\tconst piAiOauthEntry = resolveWorkspaceOrImport(\"ai/dist/oauth.js\", \"@ashx-j/lunr-ai/oauth\");\n\tconst piAiProvidersEntry = resolveWorkspaceOrImport(\n\t\t\"ai/dist/providers/all.js\",\n\t\t\"@ashx-j/lunr-ai/providers/all\",\n\t);\n\n\t_aliases = {\n\t\t\"@ashx-j/lunr\": piCodingAgentEntry,\n\t\t\"@ashx-j/lunr-agent\": piAgentCoreEntry,\n\t\t\"@ashx-j/lunr-tui\": piTuiEntry,\n\t\t\"@ashx-j/lunr-ai/providers/all\": piAiProvidersEntry,\n\t\t\"@ashx-j/lunr-ai/compat\": piAiCompatEntry,\n\t\t\"@ashx-j/lunr-ai/oauth\": piAiOauthEntry,\n\t\t\"@ashx-j/lunr-ai\": piAiCompatEntry,\n\t\t\"@ashx-j/lunr\": piCodingAgentEntry,\n\t\t\"@ashx-j/lunr-agent\": piAgentCoreEntry,\n\t\t\"@ashx-j/lunr-tui\": piTuiEntry,\n\t\t\"@ashx-j/lunr-ai/providers/all\": piAiProvidersEntry,\n\t\t\"@ashx-j/lunr-ai/compat\": piAiCompatEntry,\n\t\t\"@ashx-j/lunr-ai/oauth\": piAiOauthEntry,\n\t\t\"@ashx-j/lunr-ai\": piAiCompatEntry,\n\t\t\"@mariozechner/pi-coding-agent\": piCodingAgentEntry,\n\t\t\"@mariozechner/pi-agent-core\": piAgentCoreEntry,\n\t\t\"@mariozechner/pi-tui\": piTuiEntry,\n\t\t\"@mariozechner/pi-ai/providers/all\": piAiProvidersEntry,\n\t\t\"@mariozechner/pi-ai/compat\": piAiCompatEntry,\n\t\t\"@mariozechner/pi-ai/oauth\": piAiOauthEntry,\n\t\t\"@mariozechner/pi-ai\": piAiCompatEntry,\n\t\ttypebox: typeboxEntry,\n\t\t\"typebox/compile\": typeboxCompileEntry,\n\t\t\"typebox/value\": typeboxValueEntry,\n\t\t\"@sinclair/typebox\": typeboxEntry,\n\t\t\"@sinclair/typebox/compile\": typeboxCompileEntry,\n\t\t\"@sinclair/typebox/value\": typeboxValueEntry,\n\t};\n\n\treturn _aliases;\n}\n\nexport function getExtensionAliasesForTest(): Record<string, string> {\n\t_aliases = null;\n\treturn getAliases();\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\nlet extensionCacheCwd: string | undefined;\nlet extensionCacheGeneration = 0;\nconst extensionCache = new Map<string, ExtensionFactory>();\n\ninterface ExtensionCacheToken {\n\tcwd: string;\n\tgeneration: number;\n}\n\nexport function clearExtensionCache(): void {\n\textensionCache.clear();\n\textensionCacheCwd = undefined;\n\textensionCacheGeneration++;\n}\n\nfunction useExtensionCacheCwd(cwd: string): ExtensionCacheToken {\n\tconst resolvedCwd = resolvePath(cwd);\n\tif (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) {\n\t\tclearExtensionCache();\n\t}\n\textensionCacheCwd = resolvedCwd;\n\treturn { cwd: resolvedCwd, generation: extensionCacheGeneration };\n}\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.bindCore() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\tconst state: { staleMessage?: string } = {};\n\tconst assertActive = () => {\n\t\tif (state.staleMessage) {\n\t\t\tthrow new Error(state.staleMessage);\n\t\t}\n\t};\n\n\tconst runtime: ExtensionRuntime = {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tsetSessionName: notInitialized,\n\t\tgetSessionName: notInitialized,\n\t\tsetLabel: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\t// registerTool() is valid during extension load; refresh is only needed post-bind.\n\t\trefreshTools: () => {},\n\t\tgetCommands: notInitialized,\n\t\tsetModel: () => Promise.reject(new Error(\"Extension runtime not initialized\")),\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\tflagValues: new Map(),\n\t\tpendingProviderRegistrations: [],\n\t\tassertActive,\n\t\tinvalidate: (message) => {\n\t\t\tstate.staleMessage ??=\n\t\t\t\tmessage ??\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\";\n\t\t},\n\t\t// Pre-bind: queue registrations so bindCore() can flush them once the\n\t\t// model registry is available. bindCore() replaces both with direct calls.\n\t\tregisterProvider: (name, config, extensionPath = \"<unknown>\") => {\n\t\t\truntime.pendingProviderRegistrations.push({ name, config, extensionPath });\n\t\t},\n\t\tunregisterProvider: (name) => {\n\t\t\truntime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);\n\t\t},\n\t};\n\n\treturn runtime;\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\truntime.assertActive();\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\truntime.assertActive();\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t\truntime.refreshTools();\n\t\t},\n\n\t\tregisterCommand(name: string, options: Omit<RegisteredCommand, \"name\" | \"sourceInfo\">): void {\n\t\t\truntime.assertActive();\n\t\t\textension.commands.set(name, {\n\t\t\t\tname,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t...options,\n\t\t\t});\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.ts\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined && !runtime.flagValues.has(name)) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\tregisterEntryRenderer<T>(customType: string, renderer: EntryRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.entryRenderers ??= new Map();\n\t\t\textension.entryRenderers.set(customType, renderer as EntryRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\truntime.assertActive();\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\tsetSessionName(name: string): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setSessionName(name);\n\t\t},\n\n\t\tgetSessionName(): string | undefined {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getSessionName();\n\t\t},\n\n\t\tsetLabel(entryId: string, label: string | undefined): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setLabel(entryId, label);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\truntime.assertActive();\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tgetCommands() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getCommands();\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.assertActive();\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tregisterProvider(name: string, config: ProviderConfig) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerProvider(name, config, extension.path);\n\t\t},\n\n\t\tunregisterProvider(name: string) {\n\t\t\truntime.assertActive();\n\t\t\truntime.unregisterProvider(name, extension.path);\n\t\t},\n\n\t\tevents: eventBus,\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nfunction isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken {\n\treturn (\n\t\tcacheToken !== undefined &&\n\t\textensionCacheCwd === cacheToken.cwd &&\n\t\textensionCacheGeneration === cacheToken.generation\n\t);\n}\n\nasync function ensureExtensionHost(): Promise<CreateJiti> {\n\tif (createJitiFn) return createJitiFn;\n\tif (isBunBinary && bunExtensionHost) {\n\t\treturn bunExtensionHost.createJiti;\n\t}\n\tconst [{ createJiti }, providersAll, codingAgent] = await Promise.all([\n\t\timport(\"jiti/static\"),\n\t\timport(\"@ashx-j/lunr-ai/providers/all\"),\n\t\timport(\"../../index.ts\"),\n\t]);\n\tcreateJitiFn = createJiti;\n\tlazyProvidersAll = providersAll;\n\tlazyCodingAgent = codingAgent;\n\treturn createJiti;\n}\n\nasync function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken) {\n\tif (isCurrentCacheToken(cacheToken)) {\n\t\tconst cachedFactory = extensionCache.get(extensionPath);\n\t\tif (cachedFactory) {\n\t\t\treturn cachedFactory;\n\t\t}\n\t}\n\n\tconst createJiti = await ensureExtensionHost();\n\tconst jiti = createJiti(import.meta.url, {\n\t\tmoduleCache: false,\n\t\t// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)\n\t\t// Also disable tryNative so jiti handles ALL imports (not just the entry point)\n\t\t// In Node.js/dev: use aliases to resolve to node_modules paths\n\t\t...(isBunBinary ? { virtualModules: virtualModules(), tryNative: false } : { alias: getAliases() }),\n\t});\n\n\tconst module = await jiti.import(extensionPath, { default: true });\n\tconst factory = module as ExtensionFactory;\n\tif (typeof factory !== \"function\") {\n\t\treturn undefined;\n\t}\n\tif (isCurrentCacheToken(cacheToken)) {\n\t\textensionCache.set(extensionPath, factory);\n\t}\n\treturn factory;\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\tconst source =\n\t\textensionPath.startsWith(\"<\") && extensionPath.endsWith(\">\")\n\t\t\t? extensionPath.slice(1, -1).split(\":\")[0] || \"temporary\"\n\t\t\t: \"local\";\n\tconst baseDir = extensionPath.startsWith(\"<\") ? undefined : path.dirname(resolvedPath);\n\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\tsourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tentryRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t};\n}\n\nasync function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\tcacheToken?: ExtensionCacheToken,\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });\n\n\ttry {\n\t\tconst factory = await loadExtensionModule(resolvedPath, cacheToken);\n\t\ttime(`${extensionPath} module import`, \"extensions\");\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tawait factory(api);\n\t\ttime(`${extensionPath} factory`, \"extensions\");\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n): Promise<Extension> {\n\tconst extension = createExtension(extensionPath, extensionPath);\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);\n\tawait factory(api);\n\ttime(`${extensionPath} factory`, \"extensions\");\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nasync function loadExtensionsInternal(\n\tpaths: string[],\n\tcwd: string,\n\teventBus?: EventBus,\n\truntime?: ExtensionRuntime,\n\tuseCache = false,\n): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst cacheToken = useCache ? useExtensionCacheCwd(cwd) : undefined;\n\tconst resolvedCwd = cacheToken?.cwd ?? resolvePath(cwd);\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst resolvedRuntime = runtime ?? createExtensionRuntime();\n\n\tfor (const extPath of paths) {\n\t\tconst { extension, error } = await loadExtension(\n\t\t\textPath,\n\t\t\tresolvedCwd,\n\t\t\tresolvedEventBus,\n\t\t\tresolvedRuntime,\n\t\t\tcacheToken,\n\t\t);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime: resolvedRuntime,\n\t};\n}\n\nexport async function loadExtensions(\n\tpaths: string[],\n\tcwd: string,\n\teventBus?: EventBus,\n\truntime?: ExtensionRuntime,\n): Promise<LoadExtensionsResult> {\n\treturn loadExtensionsInternal(paths, cwd, eventBus, runtime);\n}\n\nexport async function loadExtensionsCached(\n\tpaths: string[],\n\tcwd: string,\n\teventBus?: EventBus,\n\truntime?: ExtensionRuntime,\n): Promise<LoadExtensionsResult> {\n\treturn loadExtensionsInternal(paths, cwd, eventBus, runtime, true);\n}\n\ninterface PiManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n}\n\nfunction readPiManifest(packageJsonPath: string): PiManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content);\n\t\tif (pkg.pi && typeof pkg.pi === \"object\") {\n\t\t\treturn pkg.pi as PiManifest;\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"pi.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\t// Check for package.json with \"pi\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readPiManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = path.resolve(dir, extPath);\n\t\t\t\tif (fs.existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with \"pi\" field → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push(entryPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedAgentDir = resolvePath(agentDir);\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/\n\tconst localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 2. Global extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(resolvedAgentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\t// Check for package.json with pi manifest or index.ts\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// No explicit entries - discover individual files in directory\n\t\t\taddPaths(discoverExtensionsInDir(resolved));\n\t\t\tcontinue;\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, resolvedCwd, eventBus);\n}\n"]}
@@ -9,50 +9,63 @@ import { fileURLToPath } from "node:url";
9
9
  import * as _bundledPiAgentCore from "@ashx-j/lunr-agent";
10
10
  import * as _bundledPiAiCompat from "@ashx-j/lunr-ai/compat";
11
11
  import * as _bundledPiAiOauth from "@ashx-j/lunr-ai/oauth";
12
- import * as _bundledPiAiProviders from "@ashx-j/lunr-ai/providers/all";
13
12
  import * as _bundledPiTui from "@ashx-j/lunr-tui";
14
- import { createJiti } from "jiti/static";
15
13
  // Static imports of packages that extensions may use.
16
- // These MUST be static so Bun bundles them into the compiled binary.
17
- // The virtualModules option then makes them available to extensions.
14
+ // jiti/static, providers/all, and the public barrel stay off the Node first-paint
15
+ // graph. Bun registers those via host-static.ts (imported from bun/cli.ts).
18
16
  import * as _bundledTypebox from "typebox";
19
17
  import * as _bundledTypeboxCompile from "typebox/compile";
20
18
  import * as _bundledTypeboxValue from "typebox/value";
21
19
  import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
22
- // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
23
- // avoiding a circular dependency. Extensions can import from @ashx-j/lunr.
24
- import * as _bundledPiCodingAgent from "../../index.js";
25
20
  import { resolvePath } from "../../utils/paths.js";
26
21
  import { createEventBus } from "../event-bus.js";
27
22
  import { execCommand } from "../exec.js";
28
23
  import { createSyntheticSourceInfo } from "../source-info.js";
29
24
  import { time } from "../timings.js";
30
- /** Modules available to extensions via virtualModules (for compiled Bun binary) */
31
- const VIRTUAL_MODULES = {
32
- typebox: _bundledTypebox,
33
- "typebox/compile": _bundledTypeboxCompile,
34
- "typebox/value": _bundledTypeboxValue,
35
- "@sinclair/typebox": _bundledTypebox,
36
- "@sinclair/typebox/compile": _bundledTypeboxCompile,
37
- "@sinclair/typebox/value": _bundledTypeboxValue,
38
- "@ashx-j/lunr-agent": _bundledPiAgentCore,
39
- "@ashx-j/lunr-tui": _bundledPiTui,
40
- // Extensions resolve the pi-ai root to the compat entrypoint (a strict
41
- // superset of the core entrypoint): existing extensions using the old
42
- // global API keep working at runtime until compat is removed.
43
- "@ashx-j/lunr-ai": _bundledPiAiCompat,
44
- "@ashx-j/lunr-ai/compat": _bundledPiAiCompat,
45
- "@ashx-j/lunr-ai/oauth": _bundledPiAiOauth,
46
- "@ashx-j/lunr-ai/providers/all": _bundledPiAiProviders,
47
- "@ashx-j/lunr": _bundledPiCodingAgent,
48
- "@mariozechner/pi-agent-core": _bundledPiAgentCore,
49
- "@mariozechner/pi-tui": _bundledPiTui,
50
- "@mariozechner/pi-ai": _bundledPiAiCompat,
51
- "@mariozechner/pi-ai/compat": _bundledPiAiCompat,
52
- "@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
53
- "@mariozechner/pi-ai/providers/all": _bundledPiAiProviders,
54
- "@mariozechner/pi-coding-agent": _bundledPiCodingAgent,
55
- };
25
+ let bunExtensionHost;
26
+ let createJitiFn;
27
+ let lazyProvidersAll;
28
+ let lazyCodingAgent;
29
+ /** Register host modules that must stay static for the Bun binary. */
30
+ export function registerBunExtensionHost(host) {
31
+ bunExtensionHost = host;
32
+ createJitiFn = host.createJiti;
33
+ lazyProvidersAll = host.providersAll;
34
+ lazyCodingAgent = host.codingAgent;
35
+ }
36
+ function scopedHostKeys(earendil, ashxj, value) {
37
+ return {
38
+ [earendil]: value,
39
+ [ashxj]: value,
40
+ };
41
+ }
42
+ function virtualModules() {
43
+ return {
44
+ typebox: _bundledTypebox,
45
+ "typebox/compile": _bundledTypeboxCompile,
46
+ "typebox/value": _bundledTypeboxValue,
47
+ "@sinclair/typebox": _bundledTypebox,
48
+ "@sinclair/typebox/compile": _bundledTypeboxCompile,
49
+ "@sinclair/typebox/value": _bundledTypeboxValue,
50
+ ...scopedHostKeys("@ashx-j/lunr-agent", "@ashx-j/lunr-agent", _bundledPiAgentCore),
51
+ ...scopedHostKeys("@ashx-j/lunr-tui", "@ashx-j/lunr-tui", _bundledPiTui),
52
+ // Extensions resolve the pi-ai root to the compat entrypoint (a strict
53
+ // superset of the core entrypoint): existing extensions using the old
54
+ // global API keep working at runtime until compat is removed.
55
+ ...scopedHostKeys("@ashx-j/lunr-ai", "@ashx-j/lunr-ai", _bundledPiAiCompat),
56
+ ...scopedHostKeys("@ashx-j/lunr-ai/compat", "@ashx-j/lunr-ai/compat", _bundledPiAiCompat),
57
+ ...scopedHostKeys("@ashx-j/lunr-ai/oauth", "@ashx-j/lunr-ai/oauth", _bundledPiAiOauth),
58
+ ...scopedHostKeys("@ashx-j/lunr-ai/providers/all", "@ashx-j/lunr-ai/providers/all", lazyProvidersAll),
59
+ ...scopedHostKeys("@ashx-j/lunr", "@ashx-j/lunr", lazyCodingAgent),
60
+ "@mariozechner/pi-agent-core": _bundledPiAgentCore,
61
+ "@mariozechner/pi-tui": _bundledPiTui,
62
+ "@mariozechner/pi-ai": _bundledPiAiCompat,
63
+ "@mariozechner/pi-ai/compat": _bundledPiAiCompat,
64
+ "@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
65
+ "@mariozechner/pi-ai/providers/all": lazyProvidersAll,
66
+ "@mariozechner/pi-coding-agent": lazyCodingAgent,
67
+ };
68
+ }
56
69
  const require = createRequire(import.meta.url);
57
70
  /**
58
71
  * Get aliases for jiti (used in Node.js/development mode).
@@ -85,6 +98,13 @@ function getAliases() {
85
98
  const piAiOauthEntry = resolveWorkspaceOrImport("ai/dist/oauth.js", "@ashx-j/lunr-ai/oauth");
86
99
  const piAiProvidersEntry = resolveWorkspaceOrImport("ai/dist/providers/all.js", "@ashx-j/lunr-ai/providers/all");
87
100
  _aliases = {
101
+ "@ashx-j/lunr": piCodingAgentEntry,
102
+ "@ashx-j/lunr-agent": piAgentCoreEntry,
103
+ "@ashx-j/lunr-tui": piTuiEntry,
104
+ "@ashx-j/lunr-ai/providers/all": piAiProvidersEntry,
105
+ "@ashx-j/lunr-ai/compat": piAiCompatEntry,
106
+ "@ashx-j/lunr-ai/oauth": piAiOauthEntry,
107
+ "@ashx-j/lunr-ai": piAiCompatEntry,
88
108
  "@ashx-j/lunr": piCodingAgentEntry,
89
109
  "@ashx-j/lunr-agent": piAgentCoreEntry,
90
110
  "@ashx-j/lunr-tui": piTuiEntry,
@@ -108,6 +128,10 @@ function getAliases() {
108
128
  };
109
129
  return _aliases;
110
130
  }
131
+ export function getExtensionAliasesForTest() {
132
+ _aliases = null;
133
+ return getAliases();
134
+ }
111
135
  let extensionCacheCwd;
112
136
  let extensionCacheGeneration = 0;
113
137
  const extensionCache = new Map();
@@ -304,6 +328,22 @@ function isCurrentCacheToken(cacheToken) {
304
328
  extensionCacheCwd === cacheToken.cwd &&
305
329
  extensionCacheGeneration === cacheToken.generation);
306
330
  }
331
+ async function ensureExtensionHost() {
332
+ if (createJitiFn)
333
+ return createJitiFn;
334
+ if (isBunBinary && bunExtensionHost) {
335
+ return bunExtensionHost.createJiti;
336
+ }
337
+ const [{ createJiti }, providersAll, codingAgent] = await Promise.all([
338
+ import("jiti/static"),
339
+ import("@ashx-j/lunr-ai/providers/all"),
340
+ import("../../index.js"),
341
+ ]);
342
+ createJitiFn = createJiti;
343
+ lazyProvidersAll = providersAll;
344
+ lazyCodingAgent = codingAgent;
345
+ return createJiti;
346
+ }
307
347
  async function loadExtensionModule(extensionPath, cacheToken) {
308
348
  if (isCurrentCacheToken(cacheToken)) {
309
349
  const cachedFactory = extensionCache.get(extensionPath);
@@ -311,12 +351,13 @@ async function loadExtensionModule(extensionPath, cacheToken) {
311
351
  return cachedFactory;
312
352
  }
313
353
  }
354
+ const createJiti = await ensureExtensionHost();
314
355
  const jiti = createJiti(import.meta.url, {
315
356
  moduleCache: false,
316
357
  // In Bun binary: use virtualModules for bundled packages (no filesystem resolution)
317
358
  // Also disable tryNative so jiti handles ALL imports (not just the entry point)
318
359
  // In Node.js/dev: use aliases to resolve to node_modules paths
319
- ...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),
360
+ ...(isBunBinary ? { virtualModules: virtualModules(), tryNative: false } : { alias: getAliases() }),
320
361
  });
321
362
  const module = await jiti.import(extensionPath, { default: true });
322
363
  const factory = module;