@selesai/code 0.8.6 → 0.8.7

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 (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/settings.json +1 -0
  16. package/dist/extensions/model-prompt-injector/config.json +10 -0
  17. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  18. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  19. package/dist/extensions/package.json +1 -0
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  21. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  22. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  23. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  24. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  25. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  26. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  27. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  28. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  29. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  30. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  31. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  32. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  33. package/dist/extensions/pi-subagents/install.mjs +2 -2
  34. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  35. package/dist/extensions/pi-subagents/package.json +1 -1
  36. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  37. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  38. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  39. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  40. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  41. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  42. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  43. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  44. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  45. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  46. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  47. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  48. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  49. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  50. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  51. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  52. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  53. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  54. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  55. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  56. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  57. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  58. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  59. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  60. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  61. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  62. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  63. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  64. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  66. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  67. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  68. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  69. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  70. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
  71. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  72. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  73. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  74. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  75. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  76. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  77. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  78. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  80. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  81. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  82. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  83. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  84. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  85. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  86. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  88. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  89. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  90. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  96. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  97. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  98. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  99. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  100. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  101. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  102. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  104. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  105. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  106. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  107. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  108. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  109. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  110. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  111. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  112. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  113. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  114. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  115. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  116. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  117. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  118. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  119. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  120. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  121. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  122. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  123. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  124. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  125. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  126. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  127. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  128. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  129. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  130. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  131. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  133. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  134. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  135. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  136. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  137. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  138. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  139. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  140. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  141. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  142. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  143. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  144. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  145. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  146. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  147. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  148. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  149. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  150. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  151. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  152. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  153. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  154. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  155. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  156. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  157. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  158. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  159. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  160. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  161. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  162. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  163. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  164. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  165. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  166. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  167. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  168. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  169. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  170. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  171. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  172. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  173. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  174. package/dist/extensions/question/index.ts +21 -6
  175. package/dist/extensions/question/question-list.ts +7 -1
  176. package/dist/extensions/question/row-layout.ts +3 -0
  177. package/dist/extensions/question/tests/exp.test.ts +70 -0
  178. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  179. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  180. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  181. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  182. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  183. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  184. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  185. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  186. package/dist/skills/pi-subagents/SKILL.md +1 -1
  187. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  188. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  189. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  190. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  191. package/docs/settings.md +6 -0
  192. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.7] - 2026-08-17
8
+
9
+ ### Added
10
+ - **Pi Subagents updated through upstream 0.50.0.** The bundled extension advances through upstream 0.49/0.50 (single-child `{ agent, task }` runs, per-tool-call `toolTimeoutMs` with config/env precedence, `debug.run` inspection, builtin role overrides with `tools: "inherit"`, resumable retained-child listings, indexed async result inboxes) with the Selesai fork layers on top: branding, `.selesai` config routing, `SELESAI_SUBAGENT*` env vars, declarative `chain`/`tasks`/`clarify` modes, workflow auto-relaunch wiring, and same-model retry for transient provider errors.
11
+ - **`model-prompt-injector` extension.** Bundled and registered in the extensions manifest, it injects model-specific prompts into the system prompt when the active model matches a configured rule (provider/model globs, bare id/name patterns, first-match-wins, enabled toggle). Modes: `prepend`, `append`, or `replace`.
12
+ - **Wire-level tool schema pruning.** `pruneToolDescriptions` (default `true`) strips every per-field `description` from tool input schemas before they reach the model, keeping one-line tool descriptions. Stripped schemas are memoized and never mutated; UI/docs definitions keep full descriptions; `keepParameterDescriptions` opts a tool out.
13
+ - **Compact subagent tool description by default.** `toolDescriptionMode` now defaults to `"compact"` (~3.2 KB vs ~6.3 KB per model turn), and a missing/invalid custom description file falls back to compact.
14
+
15
+ ### Changed
16
+ - **Slimmer default system prompt.** The always-on Pi-docs block shrank from 8 lines to 2 (paths only); docs are still read on demand when the user asks about pi.
17
+ - **Bundled defaults** now set `pruneToolDescriptions: true`.
18
+ - **Test coverage.** `src/extensions` (pi-* excluded) now enforces 100% stmt/branch/func/line coverage via vitest thresholds; `safeMaxTokens()` clamps missing/oversized context-window metadata to a sane default.
19
+
20
+ ### Fixed
21
+ - **Subagent prompt-runtime suite passes inside subagent child environments.** The watchdog-lifecycle test now scrubs the steer env vars (`SELESAI_SUBAGENT_STEER_*`) before registering, so the steering inbox's extra `agent_end` handler cannot skew the handler count.
22
+
7
23
  ## [0.8.6] - 2026-08-15
