@kkelly-offical/kkcode 0.1.2 → 0.1.6

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 (58) hide show
  1. package/README.md +120 -178
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +41 -0
  4. package/src/agent/prompt/frontend-designer.txt +58 -0
  5. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  6. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  7. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  8. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  9. package/src/config/defaults.mjs +260 -195
  10. package/src/config/schema.mjs +71 -6
  11. package/src/core/constants.mjs +91 -46
  12. package/src/index.mjs +1 -1
  13. package/src/knowledge/frontend-aesthetics.txt +39 -0
  14. package/src/knowledge/loader.mjs +2 -1
  15. package/src/knowledge/tailwind.txt +12 -3
  16. package/src/mcp/client-http.mjs +141 -157
  17. package/src/mcp/client-sse.mjs +288 -286
  18. package/src/mcp/client-stdio.mjs +533 -451
  19. package/src/mcp/constants.mjs +2 -0
  20. package/src/mcp/registry.mjs +479 -394
  21. package/src/mcp/stdio-framing.mjs +133 -127
  22. package/src/mcp/tool-result.mjs +24 -0
  23. package/src/observability/index.mjs +42 -0
  24. package/src/observability/metrics.mjs +137 -0
  25. package/src/observability/tracer.mjs +137 -0
  26. package/src/orchestration/background-manager.mjs +372 -358
  27. package/src/orchestration/background-worker.mjs +305 -245
  28. package/src/orchestration/longagent-manager.mjs +171 -116
  29. package/src/orchestration/stage-scheduler.mjs +728 -489
  30. package/src/permission/exec-policy.mjs +9 -11
  31. package/src/provider/anthropic.mjs +1 -0
  32. package/src/provider/openai.mjs +340 -339
  33. package/src/provider/retry-policy.mjs +68 -68
  34. package/src/provider/router.mjs +241 -228
  35. package/src/provider/sse.mjs +104 -91
  36. package/src/repl.mjs +1 -1
  37. package/src/session/checkpoint.mjs +66 -3
  38. package/src/session/engine.mjs +227 -225
  39. package/src/session/longagent-4stage.mjs +460 -0
  40. package/src/session/longagent-hybrid.mjs +1081 -0
  41. package/src/session/longagent-plan.mjs +365 -329
  42. package/src/session/longagent-project-memory.mjs +53 -0
  43. package/src/session/longagent-scaffold.mjs +291 -100
  44. package/src/session/longagent-task-bus.mjs +54 -0
  45. package/src/session/longagent-utils.mjs +472 -0
  46. package/src/session/longagent.mjs +884 -1462
  47. package/src/session/project-context.mjs +30 -0
  48. package/src/session/store.mjs +510 -503
  49. package/src/session/task-validator.mjs +4 -3
  50. package/src/skill/builtin/design.mjs +76 -0
  51. package/src/skill/builtin/frontend.mjs +8 -0
  52. package/src/skill/registry.mjs +390 -336
  53. package/src/storage/ghost-commit-store.mjs +18 -8
  54. package/src/tool/executor.mjs +11 -0
  55. package/src/tool/git-auto.mjs +0 -19
  56. package/src/tool/registry.mjs +71 -37
  57. package/src/ui/activity-renderer.mjs +664 -410
  58. package/src/util/git.mjs +23 -0
