one-code-extension 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -202
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- 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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
*
|
|
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
|
|
80
|
+
export function deferredAddendumText(added: readonly string[]): string {
|
|
57
81
|
return [
|
|
58
|
-
"
|
|
59
|
-
...
|
|
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
|
|
72
|
-
* query is not a select: query.
|
|
73
|
-
*
|
|
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()
|
|
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
|
/**
|