8
24
 
9
25
  ### Added
@@ -35,7 +35,7 @@ import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-man
35
35
  import { createSyntheticSourceInfo } from "./source-info.js";
36
36
  import { buildSystemPrompt } from "./system-prompt.js";
37
37
  import { createLocalBashOperations } from "./tools/bash.js";
38
- import { createAllToolDefinitions } from "./tools/index.js";
38
+ import { createAllToolDefinitions, stripToolParameterDescriptions } from "./tools/index.js";
39
39
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
40
40
  import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
41
41
  /** Parse consecutive skill blocks at the start of a user message. */
@@ -2056,6 +2056,18 @@ export class AgentSession {
2056
2056
  toolRegistry.set(tool.name, tool);
2057
2057
  }
2058
2058
  this._toolRegistry = toolRegistry;
2059
+ if (this.settingsManager.getPruneToolDescriptions()) {
2060
+ const keepDescriptions = new Set();
2061
+ for (const [name, entry] of definitionRegistry) {
2062
+ if (entry.definition.keepParameterDescriptions)
2063
+ keepDescriptions.add(name);
2064
+ }
2065
+ for (const [name, tool] of toolRegistry) {
2066
+ if (!keepDescriptions.has(name)) {
2067
+ toolRegistry.set(name, stripToolParameterDescriptions(tool));
2068
+ }
2069
+ }
2070
+ }
2059
2071
  const nextActiveToolNames = (options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]).filter((name) => isAllowedTool(name));
