one-code-extension 0.1.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +222 -177
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +128 -0
  8. package/docs/guide/configuration.md +173 -0
  9. package/docs/guide/differences-from-claude-code.md +152 -0
  10. package/docs/guide/doctor.md +88 -0
  11. package/docs/guide/hooks.md +111 -0
  12. package/docs/guide/installation.md +126 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
  14. package/docs/guide/providers-and-models.md +196 -0
  15. package/docs/guide/reference.md +226 -0
  16. package/docs/guide/sessions-and-context.md +192 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +178 -0
  18. package/docs/guide/subagents-and-workflows.md +195 -0
  19. package/docs/guide/tasks-and-background-work.md +113 -0
  20. package/docs/guide/terminal-interface.md +157 -0
  21. package/docs/guide/tools.md +177 -0
  22. package/docs/guide/troubleshooting.md +177 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -217
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. package/extensions/workflow/permission-gate.ts +0 -71
@@ -14,14 +14,27 @@
14
14
  * down to the cwd. (pi's resource-loader prefers AGENTS.md over CLAUDE.md per
15
15
  * dir and omits CLAUDE.local.md / MEMORY.md, so it can't produce these bytes.)
16
16
  *
17
+ * Two One Code additions layer on top of that Claude Code base, both no-ops when
18
+ * unused so the block stays byte-exact for anyone who doesn't reach for them:
19
+ * - `@path` imports inside any context file are expanded in place, matching
20
+ * Claude Code (recursive, depth-capped, cycle-safe, code-span aware) — so a
21
+ * CLAUDE.md that just says `@AGENTS.md` reuses an existing AGENTS.md.
22
+ * - `ONECODE.md` / `onecode.md` / `One Code.md` files (global `~/.onecode` +
23
+ * each project directory) carry One Code-specific instructions that Claude
24
+ * Code never reads. They ride their OWN `# oneCodeMd` block (see
25
+ * `buildOneCodeBlock` / `discoverOneCodeFiles`), emitted after the `# claudeMd`
26
+ * block so they take precedence over CLAUDE.md — keeping `# claudeMd` itself
27
+ * byte-exact with Claude Code.
28
+ *
17
29
  * What is NOT yet replicated: enterprise-policy files and nested subtree
18
30
  * CLAUDE.md loaded on-demand when a file under them is read (a follow-up needing
19
31
  * file-tracker integration; absent from every turn-1 capture we have).
20
32
  */
21
33
 
22
- import { existsSync, statSync } from "node:fs";
23
- import { dirname, join } from "node:path";
34
+ import { existsSync, readdirSync, statSync } from "node:fs";
35
+ import { dirname, isAbsolute, join } from "node:path";
24
36
  import { tryReadFile } from "./plugins.ts";
37
+ import { expandTilde } from "./paths.ts";
25
38
 
26
39
  /** One CLAUDE.md-family file as it appears in the block. `content` is raw (untrimmed). */
27
40
  export interface ContextFile {
@@ -34,6 +47,15 @@ export const GLOBAL_DESCRIPTOR = "user's private global instructions for all pro
34
47
  export const PROJECT_DESCRIPTOR = "project instructions, checked into the codebase";
35
48
  export const LOCAL_DESCRIPTOR = "user's private project instructions, not checked in";
36
49
  export const MEMORY_DESCRIPTOR = "user's auto-memory, persists across conversations";
50
+ export const ONECODE_DESCRIPTOR = "One Code-specific instructions, not read by Claude Code";
51
+ export const ONECODE_GLOBAL_DESCRIPTOR =
52
+ "One Code-specific global instructions for all projects, not read by Claude Code";
53
+
54
+ /** One Code instruction filenames, in preference order (first present per dir wins). */
55
+ const ONECODE_NAMES = ["ONECODE.md", "onecode.md", "One Code.md"] as const;
56
+
57
+ /** Max `@import` hops, matching Claude Code. Depth 0 is the importing file itself. */
58
+ const MAX_IMPORT_DEPTH = 5;
37
59
 
38
60
  const PREAMBLE =
39
61
  "As you answer the user's questions, you can use the following context:\n" +
@@ -60,48 +82,317 @@ function readFileIfPresent(path: string): string | null {
60
82
  * first, then project files from the farthest ancestor down to the cwd. Within a
61
83
  * directory, `CLAUDE.md` then `CLAUDE.local.md`. `homeClaudeDir` is `~/.claude`.
62
84
  */
63
- export function discoverContextFiles(opts: {
64
- cwd: string;
65
- homeClaudeDir: string;
66
- }): ContextFile[] {
67
- const files: ContextFile[] = [];
68
- const seen = new Set<string>();
85
+ function isPresentFile(path: string): boolean {
86
+ try {
87
+ return existsSync(path) && statSync(path).isFile();
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * The One Code instruction file in `dir`, or null. Prefers an exact candidate
95
+ * casing, then any case-insensitive match, and returns the file's real on-disk
96
+ * name so `Contents of {path}` stays accurate on case-insensitive filesystems
97
+ * (macOS), where `onecode.md` on disk would otherwise be reported as `ONECODE.md`.
98
+ *
99
+ * Fast path first: cheap `stat` probes of the candidate names, so the common case
100
+ * (no One Code file in this directory — true of every ancestor up to the root)
101
+ * costs a few stats and never lists the directory. Only when a probe hits do we
102
+ * read the directory once to recover the real casing.
103
+ */
104
+ function firstOneCodeFile(dir: string): string | null {
105
+ if (!ONECODE_NAMES.some((n) => isPresentFile(join(dir, n)))) return null;
106
+ let entries: string[];
107
+ try {
108
+ entries = readdirSync(dir);
109
+ } catch {
110
+ return null;
111
+ }
112
+ const pick = ONECODE_NAMES.find((n) => entries.includes(n)) ?? entries.find((e) => e.toLowerCase() === "onecode.md");
113
+ if (pick && isPresentFile(join(dir, pick))) return join(dir, pick);
114
+ return null;
115
+ }
69
116
 
70
- const globalPath = join(opts.homeClaudeDir, "CLAUDE.md");
71
- const globalContent = readFileIfPresent(globalPath);
72
- if (globalContent !== null) {
73
- files.push({ path: globalPath, content: globalContent, descriptor: GLOBAL_DESCRIPTOR });
74
- seen.add(globalPath);
117
+ /** Resolve an `@import` reference: `~`/`~/` → home, absolute as-is, else relative to `baseDir`. */
118
+ function resolveImportPath(ref: string, baseDir: string, home: string): string {
119
+ const p = expandTilde(ref, home);
120
+ return isAbsolute(p) ? p : join(baseDir, p);
121
+ }
122
+
123
+ /**
124
+ * Read a `@import` target, tolerating trailing sentence punctuation (`@a/b.md.`).
125
+ * Returns the resolved path, its raw content, and any stripped trailing text to
126
+ * re-append after the inlined content. `null` when nothing readable resolves.
127
+ */
128
+ function readImportTarget(
129
+ ref: string,
130
+ baseDir: string,
131
+ home: string,
132
+ read: (path: string) => string | null,
133
+ stack: Set<string>,
134
+ ): { resolved: string; content: string; trailing: string } | null {
135
+ let candidate = ref;
136
+ for (;;) {
137
+ const resolved = resolveImportPath(candidate, baseDir, home);
138
+ if (!stack.has(resolved)) {
139
+ const content = read(resolved);
140
+ if (content !== null) return { resolved, content, trailing: ref.slice(candidate.length) };
141
+ }
142
+ // Not a readable file (and not a cycle already on the stack): peel one trailing
143
+ // punctuation char and retry, so `@docs/x.md.` still resolves `docs/x.md`.
144
+ if (candidate.length <= 1 || !/[.,;:!?)\]]$/.test(candidate)) return null;
145
+ candidate = candidate.slice(0, -1);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Claude Code's `@path` imports: replace each `@path` reference with the
151
+ * referenced file's (recursively expanded) contents. `@` inside inline-code spans
152
+ * or fenced code blocks is left alone; a reference that does not resolve to a
153
+ * readable file is left as literal text; cycles and hops past MAX_IMPORT_DEPTH
154
+ * stop recursion. `read` defaults to the module's own file reader (injectable for
155
+ * tests). A file with no importable `@` tokens is returned unchanged.
156
+ */
157
+ export function expandImports(
158
+ content: string,
159
+ baseDir: string,
160
+ opts: { home: string; read?: (path: string) => string | null },
161
+ ): string {
162
+ return expandImportsInner(content, baseDir, opts.home, opts.read ?? readFileIfPresent, new Set<string>(), 0);
163
+ }
164
+
165
+ /**
166
+ * The absolute paths every `@path` import in `content` resolves to, transitively
167
+ * (same traversal, resolution, cycle/depth rules as `expandImports`). Used to
168
+ * tell whether a file — e.g. an `AGENTS.md` — is actually pulled into context,
169
+ * rather than merely present on disk.
170
+ */
171
+ export function collectImportedPaths(
172
+ content: string,
173
+ baseDir: string,
174
+ opts: { home: string; read?: (path: string) => string | null },
175
+ ): Set<string> {
176
+ const found = new Set<string>();
177
+ expandImportsInner(content, baseDir, opts.home, opts.read ?? readFileIfPresent, new Set<string>(), 0, (p) =>
178
+ found.add(p),
179
+ );
180
+ return found;
181
+ }
182
+
183
+ function expandImportsInner(
184
+ content: string,
185
+ baseDir: string,
186
+ home: string,
187
+ read: (path: string) => string | null,
188
+ stack: Set<string>,
189
+ depth: number,
190
+ onResolve?: (path: string) => void,
191
+ ): string {
192
+ if (depth >= MAX_IMPORT_DEPTH) return content;
193
+ if (!content.includes("@")) return content;
194
+
195
+ const out: string[] = [];
196
+ let inFence = false;
197
+ let fenceMarker = "";
198
+ for (const line of content.split("\n")) {
199
+ const fence = line.match(/^\s*(`{3,}|~{3,})/);
200
+ if (fence) {
201
+ const marker = fence[1][0];
202
+ if (!inFence) {
203
+ inFence = true;
204
+ fenceMarker = marker;
205
+ } else if (marker === fenceMarker) {
206
+ inFence = false;
207
+ fenceMarker = "";
208
+ }
209
+ out.push(line);
210
+ continue;
211
+ }
212
+ out.push(inFence ? line : expandImportLine(line, baseDir, home, read, stack, depth, onResolve));
75
213
  }
214
+ return out.join("\n");
215
+ }
216
+
217
+ function expandImportLine(
218
+ line: string,
219
+ baseDir: string,
220
+ home: string,
221
+ read: (path: string) => string | null,
222
+ stack: Set<string>,
223
+ depth: number,
224
+ onResolve?: (path: string) => void,
225
+ ): string {
226
+ if (!line.includes("@")) return line;
227
+
228
+ // Shield inline-code spans so `@path` inside backticks is never expanded. NUL
229
+ // delimiters can't occur in source text, so restoration never mis-fires on a
230
+ // real number that happens to be surrounded by spaces.
231
+ const spans: string[] = [];
232
+ const shielded = line.replace(/(`+)[\s\S]*?\1/g, (m) => {
233
+ spans.push(m);
234
+ return `\u0000${spans.length - 1}\u0000`;
235
+ });
76
236
 
77
- // Walk cwd → root collecting directories, then emit farthest-ancestor first.
237
+ const replaced = shielded.replace(/(^|\s)@(\S+)/g, (whole, lead: string, ref: string) => {
238
+ const target = readImportTarget(ref, baseDir, home, read, stack);
239
+ if (!target) return whole; // unresolved or cycle: leave the literal text
240
+ onResolve?.(target.resolved);
241
+ const next = new Set(stack);
242
+ next.add(target.resolved);
243
+ const expanded = expandImportsInner(target.content, dirname(target.resolved), home, read, next, depth + 1, onResolve);
244
+ return `${lead}${expanded}${target.trailing}`;
245
+ });
246
+
247
+ return replaced.replace(/\u0000(\d+)\u0000/g, (_m, i: string) => spans[Number(i)]);
248
+ }
249
+
250
+ /** A discovered CLAUDE.md-family path and its descriptor, without file content. */
251
+ export interface ContextFilePath {
252
+ path: string;
253
+ descriptor: string;
254
+ }
255
+
256
+ /** The cwd and each ancestor up to the filesystem root, ordered farthest-first. */
257
+ export function ancestorDirs(cwd: string): string[] {
78
258
  const dirs: string[] = [];
79
- let dir = opts.cwd;
259
+ let dir = cwd;
80
260
  while (true) {
81
261
  dirs.unshift(dir);
82
262
  const parent = dirname(dir);
83
263
  if (parent === dir) break;
84
264
  dir = parent;
85
265
  }
266
+ return dirs;
267
+ }
86
268
 
87
- for (const d of dirs) {
88
- for (const [name, descriptor] of [
89
- ["CLAUDE.md", PROJECT_DESCRIPTOR],
90
- ["CLAUDE.local.md", LOCAL_DESCRIPTOR],
91
- ] as const) {
92
- const p = join(d, name);
93
- if (seen.has(p)) continue;
94
- const content = readFileIfPresent(p);
95
- if (content !== null) {
96
- files.push({ path: p, content, descriptor });
97
- seen.add(p);
98
- }
269
+ /**
270
+ * The ordered CLAUDE.md-family paths that exist, WITHOUT reading their contents:
271
+ * global `~/.claude/CLAUDE.md` first, then project `CLAUDE.md`/`CLAUDE.local.md`
272
+ * from the farthest ancestor down to cwd (`CLAUDE.md` before `CLAUDE.local.md`
273
+ * within a directory). `discoverContextFiles` reads content on top of this;
274
+ * callers that only need paths/descriptors (e.g. `/memory`'s picker) use it
275
+ * directly to avoid loading files they will discard.
276
+ *
277
+ * When `homeOneCodeDir` is given, One Code's own `ONECODE.md` files join the
278
+ * list: the global one from `~/.onecode` right after the global CLAUDE.md, and
279
+ * each directory's after its CLAUDE.md/CLAUDE.local.md. The `/memory` picker passes
280
+ * it (so ONECODE.md is an editable target); the model-facing `# claudeMd` block
281
+ * does NOT — there ONECODE.md rides its own `# oneCodeMd` block via
282
+ * `discoverOneCodeFiles` instead, so `# claudeMd` stays byte-exact with CC.
283
+ *
284
+ * When `agentsFallback` is set, a directory with no `CLAUDE.md` falls back to its
285
+ * `AGENTS.md` (CLAUDE.md > AGENTS.md — the first-match Codex/opencode do, but
286
+ * CLAUDE-preferred for our CC-compat audience). A directory that has a CLAUDE.md
287
+ * ignores its AGENTS.md, so `# claudeMd` stays byte-exact with CC whenever
288
+ * CLAUDE.md is present.
289
+ */
290
+ export function discoverContextFilePaths(opts: {
291
+ cwd: string;
292
+ homeClaudeDir: string;
293
+ homeOneCodeDir?: string;
294
+ agentsFallback?: boolean;
295
+ }): ContextFilePath[] {
296
+ const paths: ContextFilePath[] = [];
297
+ const seen = new Set<string>();
298
+ const includeOneCode = opts.homeOneCodeDir !== undefined;
299
+
300
+ /** Adds the path if present; returns whether it was added. */
301
+ const push = (path: string, descriptor: string): boolean => {
302
+ if (seen.has(path) || !isPresentFile(path)) return false;
303
+ paths.push({ path, descriptor });
304
+ seen.add(path);
305
+ return true;
306
+ };
307
+
308
+ push(join(opts.homeClaudeDir, "CLAUDE.md"), GLOBAL_DESCRIPTOR);
309
+ if (includeOneCode && opts.homeOneCodeDir) {
310
+ const globalOneCode = firstOneCodeFile(opts.homeOneCodeDir);
311
+ if (globalOneCode) push(globalOneCode, ONECODE_GLOBAL_DESCRIPTOR);
312
+ }
313
+
314
+ for (const d of ancestorDirs(opts.cwd)) {
315
+ const hasClaude = push(join(d, "CLAUDE.md"), PROJECT_DESCRIPTOR);
316
+ // AGENTS.md stands in for a missing CLAUDE.md in this directory.
317
+ if (!hasClaude && opts.agentsFallback) push(join(d, "AGENTS.md"), AGENTS_DESCRIPTOR);
318
+ push(join(d, "CLAUDE.local.md"), LOCAL_DESCRIPTOR);
319
+ if (includeOneCode) {
320
+ const oneCode = firstOneCodeFile(d);
321
+ if (oneCode) push(oneCode, ONECODE_DESCRIPTOR);
99
322
  }
100
323
  }
101
324
 
325
+ return paths;
326
+ }
327
+
328
+ export function discoverContextFiles(opts: {
329
+ cwd: string;
330
+ homeClaudeDir: string;
331
+ homeOneCodeDir?: string;
332
+ agentsFallback?: boolean;
333
+ /** Home directory for resolving `~` in `@path` imports. */
334
+ home: string;
335
+ }): ContextFile[] {
336
+ const files: ContextFile[] = [];
337
+ for (const { path, descriptor } of discoverContextFilePaths(opts)) {
338
+ // Re-check the read: a file present at enumeration but unreadable now is
339
+ // omitted, exactly as before (the block must never carry empty entries).
340
+ const content = readFileIfPresent(path);
341
+ if (content === null) continue;
342
+ files.push({ path, content: expandImports(content, dirname(path), { home: opts.home }), descriptor });
343
+ }
344
+ return files;
345
+ }
346
+
347
+ /**
348
+ * The ONECODE.md files that exist, ordered global-first then farthest ancestor
349
+ * down to cwd (nearer wins). These ride in their own `# oneCodeMd` block, not the
350
+ * `# claudeMd` block, so the latter stays byte-exact with Claude Code.
351
+ */
352
+ function discoverOneCodeFilePaths(opts: { cwd: string; homeOneCodeDir: string }): ContextFilePath[] {
353
+ const paths: ContextFilePath[] = [];
354
+ const seen = new Set<string>();
355
+ const push = (path: string | null, descriptor: string) => {
356
+ if (!path || seen.has(path)) return;
357
+ paths.push({ path, descriptor });
358
+ seen.add(path);
359
+ };
360
+ push(firstOneCodeFile(opts.homeOneCodeDir), ONECODE_GLOBAL_DESCRIPTOR);
361
+ for (const d of ancestorDirs(opts.cwd)) push(firstOneCodeFile(d), ONECODE_DESCRIPTOR);
362
+ return paths;
363
+ }
364
+
365
+ /** ONECODE.md files with `@import`s expanded, for the `# oneCodeMd` block. */
366
+ export function discoverOneCodeFiles(opts: { cwd: string; homeOneCodeDir: string; home: string }): ContextFile[] {
367
+ const files: ContextFile[] = [];
368
+ for (const { path, descriptor } of discoverOneCodeFilePaths(opts)) {
369
+ const content = readFileIfPresent(path);
370
+ if (content === null) continue;
371
+ files.push({ path, content: expandImports(content, dirname(path), { home: opts.home }), descriptor });
372
+ }
102
373
  return files;
103
374
  }
104
375
 
376
+ const ONECODE_PREAMBLE =
377
+ "# oneCodeMd\n" +
378
+ "The following are One Code-specific instructions, read only by One Code and not by other tools. " +
379
+ "IMPORTANT: they take precedence over the CLAUDE.md instructions above and over any default behavior — " +
380
+ "where they conflict with CLAUDE.md, follow these. Follow them exactly as written.";
381
+
382
+ /**
383
+ * Assemble the `# oneCodeMd` block's inner text (wrapper added by lib/reminders.ts):
384
+ * the precedence preamble, then one `Contents of {path} ({descriptor}):\n\n{content}`
385
+ * section per file (same raw-content join as the claudeMd block). Returns null when
386
+ * there are no ONECODE.md files, so nothing extra rides when the feature is unused.
387
+ */
388
+ export function buildOneCodeBlock(files: ContextFile[]): string | null {
389
+ if (files.length === 0) return null;
390
+ const sections = files.map((s) => `Contents of ${s.path} (${s.descriptor}):\n\n${s.content}`).join("\n");
391
+ return `${ONECODE_PREAMBLE}\n\n${sections}`;
392
+ }
393
+
394
+ export const AGENTS_DESCRIPTOR = "cross-tool agent instructions, AGENTS.md standard";
395
+
105
396
  /**
106
397
  * Assemble the block's inner text (the `<system-reminder>` wrapper is added by
107
398
  * lib/reminders.ts). Byte-exact rule reverse-engineered from opus-4-8.json:
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Read-only access to Claude Code's settings files (pure fs, no pi imports).
3
+ *
4
+ * Sources, lowest to highest precedence — the same ladder the permissions
5
+ * extension merges:
6
+ * ~/.claude/settings.json (user)
7
+ * <cwd>/.claude/settings.json (project, checked in)
8
+ * <cwd>/.claude/settings.local.json (project, personal)
9
+ *
10
+ * One Code NEVER writes these files. Plugin enable/disable initiated from One
11
+ * Code goes to the One Code plugin root (lib/plugin-overrides.ts), never here.
12
+ */
13
+
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { claudeUserDir } from "./paths.ts";
17
+
18
+ export interface ClaudeSettingsFile {
19
+ enabledPlugins?: Record<string, unknown>;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ /**
24
+ * Claude Code's *user* settings file. The one canonical statement of that path,
25
+ * so the loaders that must recognise "the borrowed `.claude` user file" (to skip
26
+ * One Code's own keys found there) all compare against the same value.
27
+ */
28
+ export function claudeUserSettingsPath(home: string): string {
29
+ return join(claudeUserDir(home), "settings.json");
30
+ }
31
+
32
+ export function settingsPaths(cwd: string, home: string): { user: string; project: string; local: string } {
33
+ return {
34
+ user: claudeUserSettingsPath(home),
35
+ project: join(cwd, ".claude", "settings.json"),
36
+ local: join(cwd, ".claude", "settings.local.json"),
37
+ };
38
+ }
39
+
40
+ export function readSettingsFile(path: string): ClaudeSettingsFile | undefined {
41
+ if (!existsSync(path)) return undefined;
42
+ try {
43
+ return JSON.parse(readFileSync(path, "utf-8")) as ClaudeSettingsFile;
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Claude Code's plugin enabled-state map (`{"name@marketplace": boolean}`),
51
+ * merged across the three settings files — later files win per key.
52
+ */
53
+ export function readEnabledPlugins(cwd: string, home: string): Record<string, boolean> {
54
+ const paths = settingsPaths(cwd, home);
55
+ const merged: Record<string, boolean> = {};
56
+ for (const path of [paths.user, paths.project, paths.local]) {
57
+ const enabled = readSettingsFile(path)?.enabledPlugins;
58
+ if (!enabled || typeof enabled !== "object") continue;
59
+ for (const [key, value] of Object.entries(enabled)) {
60
+ if (typeof value === "boolean") merged[key] = value;
61
+ }
62
+ }
63
+ return merged;
64
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Bounding for text shown in a consent modal (pure).
3
+ *
4
+ * A consent dialog lists what an untrusted repo wants to run — hook commands,
5
+ * MCP server commands. The list must not hide where an attack lives (so a single
6
+ * item is shown in full up to a generous per-item cap, never an 80-char slice),
7
+ * but it also must not render megabytes into a modal (hundreds of items, or one
8
+ * pathologically long one). This bounds the total: full items until a budget,
9
+ * each item capped, then a pointer to the real file (which is the source of
10
+ * truth the user should read before approving anything this large).
11
+ */
12
+
13
+ const PER_ITEM_CAP = 2000;
14
+ const TOTAL_CAP = 4000;
15
+
16
+ export function boundConsentItems(items: string[], moreHint: string): string {
17
+ const shown: string[] = [];
18
+ let used = 0;
19
+ for (const item of items) {
20
+ const capped = item.length > PER_ITEM_CAP ? `${item.slice(0, PER_ITEM_CAP)}… [truncated, ${item.length} chars]` : item;
21
+ // Always show the first item (so an empty modal never happens); stop once
22
+ // the budget is spent.
23
+ if (shown.length > 0 && used + capped.length + 1 > TOTAL_CAP) break;
24
+ shown.push(capped);
25
+ used += capped.length + 1;
26
+ }
27
+ const hidden = items.length - shown.length;
28
+ const more = hidden > 0 ? `\n… and ${hidden} more not shown — ${moreHint}` : "";
29
+ return shown.join("\n") + more;
30
+ }
@@ -10,6 +10,9 @@
10
10
  * `{ name, keywords? }` while extensions are loading (before session_start).
11
11
  */
12
12
 
13
+ import { parseClaudeVersion } from "./model-tier.ts";
14
+ import { normalizeToolName } from "../permissions/matcher.ts";
15
+
13
16
  export const DEFER_CHANNEL = "one-code:defer-tool";
14
17
 
15
18
  export interface DeferRequest {
@@ -46,31 +49,155 @@ export class DeferredRegistry {
46
49
  }
47
50
  }
48
51
 
49
- export const deferredRegistry = new DeferredRegistry();
52
+ // The one registry instance is constructed by its owner (tool-search/index.ts):
53
+ // a shared export here would be a distinct object in every other extension's
54
+ // jiti module graph, and a tool deferred through that copy would steer nothing.
50
55
 
51
56
  /**
52
57
  * The every-turn reminder telling the model which tools exist but are not
53
- * loaded. One line per tool (first description line only), like Claude Code's
54
- * deferred-tools listing.
58
+ * loaded — Claude Code's shape (cc-haiku.json, message 1, block 1): one fixed
59
+ * sentence, then bare names one per line. No descriptions: the model searches
60
+ * by keyword through `tool_search` (which does see descriptions), and a name-
61
+ * only list is ~7x smaller — the description form grew by several KB per MCP
62
+ * server and was the single largest block after `# claudeMd`. The wording is
63
+ * CC's with our tool name and our dispatcher's actual error text; it stays
64
+ * byte-identical after a load (as CC's does) so the cached prefix holds — a
65
+ * loaded tool is simply callable, and the sentence says as much.
66
+ */
67
+ export function deferredReminderText(tools: Array<Pick<SearchableTool, "name"> & Partial<SearchableTool>>): string {
68
+ return [
69
+ 'The following deferred tools are available via tool_search. Their schemas are NOT loaded — calling one directly fails with "Tool <name> not found" until you load it. Use tool_search with query "select:<name>[,<name>...]" to load tool schemas before calling them (once loaded, a tool stays callable for the rest of the session):',
70
+ ...tools.map((t) => t.name),
71
+ ].join("\n");
72
+ }
73
+
74
+ /**
75
+ * The one-shot that announces deferred tools which registered AFTER the first
76
+ * request went out (a slow MCP connect, a reconnect, a plugin install). The
77
+ * standing listing on message 1 is frozen once cached (tool-search/announce.ts);
78
+ * this rides the tail instead, so the cached prefix holds.
55
79
  */
56
- export function deferredReminderText(tools: Array<Pick<SearchableTool, "name" | "description">>): string {
80
+ export function deferredAddendumText(added: readonly string[]): string {
57
81
  return [
58
- "The following tools are available but their schemas are NOT loaded, so they cannot be called yet:",
59
- ...tools.map((t) => `- ${t.name}: ${t.description.split("\n")[0]}`),
60
- "",
61
- "Load one with tool_search before using it — `select:<name>[,<name>]` for exact names, or keywords to search. Once a schema is loaded it stays callable for the rest of the session.",
82
+ "Additional deferred tools became available via tool_search since this conversation started (they registered after the first request). Same rules: load with tool_search \"select:<name>\" before calling.",
83
+ ...added,
62
84
  ].join("\n");
63
85
  }
64
86
 
87
+ /**
88
+ * Whether Anthropic accepts client-side `tool_reference` blocks for this model —
89
+ * pi-ai's `defaultSupportsToolReferences` rule, with the model's own compat flag
90
+ * winning: first-party Anthropic, not Haiku, Claude 4.5 or newer.
91
+ */
92
+ export function supportsToolReferences(
93
+ model: { provider?: string; id?: string; compat?: { supportsToolReferences?: boolean } } | undefined,
94
+ ): boolean {
95
+ if (!model) return false;
96
+ if (typeof model.compat?.supportsToolReferences === "boolean") return model.compat.supportsToolReferences;
97
+ if (model.provider !== "anthropic" || !model.id || model.id.includes("haiku")) return false;
98
+ const version = parseClaudeVersion(model.id);
99
+ return version !== undefined && (version.major > 4 || (version.major === 4 && version.minor >= 5));
100
+ }
101
+
102
+ /** The slice of pi's ToolInfo the deferred definitions need. */
103
+ export interface DeferrableToolInfo {
104
+ name: string;
105
+ description?: string;
106
+ parameters?: { properties?: unknown; required?: unknown };
107
+ }
108
+
109
+ /**
110
+ * An Anthropic request with every deferred tool that pi left out appended as a
111
+ * `defer_loading: true` definition, sorted by name (pi's own entries stay first
112
+ * and untouched). Anthropic keeps deferred definitions out of the cached prefix,
113
+ * but ADDING one to `tools` mid-session still re-caches the message history —
114
+ * measured 2026-09-04 on Sonnet 5: the request after a `tool_search` load read
115
+ * only tools + system (8.7k of 25k tokens); with the full deferred set present
116
+ * from request 1 it read all 25k. Unloaded deferred definitions cost no input
117
+ * tokens, so sending them always is free and keeps `tools` byte-stable.
118
+ *
119
+ * Returns undefined (leave the payload alone) when there is nothing to add, the
120
+ * request carries no tools, or the tool names on the wire are not the registry's
121
+ * (pi's OAuth "stealth" mode renames them to Claude Code casing, and a name that
122
+ * cannot be matched must not be duplicated). Schemas follow pi's own
123
+ * `convertTools` shape. Never mutates its input.
124
+ */
125
+ export function withDeferredToolDefinitions(
126
+ payload: Record<string, unknown>,
127
+ tools: readonly DeferrableToolInfo[],
128
+ isDeferred: (name: string) => boolean,
129
+ ): Record<string, unknown> | undefined {
130
+ const existing = payload.tools;
131
+ if (!Array.isArray(existing) || existing.length === 0) return undefined;
132
+ const registry = new Set(tools.map((t) => t.name));
133
+ const present = new Set<string>();
134
+ for (const tool of existing) {
135
+ const name = (tool as { name?: unknown } | null)?.name;
136
+ if (typeof name !== "string" || !registry.has(name)) return undefined;
137
+ present.add(name);
138
+ }
139
+ const extra = tools
140
+ .filter((t) => isDeferred(t.name) && !present.has(t.name))
141
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
142
+ .map((t) => ({
143
+ name: t.name,
144
+ description: t.description ?? "",
145
+ input_schema: {
146
+ type: "object",
147
+ properties: t.parameters?.properties ?? {},
148
+ required: t.parameters?.required ?? [],
149
+ },
150
+ defer_loading: true,
151
+ }));
152
+ if (extra.length === 0) return undefined;
153
+ return { ...payload, tools: [...existing, ...extra] };
154
+ }
155
+
156
+ /**
157
+ * The bare error pi's core dispatcher returns when the model calls a tool whose
158
+ * schema isn't in the active set — including every deferred tool before
159
+ * `tool_search` loads it (pi-agent-core `prepareToolCall`). The message is
160
+ * `Tool <name> not found`; this recovers `<name>` so a deferred miss can be
161
+ * steered back to `tool_search`. Returns undefined for any other error text.
162
+ */
163
+ export function toolNotFoundName(text: string): string | undefined {
164
+ return /^Tool (\S+) not found$/.exec(text.trim())?.[1];
165
+ }
166
+
167
+ /** Flatten a tool result's content blocks to plain text (results are untyped over the bus). */
168
+ export function resultText(result: unknown): string {
169
+ const content = (result as { content?: unknown })?.content;
170
+ if (typeof content === "string") return content;
171
+ if (!Array.isArray(content)) return "";
172
+ return content
173
+ .map((block) => (block && typeof block === "object" && "text" in block ? String((block as { text: unknown }).text) : ""))
174
+ .join("");
175
+ }
176
+
177
+ /**
178
+ * The one-shot correction that rides in right after a deferred tool's
179
+ * "not found" failure, pointing the model at `tool_search` instead of leaving it
180
+ * to recover from the bare error on its own.
181
+ */
182
+ export function deferredMissReminderText(name: string): string {
183
+ return [
184
+ `\`${name}\` is a deferred tool — its schema is not loaded, so that call failed with "Tool ${name} not found".`,
185
+ `Load it first with tool_search (\`select:${name}\`), then reissue the call. It stays callable for the rest of the session once loaded.`,
186
+ ].join(" ");
187
+ }
188
+
65
189
  export interface SearchMatch {
66
190
  name: string;
67
191
  score: number;
68
192
  }
69
193
 
70
194
  /**
71
- * The exact names a `select:` query asks for (lowercased), or undefined when the
72
- * query is not a select: query. Lets tool_search report which requested names
73
- * matched nothing instead of silently dropping them.
195
+ * The exact names a `select:` query asks for (lowercased and mapped through the
196
+ * Claude Code alias table), or undefined when the query is not a select: query.
197
+ * A CC-trained model writes `select:WebFetch,NotebookEdit`; without the alias
198
+ * pass those are "not found — check spelling" even though the tools exist under
199
+ * our names (TOOL-FIDELITY-REVIEW-2026-09-07 M1). Lets tool_search report which
200
+ * requested names matched nothing instead of silently dropping them.
74
201
  */
75
202
  export function selectedNames(query: string): string[] | undefined {
76
203
  const trimmed = query.trim();
@@ -78,8 +205,12 @@ export function selectedNames(query: string): string[] | undefined {
78
205
  return trimmed
79
206
  .slice("select:".length)
80
207
  .split(",")
81
- .map((n) => n.trim().toLowerCase())
82
- .filter(Boolean);
208
+ .map((n) => n.trim())
209
+ .filter(Boolean)
210
+ // normalizeToolName lowercases ordinary names but PRESERVES case for
211
+ // `mcp__…` names; the trailing toLowerCase keeps select: case-insensitive
212
+ // for those too. Don't drop it as "redundant".
213
+ .map((n) => normalizeToolName(n).toLowerCase());
83
214
  }
84
215
 
85
216
  /**