@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,286 +1,288 @@
1
- import { McpError } from "../core/errors.mjs"
2
- import { EventBus } from "../core/events.mjs"
3
- import { EVENT_TYPES } from "../core/constants.mjs"
4
-
5
- /**
6
- * MCP Streamable HTTP (SSE) client.
7
- *
8
- * Protocol: JSON-RPC 2.0 over HTTP POST with optional SSE response streaming.
9
- * - POST to endpoint: send JSON-RPC request, receive JSON or SSE stream
10
- * - GET to endpoint: open persistent SSE stream for server-initiated notifications
11
- * - Session management via Mcp-Session-Id header
12
- */
13
- export function createSseMcpClient(serverName, config) {
14
- const baseUrl = String(config.url || "").replace(/\/$/, "")
15
- const timeoutMs = Number(config.timeout_ms || 30000)
16
- const headers = config.headers || {}
17
-
18
- let sessionId = null
19
- let nextId = 1
20
- let initialized = false
21
- let notificationStream = null
22
-
23
- function normalizeToolResult(result, toolName) {
24
- if (result?.isError) {
25
- const text = Array.isArray(result.content)
26
- ? result.content.map((item) => item?.text || "").join("\n").trim()
27
- : ""
28
- throw new McpError(text || "mcp tool returned isError", {
29
- reason: "bad_response",
30
- server: serverName,
31
- action: `tools/call:${toolName}`,
32
- phase: "request"
33
- })
34
- }
35
- const content = Array.isArray(result?.content) ? result.content : null
36
- const contentText = content
37
- ? content.map((item) => (typeof item?.text === "string" ? item.text : "")).join("\n").trim()
38
- : ""
39
- const output =
40
- contentText ||
41
- (typeof result?.output === "string" ? result.output : "") ||
42
- (typeof result === "string" ? result : JSON.stringify(result))
43
- return content ? { output, raw: result, content } : { output, raw: result }
44
- }
45
-
46
- function buildHeaders(extra = {}) {
47
- const h = {
48
- "content-type": "application/json",
49
- accept: "application/json, text/event-stream",
50
- ...headers,
51
- ...extra
52
- }
53
- if (sessionId) h["mcp-session-id"] = sessionId
54
- return h
55
- }
56
-
57
- async function sendRequest(method, params = {}) {
58
- const id = nextId++
59
- const body = { jsonrpc: "2.0", id, method, params }
60
- const startedAt = Date.now()
61
-
62
- const controller = new AbortController()
63
- const timer = setTimeout(() => controller.abort(), timeoutMs)
64
-
65
- try {
66
- const res = await fetch(baseUrl, {
67
- method: "POST",
68
- headers: buildHeaders(),
69
- body: JSON.stringify(body),
70
- signal: controller.signal
71
- })
72
-
73
- clearTimeout(timer)
74
- const elapsed = Date.now() - startedAt
75
-
76
- // Capture session ID from response
77
- const newSessionId = res.headers.get("mcp-session-id")
78
- if (newSessionId) sessionId = newSessionId
79
-
80
- EventBus.emit({
81
- type: EVENT_TYPES.MCP_REQUEST,
82
- payload: { server: serverName, action: method, elapsed, status: res.status }
83
- }).catch(() => {})
84
-
85
- if (!res.ok) {
86
- const text = await res.text().catch(() => "")
87
- throw new McpError(
88
- `mcp server "${serverName}" HTTP ${res.status}: ${text.slice(0, 500)}`,
89
- {
90
- reason: res.status >= 500 ? "server_crash" : "bad_response",
91
- server: serverName,
92
- action: method,
93
- phase: "request",
94
- statusCode: res.status
95
- }
96
- )
97
- }
98
-
99
- const contentType = res.headers.get("content-type") || ""
100
-
101
- // SSE response — parse events and return the final result
102
- if (contentType.includes("text/event-stream")) {
103
- return await parseSseResponse(res.body, id)
104
- }
105
-
106
- // Regular JSON response
107
- const json = await res.json().catch(() => ({}))
108
- if (json.error) {
109
- throw new McpError(
110
- `mcp server "${serverName}" error: ${json.error.message || JSON.stringify(json.error)}`,
111
- { reason: "bad_response", server: serverName, action: method, code: json.error.code, phase: "request" }
112
- )
113
- }
114
- return json.result ?? json
115
- } catch (error) {
116
- clearTimeout(timer)
117
- if (error instanceof McpError) throw error
118
- const reason = error.name === "AbortError" ? "timeout" : "connection_refused"
119
- throw new McpError(
120
- `mcp server "${serverName}" ${reason}: ${error.message}`,
121
- { reason, server: serverName, action: method, phase: "request" }
122
- )
123
- }
124
- }
125
-
126
- async function parseSseResponse(body, requestId) {
127
- const reader = body.getReader()
128
- const decoder = new TextDecoder()
129
- let buffer = ""
130
- let result = null
131
-
132
- try {
133
- while (true) {
134
- const { done, value } = await reader.read()
135
- if (done) break
136
- buffer += decoder.decode(value, { stream: true })
137
-
138
- const parts = buffer.split("\n\n")
139
- buffer = parts.pop()
140
-
141
- for (const part of parts) {
142
- const event = parseSsePart(part)
143
- if (!event) continue
144
-
145
- try {
146
- const msg = JSON.parse(event.data)
147
- // Match our request ID
148
- if (msg.id === requestId) {
149
- if (msg.error) {
150
- throw new McpError(
151
- `mcp server "${serverName}" error: ${msg.error.message || JSON.stringify(msg.error)}`,
152
- { reason: "bad_response", server: serverName, code: msg.error.code, phase: "request" }
153
- )
154
- }
155
- result = msg.result ?? msg
156
- }
157
- // Server notifications — emit as events
158
- if (!msg.id && msg.method) {
159
- EventBus.emit({
160
- type: EVENT_TYPES.MCP_REQUEST,
161
- payload: { server: serverName, action: `notification:${msg.method}`, notification: true }
162
- }).catch(() => {})
163
- }
164
- } catch (e) {
165
- if (e instanceof McpError) throw e
166
- // Non-JSON SSE data skip
167
- }
168
- }
169
- }
170
- } finally {
171
- try { reader.releaseLock() } catch { /* reader may have pending read if stream was force-closed */ }
172
- }
173
-
174
- return result ?? {}
175
- }
176
-
177
- function parseSsePart(part) {
178
- const trimmed = part.trim()
179
- if (!trimmed) return null
180
- let event = null
181
- let data = ""
182
- for (const line of trimmed.split("\n")) {
183
- if (line.startsWith("event:")) event = line.slice(6).trim()
184
- else if (line.startsWith("data:")) data += line.slice(5).trim()
185
- }
186
- if (!data) return null
187
- return { event, data }
188
- }
189
-
190
- async function ensureInitialized() {
191
- if (initialized) return
192
- const result = await sendRequest("initialize", {
193
- protocolVersion: "2024-11-05",
194
- capabilities: {},
195
- clientInfo: { name: "kkcode", version: "0.1.2" }
196
- })
197
- // Send initialized notification
198
- try {
199
- await fetch(baseUrl, {
200
- method: "POST",
201
- headers: buildHeaders(),
202
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
203
- })
204
- } catch { /* best-effort */ }
205
- initialized = true
206
- return result
207
- }
208
-
209
- return {
210
- serverName,
211
- transport: "sse",
212
-
213
- async health() {
214
- try {
215
- await ensureInitialized()
216
- await sendRequest("ping")
217
- return { ok: true }
218
- } catch (error) {
219
- return { ok: false, error: error.message, reason: error.reason || "unknown" }
220
- }
221
- },
222
-
223
- async listTools() {
224
- await ensureInitialized()
225
- const out = await sendRequest("tools/list")
226
- return Array.isArray(out?.tools) ? out.tools : []
227
- },
228
-
229
- async listPrompts() {
230
- await ensureInitialized()
231
- try {
232
- const out = await sendRequest("prompts/list")
233
- return Array.isArray(out?.prompts) ? out.prompts : []
234
- } catch {
235
- return []
236
- }
237
- },
238
-
239
- async getPrompt(name, args = {}) {
240
- await ensureInitialized()
241
- return sendRequest("prompts/get", { name, arguments: args })
242
- },
243
-
244
- async listResources() {
245
- await ensureInitialized()
246
- try {
247
- const out = await sendRequest("resources/list")
248
- return Array.isArray(out?.resources) ? out.resources : []
249
- } catch {
250
- return []
251
- }
252
- },
253
-
254
- async listTemplates() {
255
- await ensureInitialized()
256
- try {
257
- const out = await sendRequest("resources/templates/list")
258
- return Array.isArray(out?.templates) ? out.templates : []
259
- } catch {
260
- return []
261
- }
262
- },
263
-
264
- async callTool(name, args = {}, signal = null) {
265
- await ensureInitialized()
266
- const result = await sendRequest("tools/call", { name, arguments: args })
267
- return normalizeToolResult(result, name)
268
- },
269
-
270
- shutdown() {
271
- if (notificationStream) {
272
- try { notificationStream.cancel() } catch { /* ignore */ }
273
- notificationStream = null
274
- }
275
- // Send session termination if we have a session
276
- if (sessionId) {
277
- fetch(baseUrl, {
278
- method: "DELETE",
279
- headers: buildHeaders()
280
- }).catch(() => {})
281
- }
282
- sessionId = null
283
- initialized = false
284
- }
285
- }
286
- }
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
+ import { MCP_PROTOCOL_VERSION, MCP_CLIENT_INFO } from "./constants.mjs"
6
+
7
+ /**
8
+ * MCP Streamable HTTP (SSE) client.
9
+ *
10
+ * Protocol: JSON-RPC 2.0 over HTTP POST with optional SSE response streaming.
11
+ * - POST to endpoint: send JSON-RPC request, receive JSON or SSE stream
12
+ * - GET to endpoint: open persistent SSE stream for server-initiated notifications
13
+ * - Session management via Mcp-Session-Id header
14
+ */
15
+ export function createSseMcpClient(serverName, config) {
16
+ const baseUrl = String(config.url || "").replace(/\/$/, "")
17
+ const timeoutMs = Number(config.timeout_ms || 30000)
18
+ const headers = config.headers || {}
19
+
20
+ let sessionId = null
21
+ let nextId = 1
22
+ let initialized = false
23
+ let notificationStream = null
24
+
25
+ function buildHeaders(extra = {}) {
26
+ const h = {
27
+ "content-type": "application/json",
28
+ accept: "application/json, text/event-stream",
29
+ ...headers,
30
+ ...extra
31
+ }
32
+ if (sessionId) h["mcp-session-id"] = sessionId
33
+ return h
34
+ }
35
+
36
+ async function sendRequest(method, params = {}, { signal: parentSignal = null } = {}) {
37
+ if (nextId > Number.MAX_SAFE_INTEGER - 1) nextId = 1
38
+ const id = nextId++
39
+ const body = { jsonrpc: "2.0", id, method, params }
40
+ const startedAt = Date.now()
41
+
42
+ const timeoutSignal = AbortSignal.timeout(timeoutMs)
43
+ const combinedSignal = parentSignal
44
+ ? AbortSignal.any([parentSignal, timeoutSignal])
45
+ : timeoutSignal
46
+
47
+ try {
48
+ const res = await fetch(baseUrl, {
49
+ method: "POST",
50
+ headers: buildHeaders(),
51
+ body: JSON.stringify(body),
52
+ signal: combinedSignal
53
+ })
54
+
55
+ const elapsed = Date.now() - startedAt
56
+
57
+ // Capture session ID from response
58
+ const newSessionId = res.headers.get("mcp-session-id")
59
+ if (newSessionId) sessionId = newSessionId
60
+
61
+ EventBus.emit({
62
+ type: EVENT_TYPES.MCP_REQUEST,
63
+ payload: { server: serverName, action: method, elapsed, status: res.status }
64
+ }).catch(() => {})
65
+
66
+ if (!res.ok) {
67
+ const text = await res.text().catch(() => "")
68
+ throw new McpError(
69
+ `mcp server "${serverName}" HTTP ${res.status}: ${text.slice(0, 500)}`,
70
+ {
71
+ reason: res.status >= 500 ? "server_crash" : "bad_response",
72
+ server: serverName,
73
+ action: method,
74
+ phase: "request",
75
+ statusCode: res.status
76
+ }
77
+ )
78
+ }
79
+
80
+ const contentType = res.headers.get("content-type") || ""
81
+
82
+ // SSE response parse events and return the final result
83
+ if (contentType.includes("text/event-stream")) {
84
+ return await parseSseResponse(res.body, id)
85
+ }
86
+
87
+ // Regular JSON response
88
+ const json = await res.json().catch((parseErr) => {
89
+ if (method === "initialize") {
90
+ throw new McpError(
91
+ `mcp server "${serverName}" malformed JSON in initialize response: ${parseErr.message}`,
92
+ { reason: "bad_response", server: serverName, action: method, phase: "request" }
93
+ )
94
+ }
95
+ EventBus.emit({
96
+ type: EVENT_TYPES.MCP_REQUEST,
97
+ payload: { server: serverName, action: method, warning: "malformed_json_response" }
98
+ }).catch(() => {})
99
+ return {}
100
+ })
101
+ if (json.error) {
102
+ throw new McpError(
103
+ `mcp server "${serverName}" error: ${json.error.message || JSON.stringify(json.error)}`,
104
+ { reason: "bad_response", server: serverName, action: method, code: json.error.code, phase: "request" }
105
+ )
106
+ }
107
+ return json.result ?? json
108
+ } catch (error) {
109
+ if (error instanceof McpError) throw error
110
+ const reason = (error.name === "AbortError" || error.name === "TimeoutError") ? "timeout" : "connection_refused"
111
+ throw new McpError(
112
+ `mcp server "${serverName}" ${reason}: ${error.message}`,
113
+ { reason, server: serverName, action: method, phase: "request" }
114
+ )
115
+ }
116
+ }
117
+
118
+ const maxSseBufferBytes = Number(config.max_sse_buffer_bytes || 4 * 1024 * 1024)
119
+
120
+ async function parseSseResponse(body, requestId) {
121
+ const reader = body.getReader()
122
+ const decoder = new TextDecoder()
123
+ let buffer = ""
124
+ let result = null
125
+
126
+ try {
127
+ while (true) {
128
+ const { done, value } = await reader.read()
129
+ if (done) break
130
+ buffer += decoder.decode(value, { stream: true })
131
+ if (Buffer.byteLength(buffer, "utf8") > maxSseBufferBytes) {
132
+ try { reader.releaseLock() } catch {}
133
+ throw new McpError(
134
+ `mcp server "${serverName}" SSE buffer exceeded ${maxSseBufferBytes} bytes`,
135
+ { reason: "bad_response", server: serverName, phase: "request" }
136
+ )
137
+ }
138
+
139
+ const parts = buffer.split("\n\n")
140
+ buffer = parts.pop()
141
+
142
+ for (const part of parts) {
143
+ const event = parseSsePart(part)
144
+ if (!event) continue
145
+
146
+ try {
147
+ const msg = JSON.parse(event.data)
148
+ // Match our request ID
149
+ if (msg.id === requestId) {
150
+ if (msg.error) {
151
+ throw new McpError(
152
+ `mcp server "${serverName}" error: ${msg.error.message || JSON.stringify(msg.error)}`,
153
+ { reason: "bad_response", server: serverName, code: msg.error.code, phase: "request" }
154
+ )
155
+ }
156
+ result = msg.result ?? msg
157
+ }
158
+ // Server notifications emit as events
159
+ if (!msg.id && msg.method) {
160
+ EventBus.emit({
161
+ type: EVENT_TYPES.MCP_REQUEST,
162
+ payload: { server: serverName, action: `notification:${msg.method}`, notification: true }
163
+ }).catch(() => {})
164
+ }
165
+ } catch (e) {
166
+ if (e instanceof McpError) throw e
167
+ // Non-JSON SSE data — skip
168
+ }
169
+ }
170
+ }
171
+ } finally {
172
+ try { reader.releaseLock() } catch { /* reader may have pending read if stream was force-closed */ }
173
+ }
174
+
175
+ return result ?? {}
176
+ }
177
+
178
+ function parseSsePart(part) {
179
+ const trimmed = part.trim()
180
+ if (!trimmed) return null
181
+ let event = null
182
+ let data = ""
183
+ for (const line of trimmed.split("\n")) {
184
+ if (line.startsWith("event:")) event = line.slice(6).trim()
185
+ else if (line.startsWith("data:")) data += line.slice(5).trim()
186
+ }
187
+ if (!data) return null
188
+ return { event, data }
189
+ }
190
+
191
+ async function ensureInitialized() {
192
+ if (initialized) return
193
+ const result = await sendRequest("initialize", {
194
+ protocolVersion: MCP_PROTOCOL_VERSION,
195
+ capabilities: {},
196
+ clientInfo: MCP_CLIENT_INFO
197
+ })
198
+ // Send initialized notification
199
+ try {
200
+ await fetch(baseUrl, {
201
+ method: "POST",
202
+ headers: buildHeaders(),
203
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
204
+ signal: AbortSignal.timeout(timeoutMs)
205
+ })
206
+ } catch { /* best-effort */ }
207
+ initialized = true
208
+ return result
209
+ }
210
+
211
+ return {
212
+ serverName,
213
+ transport: "sse",
214
+
215
+ async health() {
216
+ try {
217
+ await ensureInitialized()
218
+ await sendRequest("ping")
219
+ return { ok: true }
220
+ } catch (error) {
221
+ return { ok: false, error: error.message, reason: error.reason || "unknown" }
222
+ }
223
+ },
224
+
225
+ async listTools() {
226
+ await ensureInitialized()
227
+ const out = await sendRequest("tools/list")
228
+ return Array.isArray(out?.tools) ? out.tools : []
229
+ },
230
+
231
+ async listPrompts() {
232
+ await ensureInitialized()
233
+ try {
234
+ const out = await sendRequest("prompts/list")
235
+ return Array.isArray(out?.prompts) ? out.prompts : []
236
+ } catch {
237
+ return []
238
+ }
239
+ },
240
+
241
+ async getPrompt(name, args = {}) {
242
+ await ensureInitialized()
243
+ return sendRequest("prompts/get", { name, arguments: args })
244
+ },
245
+
246
+ async listResources() {
247
+ await ensureInitialized()
248
+ try {
249
+ const out = await sendRequest("resources/list")
250
+ return Array.isArray(out?.resources) ? out.resources : []
251
+ } catch {
252
+ return []
253
+ }
254
+ },
255
+
256
+ async listTemplates() {
257
+ await ensureInitialized()
258
+ try {
259
+ const out = await sendRequest("resources/templates/list")
260
+ return Array.isArray(out?.templates) ? out.templates : []
261
+ } catch {
262
+ return []
263
+ }
264
+ },
265
+
266
+ async callTool(name, args = {}, signal = null) {
267
+ await ensureInitialized()
268
+ const result = await sendRequest("tools/call", { name, arguments: args }, { signal })
269
+ return normalizeToolResult(result, serverName, name)
270
+ },
271
+
272
+ shutdown() {
273
+ if (notificationStream) {
274
+ try { notificationStream.cancel() } catch { /* ignore */ }
275
+ notificationStream = null
276
+ }
277
+ // Send session termination if we have a session
278
+ if (sessionId) {
279
+ fetch(baseUrl, {
280
+ method: "DELETE",
281
+ headers: buildHeaders()
282
+ }).catch(() => {})
283
+ }
284
+ sessionId = null
285
+ initialized = false
286
+ }
287
+ }
288
+ }