thincoder 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -13
- package/bin/thincoder.mjs +27 -346
- package/package.json +1 -1
- package/src/agent/dispatch.mjs +98 -0
- package/src/agent/helpers.mjs +185 -0
- package/src/agent/setup.mjs +117 -0
- package/src/agent-tools/goal.mjs +71 -0
- package/src/agent-tools/plan.mjs +31 -0
- package/src/agent-tools/recent-changes.mjs +23 -0
- package/src/agent-tools/skill.mjs +46 -0
- package/src/agent-tools/subagent.mjs +113 -0
- package/src/agent-tools/task.mjs +67 -0
- package/src/agent-tools/verify.mjs +198 -0
- package/src/agent-tools.mjs +12 -0
- package/src/agent.mjs +90 -1040
- package/src/cli/distill-command.mjs +85 -0
- package/src/cli/make-agent.mjs +85 -0
- package/src/cli/memory-command.mjs +63 -0
- package/src/cli/permission.mjs +41 -0
- package/src/cli/setup-wizard.mjs +70 -0
- package/src/config.mjs +5 -8
- package/src/context.mjs +10 -13
- package/src/distill.mjs +4 -3
- package/src/embedding.mjs +4 -2
- package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
- package/src/mcp/helpers.mjs +37 -0
- package/src/mcp/transport-http.mjs +176 -0
- package/src/mcp/transport-stdio.mjs +84 -0
- package/src/mcp/transport-ws.mjs +87 -0
- package/src/mcp.mjs +4 -428
- package/src/memory/code-index.mjs +211 -0
- package/src/memory/code-sync.mjs +306 -0
- package/src/memory/core.mjs +277 -0
- package/src/memory/docs.mjs +262 -0
- package/src/memory/schema.mjs +426 -0
- package/src/memory.mjs +12 -1403
- package/src/provider/core.mjs +239 -0
- package/src/provider/index.mjs +6 -0
- package/src/provider/rate.mjs +104 -0
- package/src/session.mjs +18 -5
- package/src/tools/bash.mjs +144 -0
- package/src/tools/file.mjs +205 -0
- package/src/tools/git.mjs +166 -0
- package/src/tools/glob.mjs +51 -0
- package/src/tools/grep.mjs +100 -0
- package/src/tools/index.mjs +22 -0
- package/src/tools/ls.mjs +36 -0
- package/src/tools/patch.mjs +226 -0
- package/src/tools/repomap-parse.mjs +168 -0
- package/src/tools/shared.mjs +257 -0
- package/src/tools/system.mjs +336 -0
- package/src/tools/web.mjs +121 -0
- package/src/tools.mjs +2 -1194
- package/src/tui/agent-turn.mjs +254 -0
- package/src/tui/ansi.mjs +32 -0
- package/src/tui/clipboard.mjs +48 -0
- package/src/tui/cmd-auto.mjs +21 -0
- package/src/tui/cmd-clear.mjs +26 -0
- package/src/tui/cmd-config.mjs +72 -0
- package/src/tui/cmd-exit.mjs +5 -0
- package/src/tui/cmd-extract.mjs +5 -0
- package/src/tui/cmd-goal.mjs +47 -0
- package/src/tui/cmd-help.mjs +25 -0
- package/src/tui/cmd-init.mjs +91 -0
- package/src/tui/cmd-mcp.mjs +146 -0
- package/src/tui/cmd-model.mjs +7 -0
- package/src/tui/cmd-new.mjs +18 -0
- package/src/tui/cmd-plan.mjs +21 -0
- package/src/tui/cmd-reindex.mjs +44 -0
- package/src/tui/cmd-restore.mjs +39 -0
- package/src/tui/cmd-session.mjs +42 -0
- package/src/tui/cmd-skills.mjs +17 -0
- package/src/tui/cmd-think.mjs +56 -0
- package/src/tui/config-helpers.mjs +34 -0
- package/src/tui/distill-cmd.mjs +45 -0
- package/src/tui/index.mjs +330 -0
- package/src/tui/interaction.mjs +79 -0
- package/src/tui/key-handler.mjs +267 -0
- package/src/tui/layout.mjs +115 -0
- package/src/tui/pickers.mjs +279 -0
- package/src/tui/render-frame.mjs +304 -0
- package/src/tui/render.mjs +205 -0
- package/src/tui/slash-commands.mjs +138 -0
- package/src/tui/startup.mjs +113 -0
- package/src/tui/wizard.mjs +168 -0
- package/src/tui-render.mjs +4 -0
- package/src/tui.mjs +3 -2566
- package/src/provider.mjs +0 -383
- /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
- /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
- /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
- /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
- /package/src/{main-overlay.md → prompts/main.md} +0 -0
- /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
- /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
- /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/transport-http.mjs — MCP HTTP + SSE transport (Streamable HTTP)
|
|
3
|
+
*/
|
|
4
|
+
import { rpcId, CALL_TIMEOUT_MS, ENDPOINT_WAIT_MS, withTimeout } from "./helpers.mjs"
|
|
5
|
+
|
|
6
|
+
export function httpTransport(baseURL, extraHeaders = {}) {
|
|
7
|
+
const url = baseURL.replace(/\/+$/, "")
|
|
8
|
+
let sessionId = null
|
|
9
|
+
let closed = false
|
|
10
|
+
let eventSource = null
|
|
11
|
+
let abortController = null
|
|
12
|
+
let postUrl = url
|
|
13
|
+
let legacySSE = false
|
|
14
|
+
|
|
15
|
+
const headers = () => {
|
|
16
|
+
const h = { "Content-Type": "application/json", Accept: "text/event-stream, application/json", ...extraHeaders }
|
|
17
|
+
if (sessionId) h["Mcp-Session-Id"] = sessionId
|
|
18
|
+
return h
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pending = new Map()
|
|
22
|
+
|
|
23
|
+
async function* parseSSE(response) {
|
|
24
|
+
const reader = response.body.getReader()
|
|
25
|
+
const decoder = new TextDecoder()
|
|
26
|
+
let buf = ""
|
|
27
|
+
let current = { data: "", event: "message" }
|
|
28
|
+
try {
|
|
29
|
+
while (true) {
|
|
30
|
+
const { done, value } = await reader.read()
|
|
31
|
+
if (done) break
|
|
32
|
+
buf += decoder.decode(value, { stream: true })
|
|
33
|
+
const lines = buf.split("\n")
|
|
34
|
+
buf = lines.pop() ?? ""
|
|
35
|
+
for (const raw of lines) {
|
|
36
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw
|
|
37
|
+
if (line === "") {
|
|
38
|
+
if (current.data) {
|
|
39
|
+
yield { event: current.event, data: current.data.trimEnd() }
|
|
40
|
+
current = { data: "", event: "message" }
|
|
41
|
+
}
|
|
42
|
+
} else if (line.startsWith("data:")) {
|
|
43
|
+
current.data += (current.data ? "\n" : "") + line.slice(5).replace(/^ /, "")
|
|
44
|
+
} else if (line.startsWith("event:")) {
|
|
45
|
+
current.event = line.slice(6).trim()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
reader.releaseLock()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function openSSE() {
|
|
55
|
+
if (closed) return
|
|
56
|
+
abortController?.abort()
|
|
57
|
+
abortController = new AbortController()
|
|
58
|
+
const resp = await fetch(url, {
|
|
59
|
+
method: "GET",
|
|
60
|
+
headers: { Accept: "text/event-stream", ...extraHeaders },
|
|
61
|
+
signal: abortController.signal,
|
|
62
|
+
})
|
|
63
|
+
if (!resp.ok) throw new Error(`SSE connect failed: HTTP ${resp.status}`)
|
|
64
|
+
eventSource = parseSSE(resp)
|
|
65
|
+
let endpointReady
|
|
66
|
+
const gotEndpoint = new Promise((resolve) => { endpointReady = resolve })
|
|
67
|
+
|
|
68
|
+
;(async () => {
|
|
69
|
+
try {
|
|
70
|
+
for await (const { event, data } of eventSource) {
|
|
71
|
+
if (closed) break
|
|
72
|
+
if (event === "endpoint") {
|
|
73
|
+
postUrl = new URL(data.trim(), url).href
|
|
74
|
+
legacySSE = true
|
|
75
|
+
endpointReady()
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const msg = JSON.parse(data)
|
|
80
|
+
const resolver = pending.get(msg.id)
|
|
81
|
+
if (resolver) {
|
|
82
|
+
pending.delete(msg.id)
|
|
83
|
+
resolver(msg)
|
|
84
|
+
}
|
|
85
|
+
} catch { /* 非 JSON,忽略 */ }
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!closed) {
|
|
89
|
+
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: `SSE error: ${error.message}` } })
|
|
90
|
+
pending.clear()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})()
|
|
94
|
+
|
|
95
|
+
const wait = new Promise((resolve) => {
|
|
96
|
+
const t = setTimeout(resolve, ENDPOINT_WAIT_MS)
|
|
97
|
+
t.unref?.()
|
|
98
|
+
})
|
|
99
|
+
await Promise.race([gotEndpoint, wait])
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function postRequest(method, params) {
|
|
103
|
+
const id = rpcId()
|
|
104
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
105
|
+
|
|
106
|
+
if (legacySSE) {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
pending.set(id, resolve)
|
|
109
|
+
fetch(postUrl, { method: "POST", headers: headers(), body, signal: AbortSignal.timeout(CALL_TIMEOUT_MS) })
|
|
110
|
+
.then((resp) => {
|
|
111
|
+
if (!resp.ok) {
|
|
112
|
+
pending.delete(id)
|
|
113
|
+
resolve({ id, error: { code: -32000, message: `POST failed: HTTP ${resp.status}` } })
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
.catch((e) => {
|
|
117
|
+
pending.delete(id)
|
|
118
|
+
resolve({ id, error: { code: -32000, message: `POST failed: ${e.message}` } })
|
|
119
|
+
})
|
|
120
|
+
}).finally(() => pending.delete(id))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const resp = await fetch(postUrl, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: headers(),
|
|
126
|
+
body,
|
|
127
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
128
|
+
})
|
|
129
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
130
|
+
|
|
131
|
+
const ct = resp.headers.get("content-type") ?? ""
|
|
132
|
+
const newSessionId = resp.headers.get("Mcp-Session-Id")
|
|
133
|
+
if (newSessionId) sessionId = newSessionId
|
|
134
|
+
|
|
135
|
+
if (ct.includes("text/event-stream")) {
|
|
136
|
+
const sse = parseSSE(resp)
|
|
137
|
+
for await (const { data } of sse) {
|
|
138
|
+
try {
|
|
139
|
+
const msg = JSON.parse(data)
|
|
140
|
+
if (msg.id === id) return msg
|
|
141
|
+
} catch { /* skip */ }
|
|
142
|
+
}
|
|
143
|
+
return { id, error: { code: -32000, message: "No JSON-RPC response in SSE stream" } }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return resp.json()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const send = async (method, params) => withTimeout(postRequest(method, params), CALL_TIMEOUT_MS)
|
|
150
|
+
|
|
151
|
+
const notify = (method, params) => {
|
|
152
|
+
fetch(postUrl, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: headers(),
|
|
155
|
+
body: JSON.stringify({ jsonrpc: "2.0", method, params }),
|
|
156
|
+
signal: AbortSignal.timeout(10_000),
|
|
157
|
+
}).catch(() => {})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const close = () => {
|
|
161
|
+
closed = true
|
|
162
|
+
abortController?.abort()
|
|
163
|
+
if (sessionId) {
|
|
164
|
+
fetch(postUrl, {
|
|
165
|
+
method: "DELETE",
|
|
166
|
+
headers: { "Mcp-Session-Id": sessionId, ...extraHeaders },
|
|
167
|
+
signal: AbortSignal.timeout(5_000),
|
|
168
|
+
}).catch(() => {})
|
|
169
|
+
sessionId = null
|
|
170
|
+
}
|
|
171
|
+
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: "Connection closed" } })
|
|
172
|
+
pending.clear()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { send, notify, close, openSSE, url, headers: extraHeaders }
|
|
176
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/transport-stdio.mjs — MCP stdio transport
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process"
|
|
5
|
+
import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
|
|
6
|
+
|
|
7
|
+
export function stdioTransport(command, args) {
|
|
8
|
+
const spawnOptions = { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { ...process.env } }
|
|
9
|
+
const child =
|
|
10
|
+
process.platform === "win32" && !/\.exe$/i.test(command)
|
|
11
|
+
? spawn("cmd.exe", ["/d", "/s", "/c", [command, ...(args ?? [])].map(quoteArg).join(" ")], {
|
|
12
|
+
...spawnOptions,
|
|
13
|
+
windowsVerbatimArguments: true,
|
|
14
|
+
})
|
|
15
|
+
: spawn(command, args ?? [], spawnOptions)
|
|
16
|
+
|
|
17
|
+
const pending = new Map()
|
|
18
|
+
const decoder = new TextDecoder()
|
|
19
|
+
let buffer = ""
|
|
20
|
+
let stderrTail = ""
|
|
21
|
+
let spawnError = null
|
|
22
|
+
let closed = false
|
|
23
|
+
|
|
24
|
+
const failAll = (message) => {
|
|
25
|
+
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message } })
|
|
26
|
+
pending.clear()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
child.stdout.on("data", (chunk) => {
|
|
30
|
+
buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true })
|
|
31
|
+
const lines = buffer.split("\n")
|
|
32
|
+
buffer = lines.pop() ?? ""
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (!line.trim()) continue
|
|
35
|
+
try {
|
|
36
|
+
const msg = JSON.parse(line)
|
|
37
|
+
const resolver = pending.get(msg.id)
|
|
38
|
+
if (resolver) {
|
|
39
|
+
pending.delete(msg.id)
|
|
40
|
+
resolver(msg)
|
|
41
|
+
}
|
|
42
|
+
} catch { /* 非 JSON 行忽略 */ }
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
child.stderr.on("data", (chunk) => {
|
|
47
|
+
stderrTail = (stderrTail + chunk.toString()).slice(-2000)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
child.stdin.on("error", () => {})
|
|
51
|
+
child.on("error", (error) => {
|
|
52
|
+
spawnError = error
|
|
53
|
+
closed = true
|
|
54
|
+
failAll(`spawn failed: ${error.message}`)
|
|
55
|
+
})
|
|
56
|
+
child.on("close", () => {
|
|
57
|
+
closed = true
|
|
58
|
+
const lastLine = stderrTail.trim().split("\n").pop()
|
|
59
|
+
failAll(`Connection closed${lastLine ? ` | stderr: ${lastLine}` : ""}`)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const send = (method, params) => {
|
|
63
|
+
if (spawnError) return Promise.resolve({ id: null, error: { code: -32000, message: `spawn failed: ${spawnError.message}` } })
|
|
64
|
+
if (closed) return Promise.reject(new Error("MCP connection closed"))
|
|
65
|
+
const id = rpcId()
|
|
66
|
+
const promise = new Promise((resolve) => pending.set(id, resolve))
|
|
67
|
+
try {
|
|
68
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n")
|
|
69
|
+
} catch (error) {
|
|
70
|
+
pending.delete(id)
|
|
71
|
+
return Promise.resolve({ id: null, error: { code: -32000, message: `stdin write failed: ${error.message}` } })
|
|
72
|
+
}
|
|
73
|
+
return withTimeout(promise, CALL_TIMEOUT_MS).finally(() => pending.delete(id))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const notify = (method, params) => {
|
|
77
|
+
if (closed) return
|
|
78
|
+
try {
|
|
79
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n")
|
|
80
|
+
} catch { /* 忽略 */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { send, notify, close: () => { if (!closed) child.kill() } }
|
|
84
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/transport-ws.mjs — MCP WebSocket transport
|
|
3
|
+
*/
|
|
4
|
+
import { rpcId, INIT_TIMEOUT_MS, CALL_TIMEOUT_MS, withTimeout, withAuthToken } from "./helpers.mjs"
|
|
5
|
+
|
|
6
|
+
export function wsTransport(wsUrl, extraHeaders = {}) {
|
|
7
|
+
const pending = new Map()
|
|
8
|
+
let closed = false
|
|
9
|
+
let ws = null
|
|
10
|
+
|
|
11
|
+
const failAll = (message) => {
|
|
12
|
+
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message } })
|
|
13
|
+
pending.clear()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const connect = () => {
|
|
17
|
+
if (closed) throw new Error("MCP WebSocket connection closed")
|
|
18
|
+
ws = new WebSocket(withAuthToken(wsUrl, extraHeaders.Authorization))
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const timeout = setTimeout(() => {
|
|
22
|
+
ws.close()
|
|
23
|
+
reject(new Error(`WebSocket connect timeout: ${wsUrl}`))
|
|
24
|
+
}, INIT_TIMEOUT_MS)
|
|
25
|
+
|
|
26
|
+
ws.addEventListener("open", () => {
|
|
27
|
+
clearTimeout(timeout)
|
|
28
|
+
resolve()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
ws.addEventListener("message", (event) => {
|
|
32
|
+
try {
|
|
33
|
+
const msg = JSON.parse(event.data.toString())
|
|
34
|
+
const resolver = pending.get(msg.id)
|
|
35
|
+
if (resolver) {
|
|
36
|
+
pending.delete(msg.id)
|
|
37
|
+
resolver(msg)
|
|
38
|
+
}
|
|
39
|
+
} catch { /* 非 JSON,忽略 */ }
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
ws.addEventListener("error", (event) => {
|
|
43
|
+
clearTimeout(timeout)
|
|
44
|
+
closed = true
|
|
45
|
+
const errMsg = event.message || "WebSocket error"
|
|
46
|
+
if (pending.size > 0) {
|
|
47
|
+
failAll(errMsg)
|
|
48
|
+
} else {
|
|
49
|
+
reject(new Error(errMsg))
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
ws.addEventListener("close", () => {
|
|
54
|
+
clearTimeout(timeout)
|
|
55
|
+
closed = true
|
|
56
|
+
failAll("WebSocket closed")
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const send = (method, params) => {
|
|
62
|
+
if (closed) return Promise.reject(new Error("MCP WebSocket connection closed"))
|
|
63
|
+
const id = rpcId()
|
|
64
|
+
const promise = new Promise((resolve) => pending.set(id, resolve))
|
|
65
|
+
try {
|
|
66
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }))
|
|
67
|
+
} catch (error) {
|
|
68
|
+
pending.delete(id)
|
|
69
|
+
return Promise.resolve({ id: null, error: { code: -32000, message: `ws send failed: ${error.message}` } })
|
|
70
|
+
}
|
|
71
|
+
return withTimeout(promise, CALL_TIMEOUT_MS).finally(() => pending.delete(id))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const notify = (method, params) => {
|
|
75
|
+
if (!closed && ws?.readyState === WebSocket.OPEN) {
|
|
76
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const close = () => {
|
|
81
|
+
closed = true
|
|
82
|
+
failAll("Connection closed")
|
|
83
|
+
try { ws?.close() } catch { /* 忽略 */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { send, notify, close, connect }
|
|
87
|
+
}
|