thincoder 0.12.61 → 0.12.63

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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
package/src/tools/lsp.mjs DELETED
@@ -1,316 +0,0 @@
1
- /**
2
- * tools/lsp.mjs — LSP (Language Server Protocol) code intelligence tool
3
- * Zero-dependency JSON-RPC 2.0 over stdio client.
4
- *
5
- * Provides: go-to-definition, find-references, hover info, document symbols, diagnostics.
6
- * Lazy-starts language servers on first call. Configurable via config.json lsp.servers.
7
- *
8
- * Config format:
9
- * "lsp": {
10
- * "servers": {
11
- * "typescript": { "command": "typescript-language-server", "args": ["--stdio"] },
12
- * "python": { "command": "pyright-langserver", "args": ["--stdio"] }
13
- * }
14
- * }
15
- */
16
-
17
- import { spawn } from "node:child_process"
18
- import { existsSync, readFileSync } from "node:fs"
19
- import { join, extname } from "node:path"
20
- import { DESC } from "./shared.mjs"
21
-
22
- // ---- JSON-RPC transport over stdio ----
23
-
24
- /** Send a JSON-RPC request to the server via stdin */
25
- function send(proc, message) {
26
- const body = JSON.stringify(message)
27
- const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`
28
- proc.stdin.write(header + body)
29
- }
30
-
31
- /** Read one JSON-RPC message from stdout. Returns parsed JSON, or null on EOF. */
32
- function readMessage(proc) {
33
- return new Promise((resolve) => {
34
- let header = ""
35
- let contentLength = -1
36
-
37
- const onData = (chunk) => {
38
- if (contentLength < 0) {
39
- header += chunk.toString()
40
- const match = header.match(/Content-Length: (\d+)\r\n\r\n/)
41
- if (match) {
42
- contentLength = parseInt(match[1])
43
- const bodyStart = header.indexOf("\r\n\r\n") + 4
44
- const remaining = header.slice(bodyStart)
45
- header = ""
46
- if (remaining.length >= contentLength) {
47
- proc.stdout.removeListener("data", onData)
48
- try { resolve(JSON.parse(remaining.slice(0, contentLength))) } catch { resolve(null) }
49
- return
50
- }
51
- // Need more data — leave remaining in a buffer-like state
52
- proc.stdout.removeListener("data", onData)
53
- readBody(proc, remaining, contentLength).then(resolve)
54
- }
55
- }
56
- }
57
-
58
- proc.stdout.on("data", onData)
59
- })
60
- }
61
-
62
- function readBody(proc, buf, targetLen) {
63
- return new Promise((resolve) => {
64
- const onData = (chunk) => {
65
- buf += chunk.toString()
66
- if (buf.length >= targetLen) {
67
- proc.stdout.removeListener("data", onData)
68
- try { resolve(JSON.parse(buf.slice(0, targetLen))) } catch { resolve(null) }
69
- }
70
- }
71
- proc.stdout.on("data", onData)
72
- })
73
- }
74
-
75
- /** Send a request and wait for the matching response */
76
- async function request(proc, method, params, id) {
77
- send(proc, { jsonrpc: "2.0", id, method, params })
78
- while (true) {
79
- const msg = await readMessage(proc)
80
- if (!msg) return null
81
- if (msg.id === id) return msg
82
- // Store notifications for later retrieval (diagnostics)
83
- if (msg.method === "textDocument/publishDiagnostics") {
84
- proc._diagnostics = proc._diagnostics || {}
85
- proc._diagnostics[msg.params.uri] = msg.params.diagnostics
86
- }
87
- }
88
- }
89
-
90
- /** Send a notification (no response expected) */
91
- function notify(proc, method, params) {
92
- send(proc, { jsonrpc: "2.0", method, params })
93
- }
94
-
95
- // ---- Language server process management ----
96
-
97
- const servers = new Map() // ext → { proc, rootUri, ready }
98
-
99
- /** Convert file path to file:// URI */
100
- function toUri(absPath) {
101
- return "file:///" + absPath.replace(/\\/g, "/").replace(/^([A-Z]):/, (_, d) => d.toLowerCase() + "%3A")
102
- }
103
-
104
- /** Resolve which language server to use for a file extension */
105
- function resolveServerConfig(config, ext) {
106
- const map = {
107
- ".js": "typescript", ".mjs": "typescript", ".cjs": "typescript",
108
- ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
109
- ".py": "python", ".pyi": "python",
110
- ".rs": "rust",
111
- ".go": "go",
112
- }
113
- const key = map[ext] || ext.slice(1)
114
- const servers = config?.lsp?.servers ?? {}
115
- return servers[key] || null
116
- }
117
-
118
- /** Start or reuse a language server for the given file */
119
- async function getServer(cwd, filePath, config) {
120
- const ext = extname(filePath).toLowerCase()
121
- const srvConfig = resolveServerConfig(config, ext)
122
- if (!srvConfig) return null
123
-
124
- const abs = join(cwd, ...filePath.split("/"))
125
- if (!existsSync(abs)) return null
126
-
127
- const rootUri = toUri(cwd)
128
- const key = `${ext}:${cwd}`
129
- let entry = servers.get(key)
130
-
131
- if (entry && entry.ready) return entry
132
-
133
- // Start new server
134
- const proc = spawn(srvConfig.command, srvConfig.args || [], {
135
- stdio: ["pipe", "pipe", "pipe"],
136
- windowsHide: true,
137
- })
138
-
139
- proc.stderr.on("data", () => {}) // suppress stderr noise
140
-
141
- entry = { proc, rootUri, ready: false }
142
- servers.set(key, entry)
143
-
144
- // Initialize handshake
145
- const initResult = await request(proc, "initialize", {
146
- processId: null,
147
- rootUri,
148
- capabilities: {
149
- textDocument: {
150
- definition: { linkSupport: false },
151
- references: {},
152
- hover: { contentFormat: ["plaintext"] },
153
- documentSymbol: { hierarchicalDocumentSymbolSupport: true },
154
- },
155
- },
156
- workspace: {},
157
- }, 1)
158
-
159
- if (!initResult) {
160
- proc.kill()
161
- servers.delete(key)
162
- return null
163
- }
164
-
165
- notify(proc, "initialized", {})
166
- entry.ready = true
167
- return entry
168
- }
169
-
170
- /** Notify the server that a file is open (required before queries) */
171
- async function ensureOpen(proc, uri, ext) {
172
- const langMap = { ".js": "javascript", ".mjs": "javascript", ".cjs": "javascript", ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript", ".py": "python", ".rs": "rust", ".go": "go" }
173
- const languageId = langMap[ext] || ext.slice(1)
174
- notify(proc, "textDocument/didOpen", {
175
- textDocument: {
176
- uri,
177
- languageId,
178
- version: 1,
179
- text: readFileSync(uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":").replace(/\//g, "\\"), "utf8"),
180
- },
181
- })
182
- }
183
-
184
- // ---- Tool definition ----
185
-
186
- export const lspTool = {
187
- name: "lsp",
188
- description: DESC("lsp"),
189
- parameters: {
190
- type: "object",
191
- properties: {
192
- subcommand: {
193
- type: "string",
194
- enum: ["definition", "references", "hover", "symbols", "diagnostics"],
195
- description: "LSP operation to perform",
196
- },
197
- uri: {
198
- type: "string",
199
- description: "Target file path (relative to project root)",
200
- },
201
- line: {
202
- type: "integer",
203
- description: "1-based line number (for definition/references/hover)",
204
- },
205
- character: {
206
- type: "integer",
207
- description: "1-based character offset (for definition/references/hover)",
208
- },
209
- },
210
- required: ["subcommand", "uri"],
211
- },
212
- readonly: true,
213
-
214
- async execute(args, ctx) {
215
- const config = ctx.agent?.config ?? {}
216
- const cwd = ctx.cwd
217
- const filePath = args.uri
218
-
219
- try {
220
- const entry = await getServer(cwd, filePath, config)
221
- if (!entry) {
222
- const ext = extname(filePath).toLowerCase()
223
- return `No LSP server configured for "${ext}" files. Add one to config.json:\n"lsp": { "servers": { "${ext.slice(1)}": { "command": "...", "args": ["--stdio"] } } }`
224
- }
225
-
226
- const { proc } = entry
227
- const abs = join(cwd, ...filePath.split("/"))
228
- const uri = toUri(abs)
229
- const ext = extname(filePath).toLowerCase()
230
- await ensureOpen(proc, uri, ext)
231
-
232
- // Wait briefly for diagnostics to arrive
233
- await new Promise((r) => setTimeout(r, 300))
234
-
235
- switch (args.subcommand) {
236
- case "definition": {
237
- if (!args.line || !args.character) return "Error: line and character required for definition"
238
- const res = await request(proc, "textDocument/definition", {
239
- textDocument: { uri },
240
- position: { line: args.line - 1, character: args.character - 1 },
241
- }, 10)
242
- if (!res?.result) return "(no definition found)"
243
- const locs = Array.isArray(res.result) ? res.result : [res.result]
244
- return locs.map((l) => {
245
- const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
246
- return `${path}:${l.range.start.line + 1}:${l.range.start.character + 1}`
247
- }).join("\n")
248
- }
249
-
250
- case "references": {
251
- if (!args.line || !args.character) return "Error: line and character required for references"
252
- const res = await request(proc, "textDocument/references", {
253
- textDocument: { uri },
254
- position: { line: args.line - 1, character: args.character - 1 },
255
- context: { includeDeclaration: false },
256
- }, 10)
257
- if (!res?.result?.length) return "(no references found)"
258
- return res.result.slice(0, 50).map((l) => {
259
- const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
260
- return `${path}:${l.range.start.line + 1}:${l.range.start.character + 1}`
261
- }).join("\n") + (res.result.length > 50 ? `\n... and ${res.result.length - 50} more` : "")
262
- }
263
-
264
- case "hover": {
265
- if (!args.line || !args.character) return "Error: line and character required for hover"
266
- const res = await request(proc, "textDocument/hover", {
267
- textDocument: { uri },
268
- position: { line: args.line - 1, character: args.character - 1 },
269
- }, 10)
270
- if (!res?.result?.contents) return "(no hover info)"
271
- const contents = res.result.contents
272
- if (typeof contents === "string") return contents
273
- if (Array.isArray(contents)) return contents.map((c) => typeof c === "string" ? c : c.value).join("\n")
274
- if (contents.value) return contents.value
275
- return JSON.stringify(contents)
276
- }
277
-
278
- case "symbols": {
279
- const res = await request(proc, "textDocument/documentSymbol", {
280
- textDocument: { uri },
281
- }, 10)
282
- if (!res?.result?.length) return "(no symbols found)"
283
- function render(nodes, depth) {
284
- const lines = []
285
- for (const n of nodes) {
286
- const kind = n.kind != null ? ` [${symbolKind(n.kind)}]` : ""
287
- lines.push(`${" ".repeat(depth)}${n.name}${kind} — L${n.range.start.line + 1}`)
288
- if (n.children?.length) lines.push(...render(n.children, depth + 1))
289
- }
290
- return lines
291
- }
292
- return render(res.result, 0).join("\n")
293
- }
294
-
295
- case "diagnostics": {
296
- const diags = proc._diagnostics?.[uri]
297
- if (!diags?.length) return "(no diagnostics)"
298
- return diags.slice(0, 30).map((d) => {
299
- const sev = { 1: "ERROR", 2: "WARN", 3: "INFO", 4: "HINT" }[d.severity] || "?"
300
- return `L${d.range.start.line + 1}: ${sev}: ${d.message}${d.code ? ` [${d.code}]` : ""}`
301
- }).join("\n") + (diags.length > 30 ? `\n... and ${diags.length - 30} more` : "")
302
- }
303
-
304
- default:
305
- return `Unknown subcommand: ${args.subcommand}`
306
- }
307
- } catch (err) {
308
- return `lsp error: ${err.message}`
309
- }
310
- },
311
- }
312
-
313
- function symbolKind(k) {
314
- const kinds = { 1: "file", 2: "module", 3: "namespace", 4: "package", 5: "class", 6: "method", 7: "property", 8: "field", 9: "constructor", 10: "enum", 11: "interface", 12: "function", 13: "variable", 14: "constant", 15: "string", 16: "number", 17: "boolean", 18: "array", 19: "object", 20: "key", 21: "null", 22: "enumMember", 23: "struct", 24: "event", 25: "operator", 26: "typeParameter" }
315
- return kinds[k] || `kind-${k}`
316
- }
package/src/tools/ops.mjs DELETED
@@ -1,299 +0,0 @@
1
- /**
2
- * ops.mjs — operational tools: file_ops (move/copy/rename), process (list),
3
- * get_current_time, wait_for (condition wait). Each exists so the model reaches
4
- * for a dedicated tool instead of shelling out to `bash` for the same operation
5
- * (parity with thinworker).
6
- */
7
- import { DESC, resolveInCwd, truncate } from "./shared.mjs"
8
- import { cp, rename, rm } from "node:fs/promises"
9
- import { existsSync } from "node:fs"
10
- import { execFileSync } from "node:child_process"
11
- import net from "node:net"
12
-
13
- // ─── file_ops ──────────────────────────────────────────────────
14
-
15
- export const fileOpsTool = {
16
- name: "file_ops",
17
- description: DESC("file_ops"),
18
- parameters: {
19
- type: "object",
20
- properties: {
21
- action: { type: "string", enum: ["move", "copy", "rename"], description: "move | copy | rename" },
22
- source: { type: "string", description: "Source path, relative to cwd or absolute" },
23
- dest: { type: "string", description: "Destination path" },
24
- },
25
- required: ["action", "source", "dest"],
26
- },
27
- readonly: false,
28
- async execute({ action, source, dest }, ctx) {
29
- if (typeof source !== "string" || !source) return "Error: source is required"
30
- if (typeof dest !== "string" || !dest) return "Error: dest is required"
31
- if (!["move", "copy", "rename"].includes(action)) return `Error: action must be move | copy | rename (got "${action}")`
32
- const src = resolveInCwd(ctx, source)
33
- const dst = resolveInCwd(ctx, dest)
34
- if (src === dst) return "Error: source and dest resolve to the same path"
35
-
36
- if (action === "copy") {
37
- await cp(src, dst, { recursive: true, force: true })
38
- return `Copied ${source} → ${dest}`
39
- }
40
- // move & rename share the rename syscall; cross-device move falls back to copy+rm.
41
- try {
42
- await rename(src, dst)
43
- } catch (e) {
44
- if (e?.code !== "EXDEV") throw e
45
- await cp(src, dst, { recursive: true, force: true })
46
- await rm(src, { recursive: true, force: true })
47
- }
48
- return `${action === "rename" ? "Renamed" : "Moved"} ${source} → ${dest}`
49
- },
50
- }
51
-
52
- // ─── process ───────────────────────────────────────────────────
53
-
54
- export const processTool = {
55
- name: "process",
56
- description: DESC("process"),
57
- parameters: {
58
- type: "object",
59
- properties: {
60
- name: { type: "string", description: "Optional name substring filter (case-insensitive)" },
61
- },
62
- },
63
- readonly: true,
64
- async execute({ name }, ctx) {
65
- const filter = typeof name === "string" && name.trim() ? name.trim().toLowerCase() : null
66
- let rows
67
- try {
68
- rows = process.platform === "win32" ? listWindows() : listPosix()
69
- } catch (e) {
70
- return `process listing failed: ${e?.message ?? String(e)}`
71
- }
72
- if (filter) rows = rows.filter((r) => r.name.toLowerCase().includes(filter))
73
- if (rows.length === 0) return filter ? `No running processes match "${name}"` : "(no processes)"
74
- return truncate(rows.map((r) => `${r.name}\tPID ${r.pid}${r.mem ? `\t${r.mem}` : ""}`).join("\n"))
75
- },
76
- }
77
-
78
- function listWindows() {
79
- // tasklist /FO CSV /NH → lines: "name.exe","1234","Console","1","12,345 K"
80
- const out = execFileSync("tasklist", ["/FO", "CSV", "/NH"], { encoding: "utf8", timeout: 10000 })
81
- const rows = []
82
- for (const line of out.split("\n")) {
83
- const parts = line.split('","')
84
- if (parts.length < 2) continue
85
- const name = parts[0].replace(/^"/, "").trim()
86
- const pid = parts[1].replace(/"/, "").trim()
87
- const mem = parts[4] ? parts[4].replace(/"/, "").trim() : ""
88
- if (!name || !pid) continue
89
- rows.push({ name, pid, mem })
90
- }
91
- return rows
92
- }
93
-
94
- function listPosix() {
95
- const out = execFileSync("ps", ["-eo", "pid=,comm="], { encoding: "utf8", timeout: 10000 })
96
- const rows = []
97
- for (const line of out.split("\n")) {
98
- const m = line.trim().match(/^(\d+)\s+(.+)$/)
99
- if (m) rows.push({ name: m[2], pid: m[1], mem: "" })
100
- }
101
- return rows
102
- }
103
-
104
- // ─── get_current_time ──────────────────────────────────────────
105
-
106
- export const getCurrentTimeTool = {
107
- name: "get_current_time",
108
- description: DESC("get_current_time"),
109
- parameters: { type: "object", properties: {} },
110
- readonly: true,
111
- async execute() {
112
- const now = new Date()
113
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "unknown"
114
- const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
115
- return `Date: ${now.toISOString()} (UTC)\nTimezone: ${tz}\nWeekday: ${days[now.getDay()]}\nLocal: ${now.toLocaleString()}`
116
- },
117
- }
118
-
119
- // ─── wait_for ───────────────────────────────────────────────────
120
-
121
- /** wait_for bounds (TOOLS.md §16): a BUILT-IN ceiling replaces the unbounded
122
- * sleep-then-wait — timeout_ms defaults to 30s (config.json agent.waitForTimeoutMs
123
- * overrides), capped at WAIT_FOR_MAX_TIMEOUT_MS (600s — the execute-tool timeout
124
- * convention, Math.min(t, 600_000)); interval_ms defaults to 1s with a 100ms
125
- * floor so polling never busy-spins. */
126
- export const WAIT_FOR_DEFAULT_TIMEOUT_MS = 30_000
127
- export const WAIT_FOR_MAX_TIMEOUT_MS = 600_000
128
- export const WAIT_FOR_DEFAULT_INTERVAL_MS = 1_000
129
- export const WAIT_FOR_MIN_INTERVAL_MS = 100
130
-
131
- // Condition-source seam (§16 评审 #4): production uses the real evaluator
132
- // (evaluateWaitForCondition); tests inject a deterministic fake source via
133
- // setWaitForConditionSource (default null — production path unchanged).
134
- let injectedConditionSource = null
135
- export function setWaitForConditionSource(source) {
136
- injectedConditionSource = typeof source === "function" ? source : null
137
- }
138
-
139
- /** Parse a wait_for condition into { kind, arg }. Unknown conditions are an
140
- * EXPLICIT error enumerating the supported forms (评审 #2 — never a silent
141
- * wait on a misspelled condition). */
142
- export function parseWaitForCondition(condition) {
143
- const c = String(condition ?? "").trim()
144
- let m
145
- if (c === "advisor settled") return { kind: "advisor", arg: null }
146
- if (c === "consult done") return { kind: "consult", arg: null }
147
- if ((m = c.match(/^subagent id:\s*(\d+) done$/))) return { kind: "subagent", arg: String(Number(m[1])) }
148
- if ((m = c.match(/^file exists:\s*(.+)$/))) return { kind: "file", arg: m[1].trim() }
149
- if ((m = c.match(/^port open:\s*(\d+)$/))) return { kind: "port", arg: Number(m[1]) }
150
- throw new Error(
151
- `wait_for: unsupported condition "${c}" — supported: "advisor settled", "subagent id:N done", "consult done", "file exists:path", "port open:N"`,
152
- )
153
- }
154
-
155
- /** Async-subagent pool of an agent — ctx.agent 可达性(评审 #1):dispatch 把
156
- * agent 实例放进工具 ctx(agent/dispatch.mjs toolCtx.agent),池挂在 agent 上
157
- * (深度 0 的 VS Code 侧 agent._asyncSubagents 与 history._asyncSubagents 同
158
- * 一 Map——见 agent.mjs;CLI 侧直接是 agent._asyncSubagents)。 */
159
- function asyncPool(agent) {
160
- if (!agent) return new Map()
161
- if (agent._asyncSubagents instanceof Map) return agent._asyncSubagents
162
- return agent.history?._asyncSubagents instanceof Map ? agent.history._asyncSubagents : new Map()
163
- }
164
-
165
- /** "subagent id:N done" — the async entry for N settled (status done / done flag)
166
- * or already left the pool (moved to pending results / digested / never spawned —
167
- * nothing is left to wait on). Absent ids are done by vacuity: the model waits on
168
- * ids its own spawn ack just returned, so an id no longer in the pool means done.
169
- * Dual key lookup (2026-09-06 advisor #1): VS Code pool keys are the numeric
170
- * spawn-time id, the CLI keys them as String(id) — accept both. */
171
- function asyncEntryDone(agent, id) {
172
- const pool = asyncPool(agent)
173
- const key = String(id)
174
- const entry = pool.get(key) ?? pool.get(Number(key)) ?? null
175
- if (!entry) return true
176
- return entry.done === true || entry.status === "done"
177
- }
178
-
179
- /** 评审池双载体(第 10 批 §18.3 #2):agent ∪ history——未初始化的载体不进集合。 */
180
- function advisorPools(agent) {
181
- return [agent?._asyncAdvisors, agent?.history?._asyncAdvisors].filter((m) => m instanceof Map)
182
- }
183
-
184
- /** Any entry matching pred that is still running/queued (not done) — in the given pool. */
185
- function hasRunningIn(pool, pred) {
186
- for (const e of pool?.values() ?? []) {
187
- if (pred(e) && !(e.done === true || e.status === "done")) return true
188
- }
189
- return false
190
- }
191
-
192
- /** "advisor settled" 真判据(第 10 批 §18.3 #2 修正):**评审池真实态**——双载体
193
- * (agent._asyncAdvisors ∪ history._asyncAdvisors)无 running/queued 条目(与 §11.2 的
194
- * 未决评审判定 advisorReviewPending 同源语义)。修前读**子代理池**的 role==="advisor"
195
- * 条目——评审条目只在 _asyncAdvisors → 恒无命中 → 恒真 0ms 秒过(用户实证)。 */
196
- function advisorSettled(agent) {
197
- return !advisorPools(agent).some((pool) => hasRunningIn(pool, (e) => e.status === "running" || e.status === "queued"))
198
- }
199
-
200
- /** "consult done" — every consult session has drained (pending 0) or was
201
- * explicitly stopped (its children were aborted). No sessions → true (vacuously). */
202
- function consultAllDone(agent) {
203
- const sessions = agent?._consultSessions
204
- if (!sessions || sessions.size === 0) return true
205
- for (const s of sessions.values()) {
206
- if (!s.stopped && (s.pending ?? 0) > 0) return false
207
- }
208
- return true
209
- }
210
-
211
- /** "port open:N" — 127.0.0.1 probe with a short connect timeout (never hangs a poll). */
212
- function portOpenProbe(port) {
213
- return new Promise((resolve) => {
214
- const socket = net.connect({ host: "127.0.0.1", port, timeout: 400 })
215
- let settled = false
216
- const finish = (v) => {
217
- if (settled) return
218
- settled = true
219
- socket.destroy()
220
- resolve(v)
221
- }
222
- socket.once("connect", () => finish(true))
223
- socket.once("error", () => finish(false))
224
- socket.once("timeout", () => finish(false))
225
- })
226
- }
227
-
228
- /** Real condition evaluator (production path) — returns a boolean; throws on an
229
- * unsupported condition string. Polling is async (await setTimeout) — the event
230
- * loop keeps running so background async work (subagents/consult) can settle
231
- * between polls (评审 #1 — no blocking spin). */
232
- export async function evaluateWaitForCondition(condition, ctx) {
233
- const parsed = parseWaitForCondition(condition)
234
- const agent = ctx?.agent
235
- switch (parsed.kind) {
236
- case "advisor":
237
- // 第 10 批修正(§18.3 #2——修前恒真 0ms 秒过):判据 = 评审池真实态(双载体)——
238
- // 不再读子代理池的 role==="advisor" 条目(该池永无此类条目);语义与 §11.2 未决
239
- // 评审判定同源(advisorReviewPending)。
240
- return advisorSettled(agent)
241
- case "subagent":
242
- return asyncEntryDone(agent, parsed.arg)
243
- case "consult":
244
- return consultAllDone(agent)
245
- case "file":
246
- return existsSync(resolveInCwd(ctx, parsed.arg))
247
- case "port":
248
- return await portOpenProbe(parsed.arg)
249
- default:
250
- return false
251
- }
252
- }
253
-
254
- async function evalConditionSource(condition, ctx) {
255
- if (injectedConditionSource) return injectedConditionSource(condition, ctx)
256
- return evaluateWaitForCondition(condition, ctx)
257
- }
258
-
259
- export const waitForTool = {
260
- name: "wait_for",
261
- description: DESC("wait_for"),
262
- parameters: {
263
- type: "object",
264
- properties: {
265
- condition: { type: "string", description: 'Condition expression — "advisor settled", "subagent id:N done", "consult done", "file exists:path", "port open:N"' },
266
- interval_ms: { type: "integer", description: "Poll interval in ms (default 1000, floor 100)" },
267
- timeout_ms: { type: "integer", description: "Overall timeout in ms (default 30000, cap 600000; config.json agent.waitForTimeoutMs overrides the default)" },
268
- },
269
- required: ["condition"],
270
- },
271
- readonly: true,
272
- async execute(args, ctx = {}) {
273
- const condition = typeof args?.condition === "string" ? args.condition.trim() : ""
274
- if (!condition) return "Error: condition is required"
275
- const intervalMs = Math.max(WAIT_FOR_MIN_INTERVAL_MS, Math.floor(args?.interval_ms ?? WAIT_FOR_DEFAULT_INTERVAL_MS))
276
- const cfgTimeout = ctx?.agent?.config?.agent?.waitForTimeoutMs
277
- const base = Number.isFinite(args?.timeout_ms)
278
- ? args.timeout_ms
279
- : Number.isFinite(cfgTimeout) && cfgTimeout > 0 ? cfgTimeout : WAIT_FOR_DEFAULT_TIMEOUT_MS
280
- const timeoutMs = Math.min(Math.max(1, Math.floor(base)), WAIT_FOR_MAX_TIMEOUT_MS)
281
- const started = Date.now()
282
- let polls = 0
283
- for (;;) {
284
- // Cancel/interrupt-safe exit(T-W7):Ctrl+C / 定向 abort 传播——不吞信号、
285
- // 不把用户停变成工具错误(dispatch 对 aborted signal 原样再抛)。
286
- if (ctx.signal?.aborted) throw new Error("wait_for: interrupted by abort signal")
287
- polls++
288
- const ok = await evalConditionSource(condition, ctx)
289
- if (ok === true) {
290
- return `wait_for: condition satisfied after ${Date.now() - started}ms (${polls} check${polls === 1 ? "" : "s"}): "${condition}"`
291
- }
292
- const remaining = started + timeoutMs - Date.now()
293
- if (remaining <= 0) {
294
- return `wait_for: timed out after ${timeoutMs}ms waiting for "${condition}" (${polls} checks — condition never became true)`
295
- }
296
- await new Promise((r) => setTimeout(r, Math.min(intervalMs, remaining)))
297
- }
298
- },
299
- }