@@ -1,46 +1,91 @@
1
- export const MODES = ["ask", "plan", "agent", "longagent"]
2
-
3
- export const TOOL_STATUSES = ["running", "completed", "error", "cancelled"]
4
-
5
- export const PERMISSION_DECISIONS = ["allow_once", "allow_session", "deny"]
6
-
7
- export const DEFAULT_MAX_STEPS = 8
8
- export const DEFAULT_REQUEST_TIMEOUT_MS = 120000
9
- export const DEFAULT_RETRY_ATTEMPTS = 3
10
- export const DEFAULT_LONGAGENT_RETRY_STORM_THRESHOLD = 3
11
- export const DEFAULT_LONGAGENT_TOKEN_ALERT_THRESHOLD = 120000
12
-
13
- export const EVENT_TYPES = {
14
- TURN_START: "turn.start",
15
- TURN_STEP_START: "turn.step.start",
16
- TURN_STEP_FINISH: "turn.step.finish",
17
- TURN_FINISH: "turn.finish",
18
- TURN_ERROR: "turn.error",
19
- TOOL_START: "tool.start",
20
- TOOL_FINISH: "tool.finish",
21
- TOOL_ERROR: "tool.error",
22
- PERMISSION_ASKED: "permission.asked",
23
- PERMISSION_DECIDED: "permission.decided",
24
- REVIEW_DECISION: "review.decision",
25
- MCP_HEALTH: "mcp.health",
26
- MCP_REQUEST: "mcp.request",
27
- LONGAGENT_HEARTBEAT: "longagent.heartbeat",
28
- LONGAGENT_ALERT: "longagent.alert",
29
- LONGAGENT_PHASE_CHANGED: "longagent.phase.changed",
30
- LONGAGENT_GATE_CHECKED: "longagent.gate.checked",
31
- LONGAGENT_RECOVERY_ENTERED: "longagent.recovery.entered",
32
- LONGAGENT_INTAKE_STARTED: "longagent.intake.started",
33
- LONGAGENT_PLAN_FROZEN: "longagent.plan.frozen",
34
- LONGAGENT_STAGE_STARTED: "longagent.stage.started",
35
- LONGAGENT_STAGE_TASK_DISPATCHED: "longagent.stage.task.dispatched",
36
- LONGAGENT_STAGE_TASK_FINISHED: "longagent.stage.task.finished",
37
- LONGAGENT_STAGE_FINISHED: "longagent.stage.finished",
38
- LONGAGENT_SCAFFOLD_COMPLETE: "longagent.scaffold.complete",
39
- LONGAGENT_GIT_BRANCH_CREATED: "longagent.git.branch.created",
40
- LONGAGENT_GIT_STAGE_COMMITTED: "longagent.git.stage.committed",
41
- LONGAGENT_GIT_MERGED: "longagent.git.merged",
42
- SESSION_COMPACTED: "session.compacted",
43
- TURN_USAGE_UPDATE: "turn.usage.update",
44
- STREAM_TEXT_START: "stream.text.start",
45
- STREAM_THINKING_START: "stream.thinking.start"
46
- }
1
+ export const MODES = ["ask", "plan", "agent", "longagent"]
2
+
3
+ export const TOOL_STATUSES = ["running", "completed", "error", "cancelled"]
4
+
5
+ export const PERMISSION_DECISIONS = ["allow_once", "allow_session", "deny"]
6
+
7
+ export const DEFAULT_MAX_STEPS = 8
8
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 120000
9
+ export const DEFAULT_RETRY_ATTEMPTS = 3
10
+ export const DEFAULT_LONGAGENT_RETRY_STORM_THRESHOLD = 3
11
+ export const DEFAULT_LONGAGENT_TOKEN_ALERT_THRESHOLD = 120000
12
+
13
+ export const LONGAGENT_4STAGE_STAGES = {
14
+ PREVIEW: "preview",
15
+ BLUEPRINT: "blueprint",
16
+ CODING: "coding",
17
+ DEBUGGING: "debugging"
18
+ }
19
+
20
+ export const EVENT_TYPES = {
21
+ TURN_START: "turn.start",
22
+ TURN_STEP_START: "turn.step.start",
23
+ TURN_STEP_FINISH: "turn.step.finish",
24
+ TURN_FINISH: "turn.finish",
25
+ TURN_ERROR: "turn.error",
26
+ TOOL_START: "tool.start",
27
+ TOOL_FINISH: "tool.finish",
28
+ TOOL_ERROR: "tool.error",
29
+ PERMISSION_ASKED: "permission.asked",
30
+ PERMISSION_DECIDED: "permission.decided",
31
+ REVIEW_DECISION: "review.decision",
32
+ MCP_HEALTH: "mcp.health",
33
+ MCP_REQUEST: "mcp.request",
34
+ MCP_RECONNECT: "mcp.reconnect",
35
+ MCP_CIRCUIT_OPEN: "mcp.circuit_open",
36
+ MCP_CIRCUIT_CLOSE: "mcp.circuit_close",
37
+ LONGAGENT_HEARTBEAT: "longagent.heartbeat",
38
+ LONGAGENT_ALERT: "longagent.alert",
39
+ LONGAGENT_PHASE_CHANGED: "longagent.phase.changed",
40
+ LONGAGENT_GATE_CHECKED: "longagent.gate.checked",
41
+ LONGAGENT_RECOVERY_ENTERED: "longagent.recovery.entered",
42
+ LONGAGENT_INTAKE_STARTED: "longagent.intake.started",
43
+ LONGAGENT_PLAN_FROZEN: "longagent.plan.frozen",
44
+ LONGAGENT_STAGE_STARTED: "longagent.stage.started",
45
+ LONGAGENT_STAGE_TASK_DISPATCHED: "longagent.stage.task.dispatched",
46
+ LONGAGENT_STAGE_TASK_FINISHED: "longagent.stage.task.finished",
47
+ LONGAGENT_STAGE_FINISHED: "longagent.stage.finished",
48
+ LONGAGENT_SCAFFOLD_COMPLETE: "longagent.scaffold.complete",
49
+ LONGAGENT_GIT_BRANCH_CREATED: "longagent.git.branch.created",
50
+ LONGAGENT_GIT_STAGE_COMMITTED: "longagent.git.stage.committed",
51
+ LONGAGENT_GIT_MERGED: "longagent.git.merged",
52
+ LONGAGENT_4STAGE_PREVIEW_START: "longagent.4stage.preview.start",
53
+ LONGAGENT_4STAGE_PREVIEW_COMPLETE: "longagent.4stage.preview.complete",
54
+ LONGAGENT_4STAGE_BLUEPRINT_START: "longagent.4stage.blueprint.start",
55
+ LONGAGENT_4STAGE_BLUEPRINT_COMPLETE: "longagent.4stage.blueprint.complete",
56
+ LONGAGENT_4STAGE_CODING_START: "longagent.4stage.coding.start",
57
+ LONGAGENT_4STAGE_CODING_COMPLETE: "longagent.4stage.coding.complete",
58
+ LONGAGENT_4STAGE_DEBUGGING_START: "longagent.4stage.debugging.start",
59
+ LONGAGENT_4STAGE_DEBUGGING_COMPLETE: "longagent.4stage.debugging.complete",
60
+ LONGAGENT_4STAGE_RETURN_TO_CODING: "longagent.4stage.return_to_coding",
61
+ LONGAGENT_HYBRID_PREVIEW_START: "longagent.hybrid.preview.start",
62
+ LONGAGENT_HYBRID_PREVIEW_COMPLETE: "longagent.hybrid.preview.complete",
63
+ LONGAGENT_HYBRID_BLUEPRINT_START: "longagent.hybrid.blueprint.start",
64
+ LONGAGENT_HYBRID_BLUEPRINT_COMPLETE: "longagent.hybrid.blueprint.complete",
65
+ LONGAGENT_HYBRID_DEBUGGING_START: "longagent.hybrid.debugging.start",
66
+ LONGAGENT_HYBRID_DEBUGGING_COMPLETE: "longagent.hybrid.debugging.complete",
67
+ LONGAGENT_HYBRID_RETURN_TO_CODING: "longagent.hybrid.return_to_coding",
68
+ LONGAGENT_HYBRID_BLUEPRINT_REVIEW: "longagent.hybrid.blueprint.review",
69
+ LONGAGENT_HYBRID_BLUEPRINT_VALIDATED: "longagent.hybrid.blueprint.validated",
70
+ LONGAGENT_HYBRID_CROSS_REVIEW: "longagent.hybrid.cross_review",
71
+ LONGAGENT_HYBRID_INCREMENTAL_GATE: "longagent.hybrid.incremental_gate",
72
+ LONGAGENT_HYBRID_CONTEXT_COMPRESSED: "longagent.hybrid.context_compressed",
73
+ LONGAGENT_HYBRID_BUDGET_WARNING: "longagent.hybrid.budget_warning",
74
+ LONGAGENT_HYBRID_CHECKPOINT_RESUMED: "longagent.hybrid.checkpoint_resumed",
75
+ LONGAGENT_HYBRID_REPLAN: "longagent.hybrid.replan",
76
+ LONGAGENT_HYBRID_MEMORY_LOADED: "longagent.hybrid.memory_loaded",
77
+ LONGAGENT_HYBRID_MEMORY_SAVED: "longagent.hybrid.memory_saved",
78
+ SESSION_COMPACTED: "session.compacted",
79
+ TURN_USAGE_UPDATE: "turn.usage.update",
80
+ STREAM_TEXT_START: "stream.text.start",
81
+ STREAM_THINKING_START: "stream.thinking.start",
82
+ LONGAGENT_DEGRADATION_APPLIED: "longagent.degradation.applied",
83
+ LONGAGENT_WRITE_LOOP_DETECTED: "longagent.write_loop.detected",
84
+ LONGAGENT_SEMANTIC_ERROR_REPEATED: "longagent.semantic_error.repeated",
85
+ LONGAGENT_PHASE_TIMEOUT: "longagent.phase.timeout",
86
+ LONGAGENT_GIT_CONFLICT_RESOLUTION: "longagent.git.conflict_resolution",
87
+ LONGAGENT_CHECKPOINT_CLEANED: "longagent.checkpoint.cleaned",
88
+ LONGAGENT_HYBRID_CHECKPOINT_INVALID: "longagent.hybrid.checkpoint_invalid",
89
+ LONGAGENT_STAGE_TASK_SKIPPED: "longagent.stage.task.skipped",
90
+ PROVIDER_FALLBACK: "provider.fallback"
91
+ }
package/src/index.mjs CHANGED
@@ -54,7 +54,7 @@ async function main() {
54
54
  }
