@kkelly-offical/kkcode 0.1.2

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 (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,394 @@
1
+ import { createHttpMcpClient } from "./client-http.mjs"
2
+ import { createStdioMcpClient } from "./client-stdio.mjs"
3
+ import { createSseMcpClient } from "./client-sse.mjs"
4
+ import { EventBus } from "../core/events.mjs"
5
+ import { EVENT_TYPES } from "../core/constants.mjs"
6
+ import { readFile } from "node:fs/promises"
7
+ import { exec } from "node:child_process"
8
+ import { promisify } from "node:util"
9
+ import { join } from "node:path"
10
+ import { homedir } from "node:os"
11
+
12
+ const state = {
13
+ loaded: false,
14
+ servers: new Map(),
15
+ tools: new Map(),
16
+ prompts: new Map(),
17
+ health: new Map(),
18
+ configured: new Map(),
19
+ loadedAt: 0,
20
+ lastSignature: "",
21
+ initPromise: null
22
+ }
23
+
24
+ function normalizeTool(serverName, tool) {
25
+ const id = `mcp_${serverName}_${tool.name}`
26
+ return {
27
+ id,
28
+ server: serverName,
29
+ name: tool.name,
30
+ description: tool.description || `${serverName}:${tool.name}`,
31
+ inputSchema: tool.inputSchema || tool.input_schema || { type: "object", properties: {}, required: [] }
32
+ }
33
+ }
34
+
35
+ function normalizePrompt(serverName, prompt) {
36
+ const id = `mcp_${serverName}_${prompt.name}`
37
+ return {
38
+ id,
39
+ server: serverName,
40
+ name: prompt.name,
41
+ description: prompt.description || `${serverName}:${prompt.name}`,
42
+ arguments: prompt.arguments || []
43
+ }
44
+ }
45
+
46
+ const execAsync = promisify(exec)
47
+ async function ensureGlobalPackage(pkg) {
48
+ const name = pkg.replace(/@[^/]*$/, "")
49
+ try {
50
+ await execAsync(`npm list -g ${name}`, { timeout: 10000 })
51
+ } catch {
52
+ await execAsync(`npm install -g ${pkg}`, { timeout: 120000 })
53
+ }
54
+ }
55
+
56
+ function resolveTransport(server = {}) {
57
+ const transport = String(server.transport || server.type || "stdio").toLowerCase()
58
+ if (transport === "http") return "http"
59
+ if (transport === "sse" || transport === "streamable-http") return "sse"
60
+ return "stdio"
61
+ }
62
+
63
+ function createClient(name, server) {
64
+ const transport = resolveTransport(server)
65
+ if (transport === "sse") return createSseMcpClient(name, server)
66
+ if (transport === "http") return createHttpMcpClient(name, server)
67
+ return createStdioMcpClient(name, server)
68
+ }
69
+
70
+ function setHealth(name, serverConfig = {}, patch = {}) {
71
+ const prev = state.health.get(name) || {
72
+ name,
73
+ transport: resolveTransport(serverConfig),
74
+ ok: false,
75
+ reason: "not_checked",
76
+ error: null,
77
+ lastCheckedAt: 0
78
+ }
79
+ const next = {
80
+ ...prev,
81
+ ...patch,
82
+ name,
83
+ transport: patch.transport || prev.transport || resolveTransport(serverConfig),
84
+ lastCheckedAt: Date.now()
85
+ }
86
+ state.health.set(name, next)
87
+ return next
88
+ }
89
+
90
+ /**
91
+ * Dynamic discovery: load MCP server configs from well-known project files.
92
+ * Checks (in order, merged):
93
+ * .mcp.json — Claude Code / VS Code convention
94
+ * .mcp/config.json — directory-based convention
95
+ * .kkcode/mcp.json — kkcode-specific
96
+ * ~/.kkcode/mcp.json — global user-level
97
+ */
98
+ async function discoverProjectServers(cwd) {
99
+ const candidates = [
100
+ join(cwd, ".mcp.json"),
101
+ join(cwd, ".mcp", "config.json"),
102
+ join(cwd, ".kkcode", "mcp.json"),
103
+ join(homedir(), ".kkcode", "mcp.json")
104
+ ]
105
+ const merged = {}
106
+ for (const filePath of candidates) {
107
+ try {
108
+ const raw = await readFile(filePath, "utf-8")
109
+ const parsed = JSON.parse(raw)
110
+ const servers = parsed?.servers || parsed?.mcpServers || {}
111
+ for (const [name, cfg] of Object.entries(servers)) {
112
+ if (!merged[name]) merged[name] = cfg
113
+ }
114
+ } catch {
115
+ // ignore missing/invalid files
116
+ }
117
+ }
118
+ return merged
119
+ }
120
+
121
+ async function connectServer(name, server) {
122
+ const transport = resolveTransport(server)
123
+ let client
124
+ try {
125
+ client = createClient(name, server)
126
+ } catch (error) {
127
+ const health = setHealth(name, server, {
128
+ ok: false,
129
+ reason: error.reason || "unknown",
130
+ error: error.message,
131
+ transport
132
+ })
133
+ await EventBus.emit({
134
+ type: EVENT_TYPES.MCP_HEALTH,
135
+ payload: { server: name, ...health }
136
+ })
137
+ return null
138
+ }
139
+
140
+ let health
141
+ try {
142
+ health = await client.health()
143
+ } catch (error) {
144
+ health = { ok: false, reason: error.reason || "unknown", error: error.message || String(error) }
145
+ }
146
+
147
+ const normalizedHealth = setHealth(name, server, {
148
+ ok: Boolean(health?.ok),
149
+ reason: health?.reason || (health?.ok ? "ok" : "unknown"),
150
+ error: health?.error || null,
151
+ phase: health?.phase || null,
152
+ transport
153
+ })
154
+
155
+ await EventBus.emit({
156
+ type: EVENT_TYPES.MCP_HEALTH,
157
+ payload: { server: name, ...normalizedHealth }
158
+ })
159
+
160
+ if (!normalizedHealth.ok) return null
161
+
162
+ state.servers.set(name, client)
163
+
164
+ // Discover tools
165
+ try {
166
+ const tools = await client.listTools()
167
+ for (const tool of tools) {
168
+ const normalized = normalizeTool(name, tool)
169
+ state.tools.set(normalized.id, normalized)
170
+ }
171
+ } catch (error) {
172
+ setHealth(name, server, {
173
+ ok: false,
174
+ reason: error.reason || "unknown",
175
+ error: `listTools failed: ${error.message}`
176
+ })
177
+ state.servers.delete(name)
178
+ await EventBus.emit({
179
+ type: EVENT_TYPES.MCP_HEALTH,
180
+ payload: { server: name, ...state.health.get(name) }
181
+ })
182
+ return null
183
+ }
184
+
185
+ // Discover prompts (optional)
186
+ if (typeof client.listPrompts === "function") {
187
+ try {
188
+ const prompts = await client.listPrompts()
189
+ for (const prompt of prompts) {
190
+ const normalized = normalizePrompt(name, prompt)
191
+ state.prompts.set(normalized.id, normalized)
192
+ }
193
+ } catch {
194
+ // optional capability
195
+ }
196
+ }
197
+
198
+ return client
199
+ }
200
+
201
+ async function reinitialize(config, { force = false, cwd = null } = {}) {
202
+ const ttlMs = Math.max(0, Number(config?.runtime?.mcp_refresh_ttl_ms || 60000))
203
+ const effectiveCwd = cwd || process.cwd()
204
+ const sig = JSON.stringify({
205
+ mcp: config?.mcp || {},
206
+ runtime: config?.runtime || {},
207
+ cwd: effectiveCwd
208
+ })
209
+
210
+ const cacheValid = state.loaded && !force && state.lastSignature === sig && Date.now() - state.loadedAt <= ttlMs
211
+ if (cacheValid) return
212
+
213
+ for (const [, client] of state.servers) {
214
+ if (typeof client.shutdown === "function") client.shutdown()
215
+ }
216
+ state.loaded = false
217
+ state.servers.clear()
218
+ state.tools.clear()
219
+ state.prompts.clear()
220
+ state.health.clear()
221
+ state.configured.clear()
222
+
223
+ // Built-in MCP servers (user config can override or disable with enabled: false)
224
+ try { await ensureGlobalPackage("@upstash/context7-mcp@latest") } catch {}
225
+ const builtinServers = {
226
+ context7: {
227
+ command: "context7-mcp",
228
+ args: [],
229
+ timeout_ms: 30000,
230
+ framing: "newline"
231
+ }
232
+ }
233
+ const configServers = config?.mcp?.servers || {}
234
+ const discoveredServers = config?.mcp?.auto_discover !== false
235
+ ? await discoverProjectServers(effectiveCwd)
236
+ : {}
237
+ const allServers = { ...builtinServers, ...discoveredServers, ...configServers }
238
+
239
+ for (const [name, serverConfig] of Object.entries(allServers)) {
240
+ state.configured.set(name, serverConfig)
241
+ if (serverConfig?.enabled === false) {
242
+ setHealth(name, serverConfig, {
243
+ ok: false,
244
+ reason: "disabled",
245
+ error: null
246
+ })
247
+ } else {
248
+ setHealth(name, serverConfig, {
249
+ ok: false,
250
+ reason: "not_checked",
251
+ error: null
252
+ })
253
+ }
254
+ }
255
+
256
+ const entries = Object.entries(allServers).filter(([, serverConfig]) => serverConfig?.enabled !== false)
257
+ await Promise.allSettled(entries.map(([name, serverConfig]) => connectServer(name, serverConfig)))
258
+
259
+ state.loaded = true
260
+ state.loadedAt = Date.now()
261
+ state.lastSignature = sig
262
+ }
263
+
264
+ export const McpRegistry = {
265
+ async initialize(config, { force = false, cwd = null } = {}) {
266
+ if (state.initPromise) {
267
+ await state.initPromise
268
+ if (!force) return
269
+ }
270
+ state.initPromise = reinitialize(config, { force, cwd })
271
+ try {
272
+ await state.initPromise
273
+ } finally {
274
+ state.initPromise = null
275
+ }
276
+ },
277
+
278
+ isReady() {
279
+ return state.loaded
280
+ },
281
+
282
+ listServers() {
283
+ return [...state.servers.keys()]
284
+ },
285
+
286
+ serverInfo(name) {
287
+ const health = state.health.get(name)
288
+ if (!health) return null
289
+ return {
290
+ name,
291
+ transport: health.transport,
292
+ lastHealth: health.ok ? "ok" : "fail",
293
+ reason: health.reason || "unknown",
294
+ lastError: health.error || null
295
+ }
296
+ },
297
+
298
+ healthSnapshot() {
299
+ return [...state.health.entries()]
300
+ .map(([name, health]) => ({
301
+ name,
302
+ transport: health.transport || "stdio",
303
+ ok: Boolean(health.ok),
304
+ reason: health.reason || "unknown",
305
+ error: health.error || null,
306
+ phase: health.phase || null,
307
+ configured: state.configured.has(name),
308
+ enabled: state.configured.get(name)?.enabled !== false,
309
+ lastCheckedAt: health.lastCheckedAt || 0
310
+ }))
311
+ .sort((a, b) => a.name.localeCompare(b.name))
312
+ },
313
+
314
+ listTools() {
315
+ return [...state.tools.values()]
316
+ },
317
+
318
+ listPrompts() {
319
+ return [...state.prompts.values()]
320
+ },
321
+
322
+ async getPrompt(promptId, args = {}) {
323
+ const prompt = state.prompts.get(promptId)
324
+ if (!prompt) throw new Error(`mcp prompt not found: ${promptId}`)
325
+ const client = state.servers.get(prompt.server)
326
+ if (!client || typeof client.getPrompt !== "function") {
327
+ throw new Error(`mcp server "${prompt.server}" does not support prompts/get`)
328
+ }
329
+ return client.getPrompt(prompt.name, args)
330
+ },
331
+
332
+ async listResources(serverName) {
333
+ const client = state.servers.get(serverName)
334
+ if (!client) return []
335
+ return client.listResources()
336
+ },
337
+
338
+ async listTemplates(serverName) {
339
+ const client = state.servers.get(serverName)
340
+ if (!client) return []
341
+ return client.listTemplates()
342
+ },
343
+
344
+ async callTool(toolId, args = {}, signal = null) {
345
+ const tool = state.tools.get(toolId)
346
+ if (!tool) throw new Error(`mcp tool not found: ${toolId}`)
347
+ const client = state.servers.get(tool.server)
348
+ if (!client) throw new Error(`mcp server not found: ${tool.server}`)
349
+ return client.callTool(tool.name, args, signal)
350
+ },
351
+
352
+ async addServer(name, serverConfig) {
353
+ if (state.servers.has(name)) {
354
+ const existing = state.servers.get(name)
355
+ if (typeof existing.shutdown === "function") existing.shutdown()
356
+ state.servers.delete(name)
357
+ for (const [id, t] of state.tools) {
358
+ if (t.server === name) state.tools.delete(id)
359
+ }
360
+ for (const [id, p] of state.prompts) {
361
+ if (p.server === name) state.prompts.delete(id)
362
+ }
363
+ }
364
+ state.configured.set(name, serverConfig)
365
+ return connectServer(name, serverConfig)
366
+ },
367
+
368
+ removeServer(name) {
369
+ const client = state.servers.get(name)
370
+ if (client && typeof client.shutdown === "function") client.shutdown()
371
+ state.servers.delete(name)
372
+ state.configured.delete(name)
373
+ state.health.delete(name)
374
+ for (const [id, t] of state.tools) {
375
+ if (t.server === name) state.tools.delete(id)
376
+ }
377
+ for (const [id, p] of state.prompts) {
378
+ if (p.server === name) state.prompts.delete(id)
379
+ }
380
+ },
381
+
382
+ shutdown() {
383
+ for (const [, client] of state.servers) {
384
+ if (typeof client.shutdown === "function") client.shutdown()
385
+ }
386
+ state.servers.clear()
387
+ state.tools.clear()
388
+ state.prompts.clear()
389
+ state.health.clear()
390
+ state.configured.clear()
391
+ state.loaded = false
392
+ state.lastSignature = ""
393
+ }
394
+ }
@@ -0,0 +1,127 @@
1
+ const CRLFCRLF = Buffer.from("\r\n\r\n", "utf8")
2
+ const NEWLINE = 0x0a
3
+
4
+ function toBuffer(chunk) {
5
+ if (Buffer.isBuffer(chunk)) return chunk
6
+ if (typeof chunk === "string") return Buffer.from(chunk, "utf8")
7
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk)
8
+ return Buffer.from(String(chunk || ""), "utf8")
9
+ }
10
+
11
+ function parseContentLengthHeader(headerText) {
12
+ const match = /(?:^|\r?\n)content-length:\s*(\d+)\s*(?:\r?\n|$)/i.exec(headerText)
13
+ if (!match) return null
14
+ const len = Number(match[1])
15
+ if (!Number.isFinite(len) || len < 0) return null
16
+ return len
17
+ }
18
+
19
+ function consumeContentLengthFrame(buffer, maxFrameBytes) {
20
+ const headerEnd = buffer.indexOf(CRLFCRLF)
21
+ if (headerEnd === -1) return { type: "need_more" }
22
+ const headerText = buffer.subarray(0, headerEnd).toString("utf8")
23
+ const length = parseContentLengthHeader(headerText)
24
+ if (length === null) return { type: "invalid_header" }
25
+ if (length > maxFrameBytes) return { type: "invalid_size", size: length }
26
+ const total = headerEnd + CRLFCRLF.length + length
27
+ if (buffer.length < total) return { type: "need_more" }
28
+ const payload = buffer
29
+ .subarray(headerEnd + CRLFCRLF.length, total)
30
+ .toString("utf8")
31
+ const rest = buffer.subarray(total)
32
+ return { type: "ok", payload, rest }
33
+ }
34
+
35
+ function consumeNewlineFrame(buffer) {
36
+ const newlineIdx = buffer.indexOf(NEWLINE)
37
+ if (newlineIdx === -1) return { type: "need_more" }
38
+ const rawLine = buffer.subarray(0, newlineIdx).toString("utf8")
39
+ const rest = buffer.subarray(newlineIdx + 1)
40
+ const payload = rawLine.trim()
41
+ if (!payload) return { type: "empty", rest }
42
+ return { type: "ok", payload, rest }
43
+ }
44
+
45
+ function dropLeadingCrlf(buffer) {
46
+ let cursor = 0
47
+ while (cursor < buffer.length) {
48
+ const c = buffer[cursor]
49
+ if (c !== 0x0d && c !== 0x0a) break
50
+ cursor += 1
51
+ }
52
+ return cursor > 0 ? buffer.subarray(cursor) : buffer
53
+ }
54
+
55
+ function seemsContentLength(buffer) {
56
+ if (!buffer.length) return false
57
+ const probe = buffer.subarray(0, Math.min(buffer.length, 32)).toString("ascii").toLowerCase()
58
+ return probe.startsWith("content-length:")
59
+ }
60
+
61
+ export function encodeRpcMessage(message, framing = "content-length") {
62
+ const payload = JSON.stringify(message)
63
+ if (framing === "newline") return `${payload}\n`
64
+ const size = Buffer.byteLength(payload, "utf8")
65
+ return `Content-Length: ${size}\r\n\r\n${payload}`
66
+ }
67
+
68
+ export function createStdioFramingDecoder({ framing = "auto", maxFrameBytes = 8 * 1024 * 1024 } = {}) {
69
+ let buffer = Buffer.alloc(0)
70
+
71
+ function push(chunk) {
72
+ buffer = Buffer.concat([buffer, toBuffer(chunk)])
73
+ const messages = []
74
+
75
+ while (true) {
76
+ if (!buffer.length) break
77
+
78
+ if (framing === "content-length") {
79
+ const parsed = consumeContentLengthFrame(buffer, maxFrameBytes)
80
+ if (parsed.type === "need_more") break
81
+ if (parsed.type === "invalid_header") throw new Error("invalid content-length header")
82
+ if (parsed.type === "invalid_size") throw new Error(`content-length exceeds limit: ${parsed.size}`)
83
+ messages.push(parsed.payload)
84
+ buffer = parsed.rest
85
+ continue
86
+ }
87
+
88
+ if (framing === "newline") {
89
+ const parsed = consumeNewlineFrame(buffer)
90
+ if (parsed.type === "need_more") break
91
+ buffer = parsed.rest
92
+ if (parsed.type === "ok") messages.push(parsed.payload)
93
+ continue
94
+ }
95
+
96
+ // auto mode: prefer standard content-length frames, then fallback to newline JSON
97
+ buffer = dropLeadingCrlf(buffer)
98
+ if (!buffer.length) break
99
+
100
+ if (seemsContentLength(buffer)) {
101
+ const parsed = consumeContentLengthFrame(buffer, maxFrameBytes)
102
+ if (parsed.type === "need_more") break
103
+ if (parsed.type === "invalid_header") throw new Error("invalid content-length header")
104
+ if (parsed.type === "invalid_size") throw new Error(`content-length exceeds limit: ${parsed.size}`)
105
+ messages.push(parsed.payload)
106
+ buffer = parsed.rest
107
+ continue
108
+ }
109
+
110
+ const parsed = consumeNewlineFrame(buffer)
111
+ if (parsed.type === "need_more") break
112
+ buffer = parsed.rest
113
+ if (parsed.type === "ok") messages.push(parsed.payload)
114
+ }
115
+
116
+ return messages
117
+ }
118
+
119
+ function reset() {
120
+ buffer = Buffer.alloc(0)
121
+ }
122
+
123
+ return {
124
+ push,
125
+ reset
126
+ }
127
+ }