2060
2072
  if (allowedToolNames) {
2061
2073
  for (const toolName of this._toolRegistry.keys()) {
@@ -353,6 +353,8 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
353
353
  promptGuidelines?: string[];
354
354
  /** Parameter schema (TypeBox) */
355
355
  parameters: TParams;
356
+ /** When global pruneToolDescriptions is enabled, keep this tool's per-field parameter descriptions on the wire (already pruned by the tool itself). */
357
+ keepParameterDescriptions?: boolean;
356
358
  /** Optional provider-side constrained sampling request for this tool. Set false to explicitly disable it, equivalent to leaving it undefined. */
357
359
  constrainedSampling?: false | ConstrainedSamplingConfig;
358
360
  /** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */
@@ -2,6 +2,17 @@ import { lazyStream, } from "@earendil-works/pi-ai";
2
2
  import { getApiProvider } from "@earendil-works/pi-ai/compat";
3
3
  import { clearConfigValueCache, getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, resolveConfigValueOrThrow, resolveHeadersOrThrow, } from "./resolve-config-value.js";
4
4
  export const clearApiKeyCache = clearConfigValueCache;
5
+ /** Sane default when a model definition omits maxTokens (kept small so providers never reject it). */
6
+ const DEFAULT_MAX_TOKENS = 16384;
7
+ /**
8
+ * Resolve a safe max output token count. Missing or non-positive values fall back
9
+ * (to a sane default or an inherited value); explicit values are never allowed to
10
+ * exceed the model's context window.
11
+ */
12
+ function safeMaxTokens(value, contextWindow, fallback = DEFAULT_MAX_TOKENS) {
13
+ const resolved = typeof value === "number" && value > 0 ? value : fallback;
14
+ return Math.min(resolved, Math.max(contextWindow, 1));
15
+ }
5
16
  function mergeCompat(base, override) {
6
17
  if (!override)
7
18
  return base;
@@ -20,6 +31,7 @@ function mergeCompat(base, override) {
20
31
  return merged;
21
32
  }
22
33
  function applyModelOverride(model, override) {
34
+ const contextWindow = override.contextWindow ?? model.contextWindow;
23
35
  return {
24
36
  ...model,
25
37
  name: override.name ?? model.name,
@@ -37,8 +49,8 @@ function applyModelOverride(model, override) {
37
49
  tiers: override.cost.tiers ?? model.cost.tiers,
38
50
  }
39
51
  : model.cost,
40
- contextWindow: override.contextWindow ?? model.contextWindow,
41
- maxTokens: override.maxTokens ?? model.maxTokens,
52
+ contextWindow,
53
+ maxTokens: safeMaxTokens(override.maxTokens ?? model.maxTokens, contextWindow, model.maxTokens),
42
54
  compat: mergeCompat(model.compat, override.compat),
43
55
  };
44
56
  }
@@ -56,6 +68,7 @@ function modelFromJson(providerId, definition, providerConfig, defaults) {
56
68
  if (definition.maxTokens !== undefined && definition.maxTokens <= 0) {
57
69
  throw new Error(`Provider ${providerId}, model ${definition.id}: invalid maxTokens`);
58
70
  }
71
+ const contextWindow = definition.contextWindow ?? 128000;
59
72
  return {
60
73
  id: definition.id,
61
74
  name: definition.name ?? definition.id,
@@ -66,8 +79,8 @@ function modelFromJson(providerId, definition, providerConfig, defaults) {
66
79
  thinkingLevelMap: definition.thinkingLevelMap,
67
80
  input: (definition.input ?? ["text"]),
68
81
  cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
69
- contextWindow: definition.contextWindow ?? 128000,
70
- maxTokens: definition.maxTokens ?? 16384,
82
+ contextWindow,
83
+ maxTokens: safeMaxTokens(definition.maxTokens, contextWindow),
71
84
  headers: undefined,
72
85
  compat: mergeCompat(providerConfig.compat, definition.compat),
73
86
  };
@@ -77,6 +77,7 @@ export interface Settings {
77
77
  branchSummary?: BranchSummarySettings;
78
78
  retry?: RetrySettings;
79
79
  hideThinkingBlock?: boolean;
80
+ pruneToolDescriptions?: boolean;
80
81
  showCacheMissNotices?: boolean;
81
82
  externalEditor?: string;
82
83
  shellPath?: string;
@@ -236,6 +237,7 @@ export declare class SettingsManager {
236
237
  };
237
238
  getWebSocketConnectTimeoutMs(): number | undefined;
238
239
  getHideThinkingBlock(): boolean;
240
+ getPruneToolDescriptions(): boolean;
239
241
  getShowCacheMissNotices(): boolean;
240
242
  getExternalEditorCommand(): string;
241
243
  setHideThinkingBlock(hide: boolean): void;
@@ -598,6 +598,9 @@ export class SettingsManager {
598
598
  getHideThinkingBlock() {
599
599
  return this.settings.hideThinkingBlock ?? false;
600
600
  }
601
+ getPruneToolDescriptions() {
602
+ return this.settings.pruneToolDescriptions ?? true;
603
+ }
601
604
  getShowCacheMissNotices() {
602
605
  return this.settings.showCacheMissNotices ?? false;
603
606
  }
@@ -103,14 +103,8 @@ In addition to the tools above, you may have access to other custom tools depend
103
103
  Guidelines:
104
104
  ${guidelines}
105
105
 
106
- Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
107
- - Main documentation: ${readmePath}
108
- - Additional docs: ${docsPath}
109
- - Examples: ${examplesPath} (extensions, custom tools, SDK)
110
- - When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
111
- - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)
112
- - When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
113
- - Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
106
+ Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI): main docs ${readmePath}, additional docs ${docsPath}, examples ${examplesPath}.
107
+ When working on pi topics, resolve docs/... and examples/... under those paths (not cwd), read the relevant .md files completely, and follow cross-references before implementing.`;
114
108
  if (appendSection) {
115
109
  prompt += appendSection;
116
110
  }
@@ -12,6 +12,12 @@ describe("buildSystemPrompt", () => {
12
12
  expect(prompt).toMatch(/Guidelines:/);
13
13
  expect(prompt).toContain("Current working directory: /tmp/project");
14
14
  });
15
+ it("keeps the collapsed Pi-docs block without the per-topic filename map", () => {
16
+ const prompt = buildSystemPrompt({ cwd: "/tmp/project" });
17
+ expect(prompt).toContain("Pi documentation");
18
+ expect(prompt).not.toContain("When asked about:");
19
+ expect(prompt).not.toContain("(docs/extensions.md");
20
+ });
15
21
  it("keeps the default tool snippets, skills, and agents XML read-gated", () => {
16
22
  const skill = {
17
23
  name: "test-skill",
@@ -5,6 +5,7 @@ export { createFindTool, createFindToolDefinition, type FindOperations, type Fin
5
5
  export { createGrepTool, createGrepToolDefinition, type GrepOperations, type GrepToolDetails, type GrepToolInput, type GrepToolOptions, } from "./grep.ts";
6
6
  export { createLsTool, createLsToolDefinition, type LsOperations, type LsToolDetails, type LsToolInput, type LsToolOptions, } from "./ls.ts";
7
7
  export { createReadTool, createReadToolDefinition, type ReadOperations, type ReadToolDetails, type ReadToolInput, type ReadToolOptions, } from "./read.ts";
8
+ export { stripSchemaDescriptions, stripToolParameterDescriptions, } from "./schema-prune.ts";
8
9
  export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationOptions, type TruncationResult, truncateHead, truncateLine, truncateTail, } from "./truncate.ts";
9
10
  export { createWriteTool, createWriteToolDefinition, type WriteOperations, type WriteToolInput, type WriteToolOptions, } from "./write.ts";
10
11
  import type { AgentTool } from "@earendil-works/pi-agent-core";
@@ -5,6 +5,7 @@ export { createFindTool, createFindToolDefinition, } from "./find.js";
5
5
  export { createGrepTool, createGrepToolDefinition, } from "./grep.js";
6
6
  export { createLsTool, createLsToolDefinition, } from "./ls.js";
7
7
  export { createReadTool, createReadToolDefinition, } from "./read.js";
8
+ export { stripSchemaDescriptions, stripToolParameterDescriptions, } from "./schema-prune.js";
8
9
  export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, truncateLine, truncateTail, } from "./truncate.js";
9
10
  export { createWriteTool, createWriteToolDefinition, } from "./write.js";
10
11
  import { createBashTool, createBashToolDefinition } from "./bash.js";
@@ -0,0 +1,16 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import type { TSchema } from "typebox";
3
+ /**
4
+ * Structural deep clone that omits every `description` key at any depth while
5
+ * preserving all other own keys (including TypeBox internals such as `~kind`
6
+ * and `~optional` symbols and function-valued keys). Uses Reflect.ownKeys +
7
+ * property descriptors so non-enumerable metadata survives; never mutates the
8
+ * input.
9
+ */
10
+ export declare function stripSchemaDescriptions<T>(value: T): T;
11
+ /**
12
+ * Returns a shallow copy of the tool with its parameter schema descriptions
13
+ * stripped. Top-level tool fields (name, description, execute, ...) are
14
+ * untouched.
15
+ */
16
+ export declare function stripToolParameterDescriptions<T extends TSchema>(tool: AgentTool<T>): AgentTool<T>;
@@ -0,0 +1,39 @@
1
+ // Memoized stripped clones, keyed by the original schema root. The input is
2
+ // never mutated; repeat calls return the same cached clone.
3
+ const strippedSchemaCache = new WeakMap();
4
+ /**
5
+ * Structural deep clone that omits every `description` key at any depth while
6
+ * preserving all other own keys (including TypeBox internals such as `~kind`
7
+ * and `~optional` symbols and function-valued keys). Uses Reflect.ownKeys +
8
+ * property descriptors so non-enumerable metadata survives; never mutates the
9
+ * input.
10
+ */
11
+ export function stripSchemaDescriptions(value) {
12
+ if (!value || typeof value !== "object")
13
+ return value;
14
+ const cached = strippedSchemaCache.get(value);
15
+ if (cached !== undefined)
16
+ return cached;
17
+ const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
18
+ for (const key of Reflect.ownKeys(value)) {
19
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
20
+ if (!descriptor)
21
+ continue;
22
+ if (key === "description")
23
+ continue;
24
+ if ("value" in descriptor) {
25
+ descriptor.value = stripSchemaDescriptions(descriptor.value);
26
+ }
27
+ Object.defineProperty(result, key, descriptor);
28
+ }
29
+ strippedSchemaCache.set(value, result);
30
+ return result;
31
+ }
32
+ /**
33
+ * Returns a shallow copy of the tool with its parameter schema descriptions
34
+ * stripped. Top-level tool fields (name, description, execute, ...) are
35
+ * untouched.
36
+ */
37
+ export function stripToolParameterDescriptions(tool) {
38
+ return { ...tool, parameters: stripSchemaDescriptions(tool.parameters) };
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Type } from "typebox";
3
+ import { Value } from "typebox/value";
4
+ import { SettingsManager } from "../settings-manager.js";
5
+ import { stripSchemaDescriptions, stripToolParameterDescriptions } from "./schema-prune.js";
6
+ describe("stripSchemaDescriptions", () => {
7
+ it("removes description keys at all depths (properties, anyOf arrays, items)", () => {
8
+ const schema = {
9
+ type: "object",
10
+ description: "root",
11
+ properties: {
12
+ name: { type: "string", description: "the name" },
13
+ tags: {
14
+ type: "array",
15
+ description: "the tags",
16
+ items: { type: "string", description: "a tag" },
17
+ },
18
+ choice: {
19
+ anyOf: [{ type: "string", description: "string branch" }, { type: "number" }],
20
+ },
21
+ },
22
+ };
23
+ const stripped = stripSchemaDescriptions(schema);
24
+ expect("description" in stripped).toBe(false);
25
+ expect("description" in stripped.properties).toBe(false);
26
+ expect("description" in stripped.properties.tags).toBe(false);
27
+ expect("description" in stripped.properties.tags.items).toBe(false);
28
+ expect("description" in stripped.properties.choice.anyOf[0]).toBe(false);
29
+ });
30
+ it("preserves non-description keys such as type, enum, required, minimum, additionalProperties", () => {
31
+ const schema = {
32
+ type: "object",
33
+ properties: {
34
+ level: { type: "string", enum: ["a", "b"], description: "level desc" },
35
+ count: { type: "integer", minimum: 1, description: "count desc" },
36
+ },
37
+ required: ["level"],
38
+ additionalProperties: false,
39
+ };
40
+ const stripped = stripSchemaDescriptions(schema);
41
+ expect(stripped).toEqual({
42
+ type: "object",
43
+ properties: {
44
+ level: { type: "string", enum: ["a", "b"] },
45
+ count: { type: "integer", minimum: 1 },
46
+ },
47
+ required: ["level"],
48
+ additionalProperties: false,
49
+ });
50
+ });
51
+ it("never mutates the input schema", () => {
52
+ const schema = {
53
+ type: "object",
54
+ properties: { name: { type: "string", description: "the name" } },
55
+ };
56
+ const snapshot = structuredClone(schema);
57
+ stripSchemaDescriptions(schema);
58
+ expect(schema).toEqual(snapshot);
59
+ expect(schema.properties.name.description).toBe("the name");
60
+ });
61
+ it("memoizes stripped clones per original root", () => {
62
+ const schema = {
63
+ type: "object",
64
+ properties: { name: { type: "string", description: "the name" } },
65
+ };
66
+ const first = stripSchemaDescriptions(schema);
67
+ const second = stripSchemaDescriptions(schema);
68
+ expect(first).toBe(second);
69
+ });
70
+ it("keeps real TypeBox schemas valid after stripping (symbols survive)", () => {
71
+ const schema = Type.Object({
72
+ name: Type.String({ description: "the name" }),
73
+ age: Type.Optional(Type.Integer({ minimum: 0, description: "the age" })),
74
+ });
75
+ const stripped = stripSchemaDescriptions(schema);
76
+ expect(Value.Check(schema, { name: "pi", age: 3 })).toBe(true);
77
+ expect(Value.Check(stripped, { name: "pi", age: 3 })).toBe(true);
78
+ expect(Value.Check(stripped, { name: "pi", age: -1 })).toBe(false);
79
+ });
80
+ });
81
+ describe("stripToolParameterDescriptions", () => {
82
+ it("keeps the tool description and name while stripping parameter descriptions", () => {
83
+ const tool = {
84
+ name: "read",
85
+ label: "Read",
86
+ description: "Read a file",
87
+ parameters: {
88
+ type: "object",
89
+ properties: { path: { type: "string", description: "file path" } },
90
+ },
91
+ execute: () => Promise.resolve({ content: [], details: {} }),
92
+ };
93
+ const stripped = stripToolParameterDescriptions(tool);
94
+ expect(stripped).not.toBe(tool);
95
+ expect(stripped.name).toBe("read");
96
+ expect(stripped.description).toBe("Read a file");
97
+ expect("description" in stripped.parameters.properties.path).toBe(false);
98
+ // Original tool is untouched.
99
+ expect("description" in tool.parameters.properties.path).toBe(true);
100
+ });
101
+ });
102
+ describe("pruneToolDescriptions setting", () => {
103
+ it("defaults to true", () => {
104
+ const manager = SettingsManager.inMemory();
105
+ expect(manager.getPruneToolDescriptions()).toBe(true);
106
+ });
107
+ it("respects explicit true and false", () => {
108
+ expect(SettingsManager.inMemory({ pruneToolDescriptions: true }).getPruneToolDescriptions()).toBe(true);
109
+ expect(SettingsManager.inMemory({ pruneToolDescriptions: false }).getPruneToolDescriptions()).toBe(false);
110
+ });
111
+ });
@@ -25,6 +25,7 @@
25
25
  "git:github.com/MasuRii/pi-tool-display",
26
26
  "git:github.com/omaclaren/pi-markdown-preview"
27
27
  ],
28
+ "pruneToolDescriptions": true,
28
29
  "quietStartup": true,
29
30
  "retry": {
30
31
  "enabled": true,
@@ -0,0 +1,10 @@
1
+ {
2
+ "rules": [
3
+ {
4
+ "match": ["deepseek-v4-pro"],
5
+ "mode": "prepend",
6
+ "prompt": "text: You are a helpful software engineer assistant.\n\ndescription: Bootstrap with shell/read, then expose the full Standard tool catalog after the first durable tool call.\n\nwhen you thought, start with `we need...`",
7
+ "enabled": true
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { ExtensionAPI } from "@selesai/code";
3
+ import modelPromptInjector, {
4
+ findRule,
5
+ patternMatches,
6
+ type InjectConfig,
7
+ type InjectRule,
8
+ type ModelRef,
9
+ } from "./index.ts";
10
+
11
+ const model = (provider: string, id: string, name = id): ModelRef => ({ provider, id, name });
12
+
13
+ function harness(config: InjectConfig, activeModel: ModelRef | undefined) {
14
+ const handlers: Record<string, Function> = {};
15
+ const pi = {
16
+ on: vi.fn((event: string, handler: Function) => {
17
+ handlers[event] = handler;
18
+ }),
19
+ } as unknown as ExtensionAPI;
20
+ const ctx = {
21
+ model: activeModel,
22
+ ui: { setStatus: vi.fn() },
23
+ };
24
+ modelPromptInjector(pi, config);
25
+ return { handlers, ctx };
26
+ }
27
+
28
+ describe("patternMatches", () => {
29
+ it("matches provider/model exactly, case-insensitively", () => {
30
+ expect(patternMatches("deepseek/deepseek-v4-pro", model("DeepSeek", "deepseek-v4-pro"))).toBe(true);
31
+ expect(patternMatches("deepseek/deepseek-v4-pro", model("openrouter", "deepseek-v4-pro"))).toBe(false);
32
+ });
33
+
34
+ it("supports wildcards in provider and id", () => {
35
+ expect(patternMatches("*/deepseek-v4-pro", model("openrouter", "deepseek-v4-pro"))).toBe(true);
36
+ expect(patternMatches("deepseek/*", model("deepseek", "deepseek-v4-pro"))).toBe(true);
37
+ expect(patternMatches("deepseek/*", model("deepseek", "deepseek-chat"))).toBe(true);
38
+ expect(patternMatches("deepseek/*", model("openai", "gpt-5"))).toBe(false);
39
+ });
40
+
41
+ it("bare patterns match id or display name", () => {
42
+ expect(patternMatches("deepseek-v4-pro", model("deepseek", "deepseek-v4-pro"))).toBe(true);
43
+ expect(patternMatches("gpt-*", model("openai", "gpt-5", "GPT-5"))).toBe(true);
44
+ expect(patternMatches("deepseek*", model("deepseek", "deepseek-v4-pro"))).toBe(true);
45
+ });
46
+
47
+ it("matches * against any model", () => {
48
+ expect(patternMatches("*", model("openai", "gpt-5"))).toBe(true);
49
+ });
50
+ });
51
+
52
+ describe("findRule", () => {
53
+ const rules: InjectRule[] = [
54
+ { match: ["deepseek/*"], prompt: "A" },
55
+ { match: ["*/deepseek-v4-pro"], prompt: "B" },
56
+ { match: ["*"], prompt: "C", enabled: false },
57
+ ];
58
+
59
+ it("first matching rule wins", () => {
60
+ expect(findRule(rules, model("deepseek", "deepseek-v4-pro"))?.prompt).toBe("A");
61
+ expect(findRule(rules, model("openrouter", "deepseek-v4-pro"))?.prompt).toBe("B");
62
+ });
63
+
64
+ it("skips disabled rules", () => {
65
+ expect(findRule([{ match: ["*"], prompt: "C", enabled: false }], model("openai", "gpt-5"))).toBeUndefined();
66
+ });
67
+ });
68
+
69
+ describe("before_agent_start", () => {
70
+ it("appends the rule prompt to the system prompt", async () => {
71
+ const { handlers, ctx } = harness({ rules: [{ match: ["deepseek/*"], prompt: "EXTRA" }] }, model("deepseek", "deepseek-chat"));
72
+ const result = await handlers["before_agent_start"]({ systemPrompt: "BASE" }, ctx);
73
+ expect(result).toEqual({ systemPrompt: "BASE\n\nEXTRA" });
74
+ });
75
+
76
+ it("prepends the rule prompt to the top of the system prompt", async () => {
77
+ const { handlers, ctx } = harness(
78
+ { rules: [{ match: ["deepseek/*"], prompt: "TOP", mode: "prepend" }] },
79
+ model("deepseek", "deepseek-chat"),
80
+ );
81
+ const result = await handlers["before_agent_start"]({ systemPrompt: "BASE" }, ctx);
82
+ expect(result).toEqual({ systemPrompt: "TOP\n\nBASE" });
83
+ });
84
+
85
+ it("replaces the system prompt in replace mode", async () => {
86
+ const { handlers, ctx } = harness(
87
+ { rules: [{ match: ["deepseek/*"], prompt: "ONLY ME", mode: "replace" }] },
88
+ model("deepseek", "deepseek-chat"),
89
+ );
90
+ const result = await handlers["before_agent_start"]({ systemPrompt: "BASE" }, ctx);
91
+ expect(result).toEqual({ systemPrompt: "ONLY ME" });
92
+ });
93
+
94
+ it("returns undefined when no rule matches", async () => {
95
+ const { handlers, ctx } = harness(
96
+ { rules: [{ match: ["deepseek/*"], prompt: "EXTRA" }] },
97
+ model("openai", "gpt-5"),
98
+ );
99
+ const result = await handlers["before_agent_start"]({ systemPrompt: "BASE" }, ctx);
100
+ expect(result).toBeUndefined();
101
+ });
102
+ });
103
+
104
+ describe("model_select", () => {
105
+ it("shows status when the selected model matches, clears otherwise", async () => {
106
+ const { handlers, ctx } = harness({ rules: [{ match: ["deepseek/*"], prompt: "EXTRA" }] }, undefined);
107
+ await handlers["model_select"]({ model: model("deepseek", "deepseek-chat") }, ctx);
108
+ expect(ctx.ui.setStatus).toHaveBeenLastCalledWith("model-prompt-injector", "prompt-inject: active");
109
+ await handlers["model_select"]({ model: model("openai", "gpt-5") }, ctx);
110
+ expect(ctx.ui.setStatus).toHaveBeenLastCalledWith("model-prompt-injector", undefined);
111
+ });
112
+ });
@@ -0,0 +1,151 @@
1
+ /**
2
+ * model-prompt-injector
3
+ *
4
+ * Injects a model-specific prompt into the system prompt whenever the active
5
+ * model matches a rule in config.json (sibling of this file).
6
+ *
7
+ * Config lives only here in host code — nothing is exposed to user settings.
8
+ * `/reload` re-reads it.
9
+ *
10
+ * config.json format:
11
+ * {
12
+ * "rules": [
13
+ * {
14
+ * "match": ["provider/model", "provider/*", "<any>/model", "bare-name", "*"],
15
+ * "mode": "prepend" | "append" | "replace", // default: "append"
16
+ * "prompt": "...",
17
+ * "enabled": true // default: true
18
+ * }
19
+ * ]
20
+ * }
21
+ *
22
+ * Matching (case-insensitive, "*" is a wildcard):
23
+ * - "provider/model" — glob against `<provider>/<id>`.
24
+ * - bare pattern (no "/") — glob against model id OR display name.
25
+ * - First matching rule wins; later rules are ignored.
26
+ * - mode "prepend" puts the prompt at the very top of the system prompt;
27
+ * mode "append" adds it to the end; mode "replace" replaces the whole system prompt.
28
+ */
29
+ import { readFileSync } from "node:fs";
30
+ import { dirname, join } from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+ import type { ExtensionAPI } from "@selesai/code";
33
+
34
+ export type InjectMode = "prepend" | "append" | "replace";
35
+
36
+ export interface InjectRule {
37
+ match: string[];
38
+ prompt: string;
39
+ mode?: InjectMode;
40
+ enabled?: boolean;
41
+ }
42
+
43
+ export interface InjectConfig {
44
+ rules: InjectRule[];
45
+ }
46
+
47
+ /** Model shape needed for matching (structurally compatible with pi's Model). */
48
+ export interface ModelRef {
49
+ provider: string;
50
+ id: string;
51
+ name: string;
52
+ }
53
+
54
+ const CONFIG_PATH = join(dirname(fileURLToPath(import.meta.url)), "config.json");
55
+
56
+ function globToRegex(glob: string): RegExp {
57
+ const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
58
+ return new RegExp(`^${escaped}$`, "i");
59
+ }
60
+
61
+ export function patternMatches(pattern: string, model: ModelRef): boolean {
62
+ const slash = pattern.indexOf("/");
63
+ if (slash >= 0) {
64
+ return (
65
+ globToRegex(pattern.slice(0, slash)).test(model.provider) &&
66
+ globToRegex(pattern.slice(slash + 1)).test(model.id)
67
+ );
68
+ }
69
+ return globToRegex(pattern).test(model.id) || globToRegex(pattern).test(model.name);
70
+ }
71
+
72
+ export function findRule(rules: readonly InjectRule[], model: ModelRef): InjectRule | undefined {
73
+ for (const rule of rules) {
74
+ if (rule.enabled === false) continue;
75
+ if (rule.match.some((pattern) => patternMatches(pattern, model))) return rule;
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ export function loadConfig(path: string = CONFIG_PATH): InjectConfig {
81
+ const fail = (message: string): InjectConfig => {
82
+ console.error(`[model-prompt-injector] ${message}`);
83
+ return { rules: [] };
84
+ };
85
+
86
+ let raw: unknown;
87
+ try {
88
+ raw = JSON.parse(readFileSync(path, "utf8"));
89
+ } catch (error) {
90
+ return fail(`cannot load ${path}: ${(error as Error).message}`);
91
+ }
92
+
93
+ if (typeof raw !== "object" || raw === null || !Array.isArray((raw as { rules?: unknown }).rules)) {
94
+ return fail(`invalid config: expected { "rules": [...] }`);
95
+ }
96
+
97
+ const rules: InjectRule[] = [];
98
+ for (const [index, entry] of (raw as { rules: unknown[] }).rules.entries()) {
99
+ const label = `rule #${index + 1}`;
100
+ if (typeof entry !== "object" || entry === null) {
101
+ console.error(`[model-prompt-injector] ${label} is not an object — skipped`);
102
+ continue;
103
+ }
104
+ const rule = entry as Partial<InjectRule>;
105
+ if (!Array.isArray(rule.match) || rule.match.length === 0 || !rule.match.every((m) => typeof m === "string")) {
106
+ console.error(`[model-prompt-injector] ${label} needs a non-empty "match" string array — skipped`);
107
+ continue;
108
+ }
109
+ if (typeof rule.prompt !== "string" || rule.prompt.trim() === "") {
110
+ console.error(`[model-prompt-injector] ${label} needs a non-empty "prompt" string — skipped`);
111
+ continue;
112
+ }
113
+ if (rule.mode !== undefined && rule.mode !== "prepend" && rule.mode !== "append" && rule.mode !== "replace") {
114
+ console.error(`[model-prompt-injector] ${label}: "mode" must be "prepend", "append" or "replace" — skipped`);
115
+ continue;
116
+ }
117
+ rules.push({
118
+ match: rule.match,
119
+ prompt: rule.prompt,
120
+ mode: rule.mode ?? "append",
121
+ enabled: rule.enabled ?? true,
122
+ });
123
+ }
124
+ return { rules };
125
+ }
126
+
127
+ export default function modelPromptInjector(pi: ExtensionAPI, config: InjectConfig = loadConfig()) {
128
+ const ruleFor = (model: ModelRef | undefined): InjectRule | undefined =>
129
+ model ? findRule(config.rules, model) : undefined;
130
+
131
+ const active = config.rules.filter((rule) => rule.enabled !== false);
132
+ if (active.length > 0) {
133
+ console.log(
134
+ `[model-prompt-injector] ${active.length} rule(s) active: ${active
135
+ .map((rule) => rule.match.join(","))
136
+ .join(" | ")}`,
137
+ );
138
+ }
139
+
140
+ pi.on("before_agent_start", async (event, ctx) => {
141
+ const rule = ruleFor(ctx.model);
142
+ if (!rule) return;
143
+ if (rule.mode === "replace") return { systemPrompt: rule.prompt };
144
+ if (rule.mode === "prepend") return { systemPrompt: `${rule.prompt}\n\n${event.systemPrompt}` };
145
+ return { systemPrompt: `${event.systemPrompt}\n\n${rule.prompt}` };
146
+ });
147
+
148
+ pi.on("model_select", async (event, ctx) => {
149
+ ctx.ui.setStatus("model-prompt-injector", ruleFor(event.model) ? "prompt-inject: active" : undefined);
150
+ });
151
+ }