thincoder 0.12.62 → 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 (228) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +53 -27
  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 -379
  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 +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  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-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +25 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-reindex.mjs +1 -1
  31. package/src/tui/cmd-restore.mjs +2 -2
  32. package/src/tui/cmd-session.mjs +24 -1
  33. package/src/tui/cmd-skills.mjs +1 -1
  34. package/src/tui/cmd-think.mjs +22 -9
  35. package/src/tui/config-helpers.mjs +1 -1
  36. package/src/tui/display-budget.mjs +33 -11
  37. package/src/tui/index.mjs +9 -9
  38. package/src/tui/interaction.mjs +16 -7
  39. package/src/tui/key-modes.mjs +9 -4
  40. package/src/tui/ledger-surface.mjs +26 -10
  41. package/src/tui/model-catalog.mjs +4 -4
  42. package/src/tui/model-picker.mjs +8 -7
  43. package/src/tui/mouse.mjs +11 -6
  44. package/src/tui/pickers.mjs +15 -2
  45. package/src/tui/render-conversation.mjs +1 -1
  46. package/src/tui/render-frame.mjs +10 -5
  47. package/src/tui/render-loop.mjs +1 -1
  48. package/src/tui/render-segments.mjs +3 -1
  49. package/src/tui/slash-commands.mjs +1 -1
  50. package/src/tui/startup.mjs +14 -14
  51. package/src/tui/subagent-blocks.mjs +20 -3
  52. package/src/tui/subagent-freeze.mjs +73 -2
  53. package/src/tui/suspension-drive.mjs +46 -22
  54. package/src/tui/tool-events.mjs +11 -8
  55. package/src/tui/wizard.mjs +3 -3
  56. package/src/abort-provenance.mjs +0 -116
  57. package/src/advisor/citations.mjs +0 -139
  58. package/src/advisor/compaction.mjs +0 -174
  59. package/src/advisor/convergence.mjs +0 -80
  60. package/src/advisor/history.mjs +0 -77
  61. package/src/advisor/loop.mjs +0 -293
  62. package/src/advisor/messages.mjs +0 -299
  63. package/src/advisor/project-context.mjs +0 -194
  64. package/src/advisor/repos.mjs +0 -150
  65. package/src/advisor/run.mjs +0 -293
  66. package/src/advisor/truncate.mjs +0 -57
  67. package/src/advisor.mjs +0 -290
  68. package/src/agent/completion.mjs +0 -146
  69. package/src/agent/dispatch.mjs +0 -489
  70. package/src/agent/helpers.mjs +0 -384
  71. package/src/agent/post-turn.mjs +0 -70
  72. package/src/agent/record-results.mjs +0 -174
  73. package/src/agent/relay-prefix.mjs +0 -39
  74. package/src/agent/run-stages.mjs +0 -244
  75. package/src/agent/setup-reminders.mjs +0 -69
  76. package/src/agent/setup.mjs +0 -354
  77. package/src/agent/spawn-child.mjs +0 -243
  78. package/src/agent-tools/advisor-async.mjs +0 -346
  79. package/src/agent-tools/advisor-settle.mjs +0 -231
  80. package/src/agent-tools/advisor.mjs +0 -260
  81. package/src/agent-tools/async-settle.mjs +0 -204
  82. package/src/agent-tools/batch-segment.mjs +0 -195
  83. package/src/agent-tools/consult.mjs +0 -473
  84. package/src/agent-tools/design-token.mjs +0 -117
  85. package/src/agent-tools/digest-budget.mjs +0 -76
  86. package/src/agent-tools/eng.mjs +0 -67
  87. package/src/agent-tools/escalate-async.mjs +0 -295
  88. package/src/agent-tools/goal.mjs +0 -119
  89. package/src/agent-tools/plan.mjs +0 -81
  90. package/src/agent-tools/read-history.mjs +0 -309
  91. package/src/agent-tools/recent-changes.mjs +0 -24
  92. package/src/agent-tools/review-streak.mjs +0 -93
  93. package/src/agent-tools/settings.mjs +0 -265
  94. package/src/agent-tools/skill.mjs +0 -47
  95. package/src/agent-tools/subagent-actions.mjs +0 -482
  96. package/src/agent-tools/subagent-async.mjs +0 -434
  97. package/src/agent-tools/subagent-panel.mjs +0 -160
  98. package/src/agent-tools/subagent-run.mjs +0 -205
  99. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  100. package/src/agent-tools/subagent-spawn.mjs +0 -459
  101. package/src/agent-tools/subagent.mjs +0 -404
  102. package/src/agent-tools/task.mjs +0 -87
  103. package/src/agent-tools/timer.mjs +0 -46
  104. package/src/agent-tools/verify.mjs +0 -271
  105. package/src/agent-tools.mjs +0 -17
  106. package/src/agent.mjs +0 -417
  107. package/src/auto-think.mjs +0 -115
  108. package/src/config-migrate.mjs +0 -70
  109. package/src/config.mjs +0 -496
  110. package/src/context.mjs +0 -392
  111. package/src/conventions.mjs +0 -223
  112. package/src/embedding.mjs +0 -120
  113. package/src/escape.mjs +0 -152
  114. package/src/expand-home.mjs +0 -16
  115. package/src/explore-distill.mjs +0 -155
  116. package/src/generate-title.mjs +0 -88
  117. package/src/git/checkpoint.mjs +0 -448
  118. package/src/git/gitmem.mjs +0 -100
  119. package/src/hooks.mjs +0 -97
  120. package/src/ledger.mjs +0 -227
  121. package/src/log.mjs +0 -195
  122. package/src/markdown.mjs +0 -106
  123. package/src/mcp/helpers.mjs +0 -51
  124. package/src/mcp/transport-http.mjs +0 -248
  125. package/src/mcp/transport-stdio.mjs +0 -140
  126. package/src/mcp/transport-ws.mjs +0 -122
  127. package/src/mcp.mjs +0 -295
  128. package/src/memory/code-index.mjs +0 -219
  129. package/src/memory/code-sync.mjs +0 -415
  130. package/src/memory/core.mjs +0 -299
  131. package/src/memory/delete.mjs +0 -236
  132. package/src/memory/docs.mjs +0 -419
  133. package/src/memory/file-walk.mjs +0 -109
  134. package/src/memory/scan.mjs +0 -95
  135. package/src/memory/schema.mjs +0 -452
  136. package/src/memory.mjs +0 -21
  137. package/src/model-ref.mjs +0 -66
  138. package/src/model-specs.mjs +0 -179
  139. package/src/peer-domains.mjs +0 -265
  140. package/src/peer-instances.mjs +0 -231
  141. package/src/prompt-overlays.mjs +0 -82
  142. package/src/prompts/advisor-design.md +0 -41
  143. package/src/prompts/advisor-round1.md +0 -41
  144. package/src/prompts/advisor-round2.md +0 -46
  145. package/src/prompts/advisor-round3.md +0 -42
  146. package/src/prompts/common.md +0 -115
  147. package/src/prompts/consult-base.md +0 -19
  148. package/src/prompts/discipline-engineering.md +0 -258
  149. package/src/prompts/discipline-normal.md +0 -185
  150. package/src/prompts/persona-coder.md +0 -21
  151. package/src/prompts/persona-eng-coder.md +0 -37
  152. package/src/prompts/persona-eng-designer.md +0 -60
  153. package/src/prompts/persona-engineering.md +0 -55
  154. package/src/prompts/persona-explore.md +0 -15
  155. package/src/prompts/persona-normal.md +0 -27
  156. package/src/prompts/persona-plan.md +0 -26
  157. package/src/provider/anthropic.mjs +0 -225
  158. package/src/provider/core.mjs +0 -476
  159. package/src/provider/errors.mjs +0 -101
  160. package/src/provider/google.mjs +0 -257
  161. package/src/provider/index.mjs +0 -7
  162. package/src/provider/list-models.mjs +0 -93
  163. package/src/provider/normalize.mjs +0 -81
  164. package/src/provider/rate.mjs +0 -108
  165. package/src/provider/responses.mjs +0 -495
  166. package/src/provider/retry.mjs +0 -88
  167. package/src/provider/sse.mjs +0 -264
  168. package/src/proxy.mjs +0 -261
  169. package/src/rules.mjs +0 -53
  170. package/src/session-gc.mjs +0 -221
  171. package/src/session-guard.mjs +0 -59
  172. package/src/session-migrate.mjs +0 -48
  173. package/src/session-rename.mjs +0 -38
  174. package/src/session-segments.mjs +0 -100
  175. package/src/session-slots.mjs +0 -492
  176. package/src/session-store.mjs +0 -441
  177. package/src/session.mjs +0 -492
  178. package/src/skills.mjs +0 -153
  179. package/src/text-budget.mjs +0 -46
  180. package/src/token-ttl.mjs +0 -274
  181. package/src/tools/apply_patch.md +0 -15
  182. package/src/tools/bash.md +0 -37
  183. package/src/tools/bash.mjs +0 -268
  184. package/src/tools/checklist-sync.mjs +0 -181
  185. package/src/tools/checklist.md +0 -13
  186. package/src/tools/checklist.mjs +0 -299
  187. package/src/tools/delete.md +0 -13
  188. package/src/tools/edit-batch.mjs +0 -191
  189. package/src/tools/edit-diff.mjs +0 -348
  190. package/src/tools/edit.md +0 -30
  191. package/src/tools/execute.md +0 -21
  192. package/src/tools/execute.mjs +0 -228
  193. package/src/tools/fetch.md +0 -12
  194. package/src/tools/file.mjs +0 -469
  195. package/src/tools/file_ops.md +0 -17
  196. package/src/tools/get_current_time.md +0 -8
  197. package/src/tools/git-checkpoint.mjs +0 -143
  198. package/src/tools/git-ext.mjs +0 -173
  199. package/src/tools/git.md +0 -54
  200. package/src/tools/git.mjs +0 -356
  201. package/src/tools/glob-dialect.mjs +0 -130
  202. package/src/tools/glob.md +0 -11
  203. package/src/tools/grep.md +0 -19
  204. package/src/tools/hashline_edit.md +0 -14
  205. package/src/tools/index.mjs +0 -36
  206. package/src/tools/insert_after.md +0 -15
  207. package/src/tools/lint.md +0 -10
  208. package/src/tools/linter.mjs +0 -128
  209. package/src/tools/ls.md +0 -12
  210. package/src/tools/lsp.md +0 -10
  211. package/src/tools/lsp.mjs +0 -316
  212. package/src/tools/ops.mjs +0 -299
  213. package/src/tools/patch.mjs +0 -282
  214. package/src/tools/process.md +0 -10
  215. package/src/tools/question.md +0 -16
  216. package/src/tools/question.mjs +0 -26
  217. package/src/tools/read.md +0 -20
  218. package/src/tools/read_image.md +0 -8
  219. package/src/tools/repomap.mjs +0 -314
  220. package/src/tools/search.mjs +0 -236
  221. package/src/tools/shared.mjs +0 -446
  222. package/src/tools/tree.md +0 -14
  223. package/src/tools/tree.mjs +0 -66
  224. package/src/tools/wait_for.md +0 -22
  225. package/src/tools/web.mjs +0 -224
  226. package/src/tools/websearch.md +0 -16
  227. package/src/tools/write.md +0 -11
  228. package/src/traces/trace-store.mjs +0 -355
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
- }