55
55
 
56
56
  const program = new Command()
57
- program.name("kkcode").description("kkcode CLI").version("0.1.2")
57
+ program.name("kkcode").description("kkcode CLI").version("0.1.6")
58
58
  program.addCommand(createChatCommand())
59
59
  program.addCommand(createThemeCommand())
60
60
  program.addCommand(createUsageCommand())
@@ -0,0 +1,39 @@
1
+ <frontend_aesthetics>
2
+ ## Frontend Design Quality Rules
3
+
4
+ When building frontend UI, avoid generic "AI-generated" aesthetics. Make creative, distinctive choices.
5
+
6
+ ### Typography
7
+ - Avoid default fonts (Inter, Roboto, Arial, system-ui). Choose distinctive fonts.
8
+ - Use high contrast: pair display fonts with monospace, serif with geometric sans.
9
+ - Use extreme weight differences (200 vs 800) and 3x+ size jumps.
10
+ - Load fonts from Google Fonts via <link> or @import.
11
+
12
+ ### Color
13
+ - Define ALL colors as CSS variables in :root.
14
+ - Use a dominant color with 1-2 sharp accents. Avoid evenly-distributed palettes.
15
+ - Draw from real palettes: IDE themes (Nord, Catppuccin, Dracula), nature, cultural aesthetics.
16
+ - AVOID: purple gradients on white, generic blue buttons, gray-on-gray cards.
17
+
18
+ ### Motion
19
+ - One high-impact animation per page (staggered reveal on load).
20
+ - Micro-interactions: hover lift, focus glow, press feedback.
21
+ - Use CSS transitions/animations. Use animation-delay for staggered effects.
22
+
23
+ ### Layout
24
+ - CSS Grid for page structure, Flexbox for components.
25
+ - Generous whitespace. Consistent spacing scale (4/8/12/16/24/32/48/64px).
26
+ - Mobile-first responsive design.
27
+
28
+ ### Depth & Atmosphere
29
+ - Layered gradients, subtle patterns, backdrop-filter for glass effects.
30
+ - Elevation hierarchy via box-shadow (not just border).
31
+ - Noise/grain textures for premium feel.
32
+
33
+ ### What NOT to do
34
+ - Cookie-cutter card grids with identical rounded corners
35
+ - Generic hero with centered text + gradient
36
+ - border-radius: 9999px on everything
37
+ - Gray placeholder text that looks like wireframe
38
+ - No visual rhythm (identical spacing everywhere)
39
+ </frontend_aesthetics>
@@ -58,7 +58,8 @@ const LANGUAGE_MAP = {
58
58
  // Project type → scenario knowledge
59
59
  const TYPE_MAP = {
60
60
  backend: ["api-design.txt"],
61
- fullstack: ["api-design.txt"],
61
+ fullstack: ["api-design.txt", "frontend-aesthetics.txt"],
62
+ frontend: ["frontend-aesthetics.txt"],
62
63
  }
63
64
 
64
65
  // Feature → knowledge file
@@ -4,7 +4,16 @@ Tailwind CSS conventions:
4
4
  - Dark mode: dark: prefix with class or media strategy
5
5
  - Component patterns: extract repeated patterns with @apply in CSS or component abstraction
6
6
  - Spacing scale: p-4 = 1rem, m-2 = 0.5rem (4px base)
7
- - Colors: bg-blue-500, text-gray-900, use project's color palette
7
+ - Colors: bg-blue-500, text-gray-900, use project's color palette from tailwind.config
8
8
  - Layout: flex, grid, container mx-auto, gap utilities
9
- - Customization: tailwind.config.js for theme extension
10
- - Avoid: inline styles, arbitrary values when a utility exists
9
+ - Customization: tailwind.config.js for theme extension (colors, fonts, spacing)
10
+ - Avoid: inline styles, arbitrary values when a utility exists
11
+
12
+ Design quality with Tailwind:
13
+ - READ tailwind.config.js first — use the project's defined colors/fonts/spacing
14
+ - Prefer semantic color names (primary, accent, muted) over raw colors (blue-500)
15
+ - Use ring utilities for focus states (ring-2 ring-offset-2)
16
+ - Transition utilities: transition-all duration-200 ease-in-out for interactions
17
+ - Shadow scale: shadow-sm → shadow → shadow-md → shadow-lg for elevation
18
+ - Use group and group-hover for parent-child hover effects
19
+ - Animate: animate-pulse for loading, animate-spin for spinners
@@ -1,157 +1,141 @@
1
- import { McpError } from "../core/errors.mjs"
2
- import { EventBus } from "../core/events.mjs"
3
- import { EVENT_TYPES } from "../core/constants.mjs"
4
-
5
- function timeoutSignal(ms, parentSignal = null) {
6
- const own = AbortSignal.timeout(ms)
7
- if (!parentSignal) return own
8
- return AbortSignal.any([parentSignal, own])
9
- }
10
-
11
- function classifyHttpError(error, status = null) {
12
- const msg = String(error?.message || error || "")
13
- if (msg.includes("AbortError") || msg.includes("timeout") || msg.includes("abort")) return "timeout"
14
- if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) return "connection_refused"
15
- if (status && status >= 500) return "server_crash"
16
- if (status && status >= 400) return "bad_response"
17
- return "unknown"
18
- }
19
-
20
- function normalizeToolResult(result, serverName, toolName) {
21
- if (result?.isError) {
22
- const text = Array.isArray(result.content)
23
- ? result.content.map((item) => item?.text || "").join("\n").trim()
24
- : ""
25
- throw new McpError(text || "mcp tool returned isError", {
26
- reason: "bad_response",
27
- server: serverName,
28
- action: `tools/call:${toolName}`,
29
- phase: "request"
30
- })
31
- }
32
- const content = Array.isArray(result?.content) ? result.content : null
33
- const contentText = content
34
- ? content.map((item) => (typeof item?.text === "string" ? item.text : "")).join("\n").trim()
35
- : ""
36
- const output =
37
- contentText ||
38
- (typeof result?.output === "string" ? result.output : "") ||
39
- (typeof result === "string" ? result : JSON.stringify(result))
40
- return content ? { output, raw: result, content } : { output, raw: result }
41
- }
42
-
43
- async function requestJson({ serverName, method, url, body = null, timeoutMs = 10000, headers = {}, signal = null }) {
44
- const action = method === "GET" ? url.split("/").pop() : body?.args ? "call_tool" : "request"
45
- const startedAt = Date.now()
46
- let status = null
47
-
48
- try {
49
- const res = await fetch(url, {
50
- method,
51
- headers: {
52
- "content-type": "application/json",
53
- ...headers
54
- },
55
- body: body ? JSON.stringify(body) : undefined,
56
- signal: timeoutSignal(timeoutMs, signal)
57
- })
58
-
59
- status = res.status
60
- const elapsed = Date.now() - startedAt
61
-
62
- EventBus.emit({
63
- type: EVENT_TYPES.MCP_REQUEST,
64
- payload: { server: serverName, action, method, elapsed, status }
65
- }).catch(() => {})
66
-
67
- if (!res.ok) {
68
- const text = await res.text().catch(() => "")
69
- const reason = classifyHttpError(null, status)
70
- throw new McpError(
71
- `mcp server "${serverName}" HTTP ${status} on ${method} ${url}: ${text.slice(0, 500)}`,
72
- { reason, server: serverName, action, phase: "request", statusCode: status }
73
- )
74
- }
75
- return res.json().catch(() => ({}))
76
- } catch (error) {
77
- if (error instanceof McpError) throw error
78
- const reason = classifyHttpError(error, status)
79
- throw new McpError(
80
- `mcp server "${serverName}" ${reason} on ${method} ${url}: ${error.message}`,
81
- { reason, server: serverName, action, phase: "request", statusCode: status }
82
- )
83
- }
84
- }
85
-
86
- export function createHttpMcpClient(serverName, config) {
87
- const baseUrl = String(config.url || "").replace(/\/$/, "")
88
- const timeoutMs = Number(config.timeout_ms || 10000)
89
- const headers = config.headers || {}
90
-
91
- return {
92
- serverName,
93
- transport: "http",
94
- async health() {
95
- try {
96
- await requestJson({ serverName, method: "GET", url: `${baseUrl}/health`, timeoutMs, headers })
97
- return { ok: true }
98
- } catch (error) {
99
- return { ok: false, error: error.message, reason: error.reason || "unknown" }
100
- }
101
- },
102
- async listTools() {
103
- const out = await requestJson({ serverName, method: "GET", url: `${baseUrl}/tools`, timeoutMs, headers })
104
- return Array.isArray(out?.tools) ? out.tools : []
105
- },
106
- async listPrompts() {
107
- try {
108
- const out = await requestJson({ serverName, method: "GET", url: `${baseUrl}/prompts`, timeoutMs, headers })
109
- return Array.isArray(out?.prompts) ? out.prompts : []
110
- } catch {
111
- return []
112
- }
113
- },
114
- async getPrompt(name, args = {}) {
115
- return requestJson({
116
- serverName,
117
- method: "POST",
118
- url: `${baseUrl}/prompts/${encodeURIComponent(name)}`,
119
- body: { arguments: args },
120
- timeoutMs,
121
- headers
122
- })
123
- },
124
- async listResources() {
125
- try {
126
- const out = await requestJson({ serverName, method: "GET", url: `${baseUrl}/resources`, timeoutMs, headers })
127
- return Array.isArray(out?.resources) ? out.resources : []
128
- } catch (error) {
129
- if (error.reason === "server_crash" || error.reason === "timeout") throw error
130
- return []
131
- }
132
- },
133
- async listTemplates() {
134
- try {
135
- const out = await requestJson({ serverName, method: "GET", url: `${baseUrl}/templates`, timeoutMs, headers })
136
- return Array.isArray(out?.templates) ? out.templates : []
137
- } catch {
138
- return []
139
- }
140
- },
141
- async callTool(name, args = {}, signal = null) {
142
- const result = await requestJson({
143
- serverName,
144
- method: "POST",
145
- url: `${baseUrl}/tools/${encodeURIComponent(name)}`,
146
- body: { args },
147
- timeoutMs,
148
- headers,
149
- signal
150
- })
151
- return normalizeToolResult(result, serverName, name)
152
- },
153
- shutdown() {
154
- // HTTP client is stateless — no persistent connections to clean up
155
- }
156
- }
157
- }
1
+ import { McpError } from "../core/errors.mjs"
2
+ import { EventBus } from "../core/events.mjs"
3
+ import { EVENT_TYPES } from "../core/constants.mjs"
4
+ import { normalizeToolResult } from "./tool-result.mjs"
5
+
6
+ function timeoutSignal(ms, parentSignal = null) {
7
+ const own = AbortSignal.timeout(ms)
8
+ if (!parentSignal) return own
9
+ return AbortSignal.any([parentSignal, own])
10
+ }
11
+
12
+ function classifyHttpError(error, status = null) {
13
+ const msg = String(error?.message || error || "")
14
+ if (msg.includes("AbortError") || msg.includes("timeout") || msg.includes("abort")) return "timeout"
15
+ if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) return "connection_refused"
16
+ if (status && status >= 500) return "server_crash"
17
+ if (status && status >= 400) return "bad_response"
18
+ return "unknown"
19
+ }
20
+
21
+ async function requestJson({ serverName, method, url, body = null, timeoutMs = 10000, headers = {}, signal = null }) {
22
+ const action = method === "GET" ? url.split("/").pop() : body?.args ? "call_tool" : "request"
23
+ const startedAt = Date.now()
24
+ let status = null
25
+
26
+ try {
27
+ const res = await fetch(url, {
28
+ method,
29
+ headers: {
30
+ "content-type": "application/json",
31
+ ...headers
32
+ },
33
+ body: body ? JSON.stringify(body) : undefined,
34
+ signal: timeoutSignal(timeoutMs, signal)
35
+ })
36
+
37
+ status = res.status
38
+ const elapsed = Date.now() - startedAt
39
+
40
+ EventBus.emit({
41
+ type: EVENT_TYPES.MCP_REQUEST,
42
+ payload: { server: serverName, action, method, elapsed, status }
43
+ }).catch(() => {})
44
+
45
+ if (!res.ok) {
46
+ const text = await res.text().catch(() => "")
47
+ const reason = classifyHttpError(null, status)
48
+ throw new McpError(
49
+ `mcp server "${serverName}" HTTP ${status} on ${method} ${url}: ${text.slice(0, 500)}`,
50
+ { reason, server: serverName, action, phase: "request", statusCode: status }
51
+ )
52
+ }
53
+ return res.json().catch((parseErr) => {
54
+ const action = body?.args ? "call_tool" : "request"
55
+ EventBus.emit({
56
+ type: EVENT_TYPES.MCP_REQUEST,
57
+ payload: { server: serverName, action, warning: "malformed_json_response", detail: parseErr.message }
58
+ }).catch(() => {})
59
+ return {}
60
+ })
61
+ } catch (error) {
62
+ if (error instanceof McpError) throw error
63
+ const reason = classifyHttpError(error, status)
64
+ throw new McpError(
65
+ `mcp server "${serverName}" ${reason} on ${method} ${url}: ${error.message}`,
66
+ { reason, server: serverName, action, phase: "request", statusCode: status }
67
+ )
68
+ }
69
+ }
70
+
71
+ export function createHttpMcpClient(serverName, config) {
72
+ const baseUrl = String(config.url || "").replace(/\/$/, "")
73
+ const timeoutMs = Number(config.timeout_ms || 10000)
74
+ const headers = config.headers || {}
75
+
76
+ return {
77
+ serverName,
78
+ transport: "http",
79
+ async health() {
80
+ try {
81
+ await requestJson({ serverName, method: "GET", url: `${baseUrl}/health`, timeoutMs, headers })
82
+ return { ok: true }
83
+ } catch (error) {
84
+ return { ok: false, error: error.message, reason: error.reason || "unknown" }
85
+ }
86
+ },
87
+ async listTools() {
88
+ const out = await requestJson({ serverName, method: "GET", url: `${baseUrl}/tools`, timeoutMs, headers })
89
+ return Array.isArray(out?.tools) ? out.tools : []
90
+ },
91
+ async listPrompts() {
92
+ try {
93
+ const out = await requestJson({ serverName, method: "GET", url: `${baseUrl}/prompts`, timeoutMs, headers })
94
+ return Array.isArray(out?.prompts) ? out.prompts : []
95
+ } catch {
96
+ return []
97
+ }
98
+ },
99
+ async getPrompt(name, args = {}) {
100
+ return requestJson({
101
+ serverName,
102
+ method: "POST",
103
+ url: `${baseUrl}/prompts/${encodeURIComponent(name)}`,
104
+ body: { arguments: args },
105
+ timeoutMs,
106
+ headers
107
+ })
108
+ },
109
+ async listResources() {
110
+ try {
111
+ const out = await requestJson({ serverName, method: "GET", url: `${baseUrl}/resources`, timeoutMs, headers })
112
+ return Array.isArray(out?.resources) ? out.resources : []
113
+ } catch {
114
+ return []
115
+ }
116
+ },
117
+ async listTemplates() {
118
+ try {
119
+ const out = await requestJson({ serverName, method: "GET", url: `${baseUrl}/templates`, timeoutMs, headers })
120
+ return Array.isArray(out?.templates) ? out.templates : []
121
+ } catch {
122
+ return []
123
+ }
124
+ },
125
+ async callTool(name, args = {}, signal = null) {
126
+ const result = await requestJson({
127
+ serverName,
128
+ method: "POST",
129
+ url: `${baseUrl}/tools/${encodeURIComponent(name)}`,
130
+ body: { args },
131
+ timeoutMs,
132
+ headers,
133
+ signal
134
+ })
135
+ return normalizeToolResult(result, serverName, name)
136
+ },
137
+ shutdown() {
138
+ // HTTP client is stateless — no persistent connections to clean up
139
+ }
140
+ }
141
+ }