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
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Plugin-provided LSP server configs (pure).
3
+ *
4
+ * The way Claude Code supports language servers: a plugin ships either a
5
+ * `.lsp.json` in its root (`{"<server>": {command, extensionToLanguage, ...}}`)
6
+ * or an `lspServers` field in `.claude-plugin/plugin.json` (a relative path to
7
+ * such a JSON file, an inline record, or an array mixing both — manifest
8
+ * entries win on server-name collision). Servers are named
9
+ * `plugin:<plugin>:<server>` and routed by file extension via their required
10
+ * `extensionToLanguage` map; there is no root-marker walk for plugin servers
11
+ * (the built-in table in servers.ts keeps that) — they root at
12
+ * `workspaceFolder ?? cwd`.
13
+ *
14
+ * `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_DATA}`, `${VAR}` and
15
+ * `${VAR:-default}` are substituted in `command`, each `args` element, `env`
16
+ * values, and `workspaceFolder` — never inside `settings` or
17
+ * `initializationOptions`, which pass through verbatim. `${user_config.*}`
18
+ * (plugin user-config prompts) is not supported: a server using it is rejected
19
+ * with a named diagnostic rather than spawned with a live placeholder.
20
+ *
21
+ * Unsupported config fields are rejected loudly per repo convention:
22
+ * `transport: "socket"`, `shutdownTimeout`, `restartOnCrash`, `maxRestarts`.
23
+ */
24
+
25
+ import { readFileSync } from "node:fs";
26
+ import { extname, join, resolve } from "node:path";
27
+ import { pathWithinBase, sanitizePathSegment } from "../lib/plugin-root.ts";
28
+
29
+ export interface ResolvedPluginServer {
30
+ /** `plugin:<pluginName>:<serverName>` — the client-map key. */
31
+ key: string;
32
+ pluginName: string;
33
+ serverName: string;
34
+ /** Normalized `.ext` (lowercase, leading dot) → LSP languageId. */
35
+ languageByExtension: Record<string, string>;
36
+ command: string;
37
+ args: string[];
38
+ /** Fully substituted; includes CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA. */
39
+ env: Record<string, string>;
40
+ /** The `${CLAUDE_PLUGIN_DATA}` directory — wiring mkdirs it before spawn. */
41
+ dataDir: string;
42
+ workspaceFolder?: string;
43
+ initializationOptions?: unknown;
44
+ settings?: unknown;
45
+ startupTimeoutMs?: number;
46
+ }
47
+
48
+ export interface PluginServerSource {
49
+ name: string;
50
+ path: string;
51
+ /** Per-origin plugin-data base dir (Plugin.dataRoot from lib/plugins.ts). */
52
+ dataRoot: string;
53
+ }
54
+
55
+ export function pluginDataDir(dataRoot: string, pluginName: string): string {
56
+ return join(dataRoot, sanitizePathSegment(pluginName));
57
+ }
58
+
59
+ /** Resolve a manifest-declared relative path, or undefined when it escapes the plugin dir. */
60
+ export function resolveWithinPlugin(pluginPath: string, relativePath: string): string | undefined {
61
+ return pathWithinBase(pluginPath, relativePath) ? resolve(pluginPath, relativePath) : undefined;
62
+ }
63
+
64
+ interface SubstitutionContext {
65
+ pluginRootDir: string;
66
+ dataDir: string;
67
+ env: Record<string, string | undefined>;
68
+ }
69
+
70
+ interface SubstitutionResult {
71
+ value: string;
72
+ /** `${user_config.*}` seen — the whole server must be rejected. */
73
+ userConfig: boolean;
74
+ /** Env vars that had no value and no default (left literal). */
75
+ missing: string[];
76
+ }
77
+
78
+ const PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_.]*)(?::-([^}]*))?\}/g;
79
+
80
+ export function substitute(value: string, ctx: SubstitutionContext): SubstitutionResult {
81
+ let userConfig = false;
82
+ const missing: string[] = [];
83
+ const result = value.replace(PLACEHOLDER, (literal, name: string, fallback: string | undefined) => {
84
+ if (name === "CLAUDE_PLUGIN_ROOT") return ctx.pluginRootDir;
85
+ if (name === "CLAUDE_PLUGIN_DATA") return ctx.dataDir;
86
+ if (name.startsWith("user_config.")) {
87
+ userConfig = true;
88
+ return literal;
89
+ }
90
+ const env = ctx.env[name];
91
+ if (env !== undefined) return env;
92
+ if (fallback !== undefined) return fallback;
93
+ missing.push(name);
94
+ return literal;
95
+ });
96
+ return { value: result, userConfig, missing };
97
+ }
98
+
99
+ /** Raw server configs gathered from one plugin's sources, manifest winning. */
100
+ export function collectRawServers(
101
+ plugin: Pick<PluginServerSource, "name" | "path">,
102
+ lspConfigPath: string | undefined,
103
+ manifestLspServers: unknown,
104
+ diagnostics: string[],
105
+ ): Record<string, unknown> {
106
+ const servers: Record<string, unknown> = {};
107
+
108
+ const mergeRecord = (raw: unknown, sourceLabel: string) => {
109
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
110
+ diagnostics.push(`${plugin.name}: ${sourceLabel} must be a JSON object mapping server names to configs`);
111
+ return;
112
+ }
113
+ Object.assign(servers, raw as Record<string, unknown>);
114
+ };
115
+
116
+ const mergeFile = (path: string, sourceLabel: string) => {
117
+ let raw: string;
118
+ try {
119
+ raw = readFileSync(path, "utf-8");
120
+ } catch (error) {
121
+ diagnostics.push(`${plugin.name}: could not read ${sourceLabel} (${(error as Error).message})`);
122
+ return;
123
+ }
124
+ try {
125
+ mergeRecord(JSON.parse(raw), sourceLabel);
126
+ } catch (error) {
127
+ diagnostics.push(`${plugin.name}: invalid JSON in ${sourceLabel} (${(error as Error).message})`);
128
+ }
129
+ };
130
+
131
+ if (lspConfigPath) mergeFile(lspConfigPath, ".lsp.json");
132
+
133
+ const mergeManifestEntry = (entry: unknown) => {
134
+ if (typeof entry === "string") {
135
+ const resolved = resolveWithinPlugin(plugin.path, entry);
136
+ if (!resolved) {
137
+ diagnostics.push(`${plugin.name}: manifest lspServers path "${entry}" escapes the plugin directory — rejected`);
138
+ return;
139
+ }
140
+ mergeFile(resolved, `manifest lspServers file "${entry}"`);
141
+ return;
142
+ }
143
+ mergeRecord(entry, "manifest lspServers");
144
+ };
145
+
146
+ if (manifestLspServers !== undefined) {
147
+ if (Array.isArray(manifestLspServers)) for (const entry of manifestLspServers) mergeManifestEntry(entry);
148
+ else mergeManifestEntry(manifestLspServers);
149
+ }
150
+
151
+ return servers;
152
+ }
153
+
154
+ /** The manifest's raw `lspServers` value, read directly off plugin.json. */
155
+ export function readManifestLspServers(pluginPath: string): unknown {
156
+ let raw: string;
157
+ try {
158
+ raw = readFileSync(join(pluginPath, ".claude-plugin", "plugin.json"), "utf-8");
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ try {
163
+ return (JSON.parse(raw) as { lspServers?: unknown }).lspServers;
164
+ } catch {
165
+ return undefined;
166
+ }
167
+ }
168
+
169
+ const UNSUPPORTED_FIELDS = ["shutdownTimeout", "restartOnCrash", "maxRestarts"] as const;
170
+
171
+ /**
172
+ * Validate + substitute one server config. Returns undefined (with the reason
173
+ * pushed to `diagnostics`) rather than producing a spawnable-but-wrong config.
174
+ */
175
+ export function resolveServerConfig(
176
+ plugin: PluginServerSource,
177
+ serverName: string,
178
+ raw: unknown,
179
+ env: Record<string, string | undefined>,
180
+ diagnostics: string[],
181
+ ): ResolvedPluginServer | undefined {
182
+ const label = `${plugin.name}:${serverName}`;
183
+ const reject = (reason: string): undefined => {
184
+ diagnostics.push(`${label}: ${reason} — server skipped`);
185
+ return undefined;
186
+ };
187
+
188
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return reject("config must be an object");
189
+ const config = raw as Record<string, unknown>;
190
+
191
+ for (const field of UNSUPPORTED_FIELDS) {
192
+ if (config[field] !== undefined) return reject(`"${field}" is not supported`);
193
+ }
194
+ if (config.transport !== undefined && config.transport !== "stdio") {
195
+ return reject(`transport "${String(config.transport)}" is not supported (stdio only)`);
196
+ }
197
+
198
+ if (typeof config.command !== "string" || config.command.length === 0) {
199
+ return reject('"command" must be a non-empty string');
200
+ }
201
+ if (config.command.includes(" ") && !config.command.startsWith("/")) {
202
+ return reject('"command" must not contain spaces — use the args array for arguments');
203
+ }
204
+
205
+ const extRaw = config.extensionToLanguage;
206
+ if (extRaw === null || typeof extRaw !== "object" || Array.isArray(extRaw)) {
207
+ return reject('"extensionToLanguage" (extension → language id map) is required');
208
+ }
209
+ const languageByExtension: Record<string, string> = {};
210
+ for (const [ext, languageId] of Object.entries(extRaw as Record<string, unknown>)) {
211
+ if (typeof languageId !== "string" || languageId.length === 0) {
212
+ return reject(`extensionToLanguage["${ext}"] must be a non-empty language id`);
213
+ }
214
+ const normalized = (ext.startsWith(".") ? ext : `.${ext}`).toLowerCase();
215
+ languageByExtension[normalized] = languageId;
216
+ }
217
+ if (Object.keys(languageByExtension).length === 0) {
218
+ return reject('"extensionToLanguage" must have at least one mapping');
219
+ }
220
+
221
+ if (config.args !== undefined && (!Array.isArray(config.args) || config.args.some((a) => typeof a !== "string"))) {
222
+ return reject('"args" must be an array of strings');
223
+ }
224
+ if (config.startupTimeout !== undefined && (typeof config.startupTimeout !== "number" || config.startupTimeout <= 0)) {
225
+ return reject('"startupTimeout" must be a positive number of milliseconds');
226
+ }
227
+ if (config.workspaceFolder !== undefined && typeof config.workspaceFolder !== "string") {
228
+ return reject('"workspaceFolder" must be a string');
229
+ }
230
+ if (
231
+ config.env !== undefined &&
232
+ (config.env === null || typeof config.env !== "object" || Array.isArray(config.env) ||
233
+ Object.values(config.env as Record<string, unknown>).some((v) => typeof v !== "string"))
234
+ ) {
235
+ return reject('"env" must be a map of string values');
236
+ }
237
+
238
+ const dataDir = pluginDataDir(plugin.dataRoot, plugin.name);
239
+ const ctx: SubstitutionContext = { pluginRootDir: plugin.path, dataDir, env };
240
+ const missing = new Set<string>();
241
+ let sawUserConfig = false;
242
+ const sub = (value: string): string => {
243
+ const result = substitute(value, ctx);
244
+ if (result.userConfig) sawUserConfig = true;
245
+ for (const name of result.missing) missing.add(name);
246
+ return result.value;
247
+ };
248
+
249
+ const command = sub(config.command);
250
+ const args = ((config.args as string[] | undefined) ?? []).map(sub);
251
+ const substitutedEnv: Record<string, string> = {};
252
+ for (const [key, value] of Object.entries((config.env as Record<string, string> | undefined) ?? {})) {
253
+ substitutedEnv[key] = sub(value);
254
+ }
255
+ const workspaceFolder = typeof config.workspaceFolder === "string" ? sub(config.workspaceFolder) : undefined;
256
+
257
+ if (sawUserConfig) {
258
+ return reject("${user_config.*} placeholders are not supported (plugin user-config prompts are not implemented)");
259
+ }
260
+ if (missing.size > 0) {
261
+ diagnostics.push(`${label}: environment variable${missing.size === 1 ? "" : "s"} not set: ${[...missing].join(", ")} (left as-is)`);
262
+ }
263
+ if (command.includes("${")) {
264
+ return reject(`command still contains an unresolved placeholder after substitution ("${command}")`);
265
+ }
266
+
267
+ return {
268
+ key: `plugin:${plugin.name}:${serverName}`,
269
+ pluginName: plugin.name,
270
+ serverName,
271
+ languageByExtension,
272
+ command,
273
+ args,
274
+ env: { CLAUDE_PLUGIN_ROOT: plugin.path, CLAUDE_PLUGIN_DATA: dataDir, ...substitutedEnv },
275
+ dataDir,
276
+ workspaceFolder,
277
+ initializationOptions: config.initializationOptions,
278
+ settings: config.settings,
279
+ startupTimeoutMs: config.startupTimeout as number | undefined,
280
+ };
281
+ }
282
+
283
+ /** Everything one plugin contributes, resolved and substituted. */
284
+ export function resolvePluginServers(
285
+ plugin: PluginServerSource,
286
+ lspConfigPath: string | undefined,
287
+ manifestLspServers: unknown,
288
+ env: Record<string, string | undefined>,
289
+ ): { servers: ResolvedPluginServer[]; diagnostics: string[] } {
290
+ const diagnostics: string[] = [];
291
+ const raw = collectRawServers(plugin, lspConfigPath, manifestLspServers, diagnostics);
292
+ const servers: ResolvedPluginServer[] = [];
293
+ for (const [serverName, config] of Object.entries(raw)) {
294
+ const resolved = resolveServerConfig(plugin, serverName, config, env, diagnostics);
295
+ if (resolved) servers.push(resolved);
296
+ }
297
+ return { servers, diagnostics };
298
+ }
299
+
300
+ export interface ExtensionRouting {
301
+ byExtension: Map<string, ResolvedPluginServer>;
302
+ collisions: string[];
303
+ }
304
+
305
+ /**
306
+ * Extension → server routing across all plugins. Deterministic on collision:
307
+ * servers sorted by plugin then server name, first claim wins, the loser is
308
+ * recorded for /lsp and a one-time notify.
309
+ */
310
+ export function resolveExtensionRouting(servers: ResolvedPluginServer[]): ExtensionRouting {
311
+ const sorted = [...servers].sort((a, b) => a.key.localeCompare(b.key));
312
+ const byExtension = new Map<string, ResolvedPluginServer>();
313
+ const collisions: string[] = [];
314
+ for (const server of sorted) {
315
+ for (const ext of Object.keys(server.languageByExtension)) {
316
+ const holder = byExtension.get(ext);
317
+ if (holder) {
318
+ collisions.push(`${ext}: ${server.key} ignored (${holder.key} claimed it first)`);
319
+ continue;
320
+ }
321
+ byExtension.set(ext, server);
322
+ }
323
+ }
324
+ return { byExtension, collisions };
325
+ }
326
+
327
+ /** LanguageId a plugin server uses for a path (routing already matched it). */
328
+ export function pluginLanguageId(server: ResolvedPluginServer, path: string): string {
329
+ const ext = extname(path).toLowerCase();
330
+ return server.languageByExtension[ext] ?? Object.values(server.languageByExtension)[0];
331
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Session-wide new-diagnostics delta (pure).
3
+ *
4
+ * The way Claude Code surfaces language-server feedback: every diagnostic the
5
+ * servers have published is diffed against a delivered-set, and only genuinely
6
+ * new ones are injected into the conversation — cross-file (an edit to one
7
+ * file surfaces the dependents it broke), all severities, deduplicated by
8
+ * content so an unchanged diagnostic is never repeated. Editing a file clears
9
+ * its delivered-set, so an issue that was fixed and reintroduced resurfaces.
10
+ *
11
+ * Caps match Claude Code's: 10 diagnostics per file, 30 total, 4000 chars,
12
+ * errors surviving truncation first; the delivered-set is bounded to the 500
13
+ * most-recently-touched files.
14
+ *
15
+ * One deliberate divergence: file headers are cwd-relative paths, not
16
+ * basenames — two files with the same basename would otherwise be
17
+ * indistinguishable in one block (docs/decisions/lsp.md).
18
+ */
19
+
20
+ import { relative } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { type LspDiagnostic, severitySymbol } from "./format.ts";
23
+
24
+ export const MAX_PER_FILE = 10;
25
+ export const MAX_TOTAL = 30;
26
+ export const MAX_CHARS = 4000;
27
+ const MAX_TRACKED_FILES = 500;
28
+
29
+ /** Content identity of one diagnostic — stable across repeated publishes. */
30
+ export function fingerprintDiagnostic(d: LspDiagnostic): string {
31
+ return JSON.stringify({
32
+ m: d.message,
33
+ s: d.severity ?? null,
34
+ r: d.range,
35
+ o: d.source ?? null,
36
+ c: d.code ?? null,
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Per-file delivered fingerprints, LRU-bounded by file count. Insertion order
42
+ * doubles as recency: touching a file re-inserts it at the back.
43
+ */
44
+ export class DeliveredTracker {
45
+ private files = new Map<string, Set<string>>();
46
+
47
+ constructor(private readonly maxFiles = MAX_TRACKED_FILES) {}
48
+
49
+ /** Forget a file (called when it is edited) so its issues can resurface. */
50
+ clear(uri: string): void {
51
+ this.files.delete(uri);
52
+ }
53
+
54
+ delivered(uri: string): Set<string> | undefined {
55
+ return this.files.get(uri);
56
+ }
57
+
58
+ markDelivered(uri: string, fingerprints: Iterable<string>): void {
59
+ const existing = this.files.get(uri) ?? new Set<string>();
60
+ this.files.delete(uri); // re-insert at the back = most recently touched
61
+ for (const fp of fingerprints) existing.add(fp);
62
+ this.files.set(uri, existing);
63
+ while (this.files.size > this.maxFiles) {
64
+ const oldest = this.files.keys().next().value;
65
+ if (oldest === undefined) break;
66
+ this.files.delete(oldest);
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Diagnostics not yet delivered, per uri. Pure read — nothing is committed
73
+ * until `markDelivered(delta, tracker)` after a successful send.
74
+ */
75
+ export function computeDelta(
76
+ all: Map<string, LspDiagnostic[]>,
77
+ tracker: DeliveredTracker,
78
+ ): Map<string, LspDiagnostic[]> {
79
+ const delta = new Map<string, LspDiagnostic[]>();
80
+ for (const [uri, diagnostics] of all) {
81
+ if (diagnostics.length === 0) continue;
82
+ const seen = tracker.delivered(uri);
83
+ const fresh = seen ? diagnostics.filter((d) => !seen.has(fingerprintDiagnostic(d))) : diagnostics;
84
+ if (fresh.length > 0) delta.set(uri, fresh);
85
+ }
86
+ return delta;
87
+ }
88
+
89
+ export function markDelivered(delta: Map<string, LspDiagnostic[]>, tracker: DeliveredTracker): void {
90
+ for (const [uri, diagnostics] of delta) {
91
+ tracker.markDelivered(uri, diagnostics.map(fingerprintDiagnostic));
92
+ }
93
+ }
94
+
95
+ function entryLine(d: LspDiagnostic): string {
96
+ const line = d.range.start.line + 1;
97
+ const column = d.range.start.character + 1;
98
+ const code = d.code !== undefined ? ` [${d.code}]` : "";
99
+ const source = d.source ? ` (${d.source})` : "";
100
+ return ` ${severitySymbol(d.severity)} [Line ${line}:${column}] ${d.message}${code}${source}`;
101
+ }
102
+
103
+ function displayPath(uri: string, cwd: string): string {
104
+ let path = uri;
105
+ try {
106
+ path = fileURLToPath(uri);
107
+ } catch {
108
+ // Not a file uri; show it verbatim.
109
+ }
110
+ return relative(cwd, path) || path;
111
+ }
112
+
113
+ /**
114
+ * The `<new-diagnostics>` block, or undefined for an empty delta. Layout:
115
+ * opening tag runs straight into the preamble, one blank line before the
116
+ * first file section and between sections, entries two-space indented.
117
+ */
118
+ export function formatNewDiagnostics(delta: Map<string, LspDiagnostic[]>, cwd: string): string | undefined {
119
+ if (delta.size === 0) return undefined;
120
+
121
+ interface FileSection {
122
+ path: string;
123
+ diagnostics: LspDiagnostic[];
124
+ omitted: number;
125
+ worst: number;
126
+ }
127
+
128
+ const total = [...delta.values()].reduce((n, list) => n + list.length, 0);
129
+ const sections: FileSection[] = [...delta.entries()].map(([uri, list]) => {
130
+ const sorted = [...list].sort((a, b) => (a.severity ?? 1) - (b.severity ?? 1));
131
+ const kept = sorted.slice(0, MAX_PER_FILE);
132
+ return {
133
+ path: displayPath(uri, cwd),
134
+ diagnostics: kept,
135
+ omitted: sorted.length - kept.length,
136
+ worst: kept[0]?.severity ?? 1,
137
+ };
138
+ });
139
+ sections.sort((a, b) => a.worst - b.worst || a.path.localeCompare(b.path));
140
+
141
+ const parts: string[] = [];
142
+ let included = 0;
143
+ for (const section of sections) {
144
+ if (included >= MAX_TOTAL) break;
145
+ const room = MAX_TOTAL - included;
146
+ const kept = section.diagnostics.slice(0, room);
147
+ included += kept.length;
148
+ parts.push(`${section.path}:\n${kept.map(entryLine).join("\n")}`);
149
+ }
150
+ const omitted = total - included;
151
+ if (omitted > 0) parts.push(`… ${omitted} more`);
152
+
153
+ let body = parts.join("\n\n");
154
+ if (body.length > MAX_CHARS) body = `${body.slice(0, MAX_CHARS)}…[truncated]`;
155
+
156
+ return `<new-diagnostics>The following new diagnostic issues were detected:\n\n${body}\n</new-diagnostics>`;
157
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * What to tell the model when MCP state changes after the first request (pure).
3
+ *
4
+ * The MCP instructions and failed-servers reminders are `first-prepend` blocks
5
+ * on message 1. Before the first request they are simply (re)written; after it
6
+ * message 1 is part of every request's cached prefix, so they are FROZEN and
7
+ * changes ride one-shot notices where the model reads next (same rule as the
8
+ * deferred-tools listing, tool-search/announce.ts). `McpAnnounced` records what
9
+ * the model has been told; `mcpDelta` compares it with the live state.
10
+ */
11
+
12
+ import { mcpFailuresReminder, mcpInstructionsReminder } from "./client.ts";
13
+
14
+ export interface McpAnnounced {
15
+ /** Servers whose instructions the model has been given, with the text it saw. */
16
+ instructed: Map<string, string>;
17
+ /** Servers the model has been told failed, with the error it saw. */
18
+ failed: Map<string, string>;
19
+ }
20
+
21
+ export interface McpSnapshot {
22
+ connected: Array<{ name: string; instructions?: string }>;
23
+ failed: Array<{ name: string; error: string }>;
24
+ }
25
+
26
+ export interface McpDelta {
27
+ /** Connected servers whose instructions the model has not seen (a new server, or new text after a reconnect). */
28
+ newInstructions: Array<{ name: string; instructions: string }>;
29
+ /** Failures the model has not been told about (a new server, or a new error). */
30
+ newFailures: Array<{ name: string; error: string }>;
31
+ /** Servers the model was told had failed that are connected now. */
32
+ recovered: string[];
33
+ /** Servers whose instructions the model saw that are no longer connected and not reported failed. */
34
+ dropped: string[];
35
+ }
36
+
37
+ export function emptyAnnounced(): McpAnnounced {
38
+ return { instructed: new Map(), failed: new Map() };
39
+ }
40
+
41
+ /** What message 1 says after it was (re)written from `snapshot`, before the first request. */
42
+ export function announcedFrom(snapshot: McpSnapshot): McpAnnounced {
43
+ return {
44
+ instructed: new Map(
45
+ snapshot.connected.filter((c) => c.instructions).map((c) => [c.name, c.instructions as string]),
46
+ ),
47
+ failed: new Map(snapshot.failed.map((f) => [f.name, f.error])),
48
+ };
49
+ }
50
+
51
+ export function mcpDelta(announced: McpAnnounced, snapshot: McpSnapshot): McpDelta {
52
+ const connectedNames = new Set(snapshot.connected.map((c) => c.name));
53
+ const failedNames = new Set(snapshot.failed.map((f) => f.name));
54
+ return {
55
+ newInstructions: snapshot.connected
56
+ .filter((c) => c.instructions && announced.instructed.get(c.name) !== c.instructions)
57
+ .map((c) => ({ name: c.name, instructions: c.instructions as string })),
58
+ newFailures: snapshot.failed.filter((f) => announced.failed.get(f.name) !== f.error),
59
+ recovered: [...announced.failed.keys()].filter((name) => connectedNames.has(name)),
60
+ dropped: [...announced.instructed.keys()].filter((name) => !connectedNames.has(name) && !failedNames.has(name)),
61
+ };
62
+ }
63
+
64
+ export function applyMcpDelta(announced: McpAnnounced, delta: McpDelta): void {
65
+ for (const s of delta.newInstructions) announced.instructed.set(s.name, s.instructions);
66
+ for (const f of delta.newFailures) announced.failed.set(f.name, f.error);
67
+ for (const name of delta.recovered) announced.failed.delete(name);
68
+ for (const name of delta.dropped) announced.instructed.delete(name);
69
+ }
70
+
71
+ /** One-shot notice texts for a delta, in the order they should be queued. Empty when nothing changed. */
72
+ export function mcpChangeNotices(delta: McpDelta): string[] {
73
+ const notices: string[] = [];
74
+ if (delta.recovered.length > 0) {
75
+ notices.push(
76
+ `MCP server${delta.recovered.length === 1 ? "" : "s"} ${delta.recovered.join(", ")} ` +
77
+ "connected after this conversation started (an earlier notice reported a connection failure). " +
78
+ "The server's tools are available as deferred tools — load them with tool_search.",
79
+ );
80
+ }
81
+ if (delta.newInstructions.length > 0) {
82
+ const names = delta.newInstructions.map((s) => s.name);
83
+ notices.push(
84
+ `MCP server${names.length === 1 ? "" : "s"} ${names.join(", ")} connected after this conversation started; ` +
85
+ "the tools are available as deferred tools (load with tool_search). Server instructions:\n\n" +
86
+ mcpInstructionsReminder(delta.newInstructions.map((s) => ({ server: { name: s.name }, instructions: s.instructions }))),
87
+ );
88
+ }
89
+ if (delta.newFailures.length > 0) {
90
+ notices.push(mcpFailuresReminder(delta.newFailures));
91
+ }
92
+ if (delta.dropped.length > 0) {
93
+ notices.push(
94
+ `MCP server${delta.dropped.length === 1 ? "" : "s"} ${delta.dropped.join(", ")} disconnected; ` +
95
+ 'their tools answer "not connected" until the server is reconnected (the user can do that in /mcp).',
96
+ );
97
+ }
98
+ return notices;
99
+ }