thincoder 0.7.8 → 0.8.1

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 (97) hide show
  1. package/README.md +32 -13
  2. package/bin/thincoder.js +4 -0
  3. package/bin/thincoder.mjs +27 -346
  4. package/package.json +2 -2
  5. package/src/agent/dispatch.mjs +98 -0
  6. package/src/agent/helpers.mjs +185 -0
  7. package/src/agent/setup.mjs +117 -0
  8. package/src/agent-tools/goal.mjs +71 -0
  9. package/src/agent-tools/plan.mjs +31 -0
  10. package/src/agent-tools/recent-changes.mjs +23 -0
  11. package/src/agent-tools/skill.mjs +46 -0
  12. package/src/agent-tools/subagent.mjs +113 -0
  13. package/src/agent-tools/task.mjs +67 -0
  14. package/src/agent-tools/verify.mjs +198 -0
  15. package/src/agent-tools.mjs +12 -0
  16. package/src/agent.mjs +90 -1040
  17. package/src/cli/distill-command.mjs +85 -0
  18. package/src/cli/make-agent.mjs +85 -0
  19. package/src/cli/memory-command.mjs +63 -0
  20. package/src/cli/permission.mjs +41 -0
  21. package/src/cli/setup-wizard.mjs +70 -0
  22. package/src/config.mjs +5 -8
  23. package/src/context.mjs +10 -13
  24. package/src/distill.mjs +4 -3
  25. package/src/embedding.mjs +4 -2
  26. package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
  27. package/src/mcp/helpers.mjs +37 -0
  28. package/src/mcp/transport-http.mjs +176 -0
  29. package/src/mcp/transport-stdio.mjs +84 -0
  30. package/src/mcp/transport-ws.mjs +87 -0
  31. package/src/mcp.mjs +4 -428
  32. package/src/memory/code-index.mjs +211 -0
  33. package/src/memory/code-sync.mjs +306 -0
  34. package/src/memory/core.mjs +277 -0
  35. package/src/memory/docs.mjs +262 -0
  36. package/src/memory/schema.mjs +426 -0
  37. package/src/memory.mjs +12 -1403
  38. package/src/provider/core.mjs +239 -0
  39. package/src/provider/index.mjs +6 -0
  40. package/src/provider/rate.mjs +104 -0
  41. package/src/session.mjs +18 -5
  42. package/src/tools/bash.mjs +144 -0
  43. package/src/tools/file.mjs +205 -0
  44. package/src/tools/git.mjs +166 -0
  45. package/src/tools/glob.mjs +51 -0
  46. package/src/tools/grep.mjs +100 -0
  47. package/src/tools/index.mjs +22 -0
  48. package/src/tools/ls.mjs +36 -0
  49. package/src/tools/patch.mjs +226 -0
  50. package/src/tools/repomap-parse.mjs +168 -0
  51. package/src/tools/shared.mjs +257 -0
  52. package/src/tools/system.mjs +336 -0
  53. package/src/tools/web.mjs +121 -0
  54. package/src/tools.mjs +2 -1194
  55. package/src/tui/agent-turn.mjs +254 -0
  56. package/src/tui/ansi.mjs +32 -0
  57. package/src/tui/clipboard.mjs +48 -0
  58. package/src/tui/cmd-auto.mjs +21 -0
  59. package/src/tui/cmd-clear.mjs +26 -0
  60. package/src/tui/cmd-config.mjs +72 -0
  61. package/src/tui/cmd-exit.mjs +5 -0
  62. package/src/tui/cmd-extract.mjs +5 -0
  63. package/src/tui/cmd-goal.mjs +47 -0
  64. package/src/tui/cmd-help.mjs +25 -0
  65. package/src/tui/cmd-init.mjs +91 -0
  66. package/src/tui/cmd-mcp.mjs +146 -0
  67. package/src/tui/cmd-model.mjs +7 -0
  68. package/src/tui/cmd-new.mjs +18 -0
  69. package/src/tui/cmd-plan.mjs +21 -0
  70. package/src/tui/cmd-reindex.mjs +44 -0
  71. package/src/tui/cmd-restore.mjs +39 -0
  72. package/src/tui/cmd-session.mjs +42 -0
  73. package/src/tui/cmd-skills.mjs +17 -0
  74. package/src/tui/cmd-think.mjs +56 -0
  75. package/src/tui/config-helpers.mjs +34 -0
  76. package/src/tui/distill-cmd.mjs +45 -0
  77. package/src/tui/index.mjs +330 -0
  78. package/src/tui/interaction.mjs +79 -0
  79. package/src/tui/key-handler.mjs +267 -0
  80. package/src/tui/layout.mjs +115 -0
  81. package/src/tui/pickers.mjs +279 -0
  82. package/src/tui/render-frame.mjs +304 -0
  83. package/src/tui/render.mjs +205 -0
  84. package/src/tui/slash-commands.mjs +138 -0
  85. package/src/tui/startup.mjs +113 -0
  86. package/src/tui/wizard.mjs +168 -0
  87. package/src/tui-render.mjs +4 -0
  88. package/src/tui.mjs +3 -2566
  89. package/src/provider.mjs +0 -383
  90. /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
  91. /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
  92. /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
  93. /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
  94. /package/src/{main-overlay.md → prompts/main.md} +0 -0
  95. /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
  96. /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
  97. /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
package/README.md CHANGED
@@ -39,7 +39,7 @@ Three layers, all "query if present, skip if absent", unified hybrid retrieval:
39
39
  - **Hybrid retrieval**: FTS5 (BM25, per-character CJK indexing, bigrams matchable) + embedding vectors (brute-force cosine) + RRF(k=60) fusion ranking
40
40
  - **Embeddings**: OpenAI-compatible `/v1/embeddings`, defaults to SiliconFlow `BAAI/bge-m3` (free tier, good CJK support); Ollama works as an offline option. Vectors generated lazily — not computed on write, backfilled and persisted on first search
41
41
  - **Entry format**: Markdown + frontmatter (type/title/tags/author/created), readable and reviewable directly on GitHub; one file per entry, naturally avoiding merge conflicts; real conflicts produce honest errors, never auto-merged
42
- - **Dual-track accumulation**: conventions written manually (`memory_put`), experience extracted from sessions via `/distill` — **the LLM proposes candidates, a human confirms each y/n** before anything is stored; never fully automatic
42
+ - **Dual-track accumulation**: conventions written manually (`memory_put`), experience extracted from sessions via `/extract` — **the LLM proposes candidates, a human confirms each y/n** before anything is stored; never fully automatic
43
43
  - **Retrieval isolation**: the Project layer is isolated by project path — project A's memories never leak into project B
44
44
 
45
45
  ## Requirements
@@ -84,7 +84,7 @@ thincoder upgrade
84
84
 
85
85
  Running from source: replace `thincoder` above with `node bin/thincoder.mjs`.
86
86
 
87
- Slash commands in the TUI: `/help`, `/model` (arrow-key picker across all models of all providers; `/model <name>` switches directly), `/provider` (add/remove providers, set keys, custom endpoints), `/think` (thinking mode toggle and reasoning effort), `/config` (view config, `/config embedkey` for the embedding key, `/config set` for parameters), `/session` (list/switch archived sessions), `/reindex` (rebuild the index), `/distill` (extract knowledge from the current session), `/clear`, `/exit`. Typing `/` shows live matching hints in the status bar.
87
+ Slash commands in the TUI: `/help`, `/model` (arrow-key picker across all models of all providers; `/model <name>` switches directly), `/provider` (add/remove providers, set keys, custom endpoints), `/think` (thinking mode toggle and reasoning effort), `/config` (view config, `/config embedkey` for the embedding key, `/config set` for parameters), `/session` (list/switch archived sessions), `/reindex` (rebuild the index), `/extract` (extract knowledge from the current session), `/restore` (restore checkpoint), `/clear`, `/exit`. High-frequency commands support abbreviations: `/h` `/x` `/m` `/p` `/t` `/c` `/n`. Typing `/` shows live matching hints in the status bar.
88
88
 
89
89
  Environment variables: `THINCODER_API_KEY` (or `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`), `THINCODER_BASE_URL`, `THINCODER_MODEL`, `SILICONFLOW_API_KEY`.
90
90
 
@@ -146,24 +146,35 @@ Environment variables: `THINCODER_API_KEY` (or `DEEPSEEK_API_KEY` / `OPENAI_API_
146
146
  ```
147
147
  bin/thincoder.mjs command entry (tui / chat / memory / sync / distill)
148
148
  src/
149
- provider.mjs LLM calls (fetch, SSE streaming, retries)
149
+ provider/ LLM calls — core.mjs (fetch, SSE streaming, reasoning_content, usage, retries),
150
+ rate.mjs (TPM/RPM proactive rate gate), index.mjs (entry)
150
151
  embedding.mjs vector embeddings (OpenAI-compatible /v1/embeddings)
151
- tools.mjs 16 builtin tools + MCP wrapping + readonly scheduling flags
152
- mcp.mjs MCP client (JSON-RPC + stdio transport, zero-dependency)
153
- agent.mjs main loop + two-phase tool execution + plan/task/goal/skill/subagent/verify tools
152
+ tools/ 16 builtin tools + MCP wrapping + readonly scheduling flags
153
+ index.mjs (registry), file/git/patch/system/web.mjs (groups), shared.mjs (schema utils),
154
+ repomap.mjs (repo dependency outline: import/export regex parsing, on-demand via tool)
155
+ tools.mjs re-export shim → src/tools/index.mjs
156
+ mcp/ MCP client — helpers.mjs, transport-stdio/http/ws.mjs (JSON-RPC, zero-dependency)
157
+ mcp.mjs MCP client entry (connectMcpServer), delegates to src/mcp/
158
+ agent.mjs main loop + two-phase tool execution + reminder injection + completion guard + fix-verify loop
154
159
  + incremental indexing (auto reindexFile after write/edit/delete)
155
- repomap.mjs repo dependency outline (import/export regex parsing, on-demand via tool)
160
+ agent/ agent loop helpers — dispatch.mjs (two-phase execution), setup.mjs (system prompt assembly), helpers.mjs
161
+ agent-tools/ self-discipline tools (task/plan/goal/verify/subagent/skill/recent_changes)
156
162
  context.mjs rough token estimation + history compaction + task re-injection
157
- memory.mjs memory core: three-layer merged retrieval + code/doc indexing (code_chunks/doc_chunks)
158
- + FTS5 + vector RRF + JSDoc extraction + single-file incremental indexing
163
+ memory/ three-layer memory schema.mjs (DDL/constants), core.mjs (CRUD + retrieval),
164
+ code-index.mjs + code-sync.mjs (code_chunks), docs.mjs (doc_chunks)
165
+ memory.mjs re-export shim → src/memory/*
159
166
  session.mjs session persistence (up to 5 archive slots, isolated by project cwd)
160
167
  skills.mjs skill discovery/loading (.thincoder/skills/*.md)
161
168
  markdown.mjs entry format (frontmatter parse/serialize)
162
- gitmem.mjs Team layer git sync (clone/pull --rebase/push, system git)
169
+ git/ checkpoint.mjs (git patch snapshots / rewind), gitmem.mjs (Team layer git sync)
163
170
  distill.mjs session knowledge extraction (candidates + human confirmation)
164
- checkpoint.mjs git patch snapshots / rewind
165
171
  config.mjs config loading
166
- tui.mjs bare-ANSI terminal UI (wide-char wrapping, scrolling, permission prompts, slash commands)
172
+ tui/ bare-ANSI terminal UI index.mjs (startTUI), render.mjs (drawing primitives),
173
+ render-frame.mjs (frame layout), ansi.mjs
174
+ tui.mjs re-export shim → src/tui/index.mjs
175
+ tui-render.mjs re-export shim → src/tui/render.mjs
176
+ prompts/ prompt texts — system.md (core), discipline.md (coding/testing rules),
177
+ main.md (main-agent overlay), explore.md / coder.md / plan.md (subagent roles)
167
178
  test/ node:test offline unit tests (npm test)
168
179
  scripts/ real-environment verification scripts (compaction, team sync)
169
180
  ```
@@ -193,6 +204,14 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
193
204
 
194
205
  ## Changelog
195
206
 
207
+ ### 0.8.0 (2026-07)
208
+ - **TUI rendering overhaul**: layout engine (`layout.mjs`) — panels are positioned declaratively by priority instead of hand-pinned arithmetic; `renderFrame` is now a pure function (no state mutation); cursor position derived from layout coordinates. Status bar slash-command hints moved from if-else chain to lookup table. Three Chinese UI strings fixed to English
209
+ - **Subagent panel redesigned**: per-instance tracking (`role#id/` prefix) — parallel subagents of the same role no longer overwrite each other. Shows current tool name + args, or streaming text last line (what it's writing right now), instead of truncated 200-char token fragments. `onToolCall` relayed from subagent to parent TUI. Only the earliest running subagent is marked done on completion (not all)
210
+ - **Tool file split**: `system.mjs` (337 lines) split into `bash.mjs` / `glob.mjs` / `grep.mjs` / `ls.mjs`; `repomap.mjs` (306 lines) split into `repomap.mjs` (public API) + `repomap-parse.mjs` (dependency graph parsing)
211
+ - **Export position normalization**: 8 files rearranged — exports at top, implementation helpers below, for top-down readability
212
+ - **Provider management UX**: Add Provider excludes already-added presets from the list; Esc in sub-pickers (Add/Remove/Key) returns to model picker instead of exiting; config operations no longer leave traces in the conversation flow (picker refresh is the feedback); `❯ You:` label restored for user messages
213
+ - **distill-cmd import fix**: resolved export name mismatch that crashed TUI startup
214
+
196
215
  ### 0.7.8 (2026-07)
197
216
  - **Provider config flow streamlined**: adding a provider now immediately prompts for API key (no more "go back to /provider → Set Key"). Switching to a keyless provider via `/model` also prompts for key inline. Empty input gives clear "skipped" feedback
198
217
  - **bash tool: file writing forbidden**: models must use write/edit/insert_after/apply_patch instead of `echo`/`sed`/`printf > file`. Fixes GBK encoding corruption on Chinese Windows (cmd.exe writes redirected files in ANSI code page, not UTF-8). `PYTHONIOENCODING=utf-8` set for Python subprocesses on Windows
@@ -233,7 +252,7 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
233
252
  - **TPM/RPM proactive rate gate**: with `tpm`/`rpm` budgets configured on a provider, requests are booked against a local sliding window (60s, input+output) before sending — over budget means sleeping until the window frees up instead of gambling on 429s; covers the main loop / compaction summaries / subagents / truncation resume. Status bar shows `TPM throttle wait ~Ns`; the gate is off for unconfigured providers
234
253
  - **429-specific backoff**: respects the `Retry-After` header, otherwise backs off 15s/30s/60s (60s window — sub-second backoff is pointless); quota/balance errors (`exceeded_current_quota_error`) are distinguished from rate limits and no longer retried uselessly
235
254
  - **Dependency injection as compact summary**: `buildSummary` (directory-level dependencies + hub files + entry points, naturally ~1-2k chars) replaces the full-outline injection; detailed import/export available on demand via `repo_outline`
236
- - **TUI menus**: `/model` `/config` `/provider` `/think` `/mcp` `/goal` `/session` `/rewind` unified into picker menus
255
+ - **TUI menus**: `/model` `/config` `/provider` `/think` `/mcp` `/goal` `/session` `/restore` unified into picker menus
237
256
  - **Session robustness**: corrupted files or disk errors during archive/switch no longer crash — silently abandoned
238
257
 
239
258
  ### 0.7.1 (2026-07)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ // Thin shim — npm's "bin" field doesn't accept .mjs entries.
3
+ // Delegates to the real entry point.
4
+ import "./thincoder.mjs"
package/bin/thincoder.mjs CHANGED
@@ -10,15 +10,16 @@
10
10
  * thincoder --help 显示帮助
11
11
  */
12
12
 
13
- import { existsSync, readFileSync } from "node:fs"
14
- import { createInterface } from "node:readline"
15
- import { execSync } from "node:child_process"
13
+ import { readFileSync } from "node:fs"
16
14
  import { join } from "node:path"
17
- import { createAgent, runAgent } from "../src/agent.mjs"
18
- import { loadConfig, saveConfig, configDir, configPath, PROVIDER_PRESETS } from "../src/config.mjs"
19
- import { createMemory, memoryTools, put, remove, search, list, syncDir, codeSearchTool, docSearchTool } from "../src/memory.mjs"
20
- import { repoOutlineTool } from "../src/repomap.mjs"
21
- import { builtinTools } from "../src/tools.mjs"
15
+ import { runAgent } from "../src/agent.mjs"
16
+ import { loadConfig, configPath } from "../src/config.mjs"
17
+ import { createMemory, syncDir } from "../src/memory.mjs"
18
+ import { makeAgent, teamConfig, gitAuthor } from "../src/cli/make-agent.mjs"
19
+ import { memoryCommand } from "../src/cli/memory-command.mjs"
20
+ import { setupWizard } from "../src/cli/setup-wizard.mjs"
21
+ import { summarize, askPermission } from "../src/cli/permission.mjs"
22
+ import { distillCommand } from "../src/cli/distill-command.mjs"
22
23
 
23
24
  const [command, ...args] = process.argv.slice(2)
24
25
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
@@ -64,82 +65,19 @@ function exitSoon(code) {
64
65
  setTimeout(() => process.exit(code), 100)
65
66
  }
66
67
 
67
- /** 组装一个带记忆的 agent(同步各层索引后返回) */
68
- async function makeAgent() {
69
- const config = loadConfig()
70
- const provider = config.provider
71
- const providers = config.providersList
72
- const memory = createMemory({ dbPath: config.memory.dbPath })
73
- // 向量检索:配了 embedding 就启用(惰性生成向量,首次搜索时补算)
74
- if (config.embedding?.apiKey) {
75
- const { createEmbedder } = await import("../src/embedding.mjs")
76
- memory.embedder = createEmbedder(config.embedding)
77
- }
78
- const cwd = process.cwd()
79
- // code/doc 索引按 origin(项目根目录)隔离:检索只查本项目
80
- memory.codeOrigin = cwd
81
- // Project 层:启动时同步 .thincoder/memory/ 目录到索引(有就同步,没有就跳过)
82
- if (config.memory.projectDir) {
83
- memory.projectOrigin = join(cwd, config.memory.projectDir)
84
- await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
85
- }
86
- // Team 层(可选):首次自动 clone;启动只索引本地目录,拉取远端走显式 thincoder sync
87
- const team = teamConfig(config)
88
- if (team) {
89
- const { ensureClone } = await import("../src/gitmem.mjs")
90
- await ensureClone(team)
91
- await syncDir(memory, { layer: "team", dir: team.dir })
92
- }
93
- const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd)]
94
-
95
- // MCP servers:并行连接(一个死 server 不会拖住启动),失败的收集警告(TUI 下 stderr 不可见,通过 agent 对象传递)
96
- const mcpServers = config.mcp?.servers ?? []
97
- let mcpTools = []
98
- const mcpWarnings = []
99
- if (mcpServers.length) {
100
- const { connectMcpServer } = await import("../src/mcp.mjs")
101
- const results = await Promise.allSettled(mcpServers.map((srv) => connectMcpServer(srv)))
102
- for (let i = 0; i < results.length; i++) {
103
- const r = results[i]
104
- if (r.status === "fulfilled") {
105
- mcpTools = mcpTools.concat(r.value)
106
- } else {
107
- const srv = mcpServers[i]
108
- const msg = `MCP server "${srv.name ?? srv.command}" failed to connect: ${r.reason?.message ?? r.reason}`
109
- console.error(`[mcp] ${msg}`)
110
- mcpWarnings.push(msg)
111
- }
68
+ /** 语义化版本比较:a<b 返回 -1,相等 0,a>b 返回 1;非数字段按字符串比 */
69
+ function compareVersions(a, b) {
70
+ const pa = String(a).split("."), pb = String(b).split(".")
71
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
72
+ const xa = pa[i] ?? "0", xb = pb[i] ?? "0"
73
+ const na = Number(xa), nb = Number(xb)
74
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) {
75
+ if (na !== nb) return na < nb ? -1 : 1
76
+ } else if (xa !== xb) {
77
+ return xa < xb ? -1 : 1
112
78
  }
113
79
  }
114
-
115
- const agent = createAgent({
116
- provider,
117
- tools: [...baseTools, ...mcpTools],
118
- config,
119
- cwd,
120
- memory,
121
- })
122
- agent.providers = providers
123
- agent.activeProvider = config.activeProvider
124
- agent._mcpWarnings = mcpWarnings
125
- return agent
126
- }
127
-
128
- /** 读取 team 配置并补全默认目录;未配置返回 null */
129
- function teamConfig(config) {
130
- const team = config.memory?.team
131
- if (!team?.repo) return null
132
- const name = team.name ?? "default"
133
- return { name, repo: team.repo, dir: team.dir ?? join(configDir, "teams", name) }
134
- }
135
-
136
- /** 条目作者:git config user.name 兜底 unknown */
137
- function gitAuthor() {
138
- try {
139
- return execSync("git config user.name", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "unknown"
140
- } catch {
141
- return "unknown"
142
- }
80
+ return 0
143
81
  }
144
82
 
145
83
  switch (command) {
@@ -233,7 +171,8 @@ switch (command) {
233
171
  if (config.memory.projectDir) {
234
172
  memory.projectOrigin = join(process.cwd(), config.memory.projectDir)
235
173
  }
236
- await memoryCommand(memory, args)
174
+ const code = await memoryCommand(memory, args)
175
+ if (code) exitSoon(code)
237
176
  break
238
177
  }
239
178
 
@@ -247,7 +186,7 @@ switch (command) {
247
186
  break
248
187
  }
249
188
  const memory = createMemory({ dbPath: config.memory.dbPath })
250
- const { ensureClone, pullTeam } = await import("../src/gitmem.mjs")
189
+ const { ensureClone, pullTeam } = await import("../src/git/gitmem.mjs")
251
190
  try {
252
191
  const cloned = await ensureClone(team)
253
192
  if (cloned) console.log(`Cloned team repo to ${team.dir}`)
@@ -262,78 +201,8 @@ switch (command) {
262
201
  }
263
202
 
264
203
  case "distill": {
265
- const flags = {}
266
- const positional = []
267
- for (const a of args) {
268
- const m = a.match(/^--([\w-]+)(?:=(.*))?$/)
269
- if (m) flags[m[1]] = m[2] ?? true
270
- else positional.push(a)
271
- }
272
- const file = positional[0]
273
- if (!file) {
274
- console.error("Usage: thincoder distill <transcript-file> [--yes] [--scope=personal|project|team]")
275
- exitSoon(1)
276
- break
277
- }
278
- const { readFile } = await import("node:fs/promises")
279
- const transcript = await readFile(file, "utf8")
280
-
281
- const config = loadConfig()
282
- let provider = config.provider
283
- if (!provider.apiKey) {
284
- if (!process.stdin.isTTY) {
285
- console.error(noKeyMessage())
286
- exitSoon(1)
287
- break
288
- }
289
- provider = await setupWizard()
290
- if (!provider) {
291
- exitSoon(1)
292
- break
293
- }
294
- }
295
- const memory = createMemory({ dbPath: config.memory.dbPath })
296
- const team = teamConfig(config)
297
- const { extractCandidates, saveCandidate } = await import("../src/distill.mjs")
298
-
299
- console.error("[distill] extracting candidates...")
300
- let candidates
301
- try {
302
- candidates = await extractCandidates(provider, transcript)
303
- } catch (error) {
304
- console.error(`[distill] ${error.message}`)
305
- exitSoon(1)
306
- break
307
- }
308
- if (candidates.length === 0) {
309
- console.log("No distillable knowledge found in this session.")
310
- break
311
- }
312
-
313
- const opts = {
314
- projectDir: config.memory.projectDir ? join(process.cwd(), config.memory.projectDir) : null,
315
- team,
316
- author: gitAuthor(),
317
- }
318
- let saved = 0
319
- for (const c of candidates) {
320
- if (flags.scope) c.scope = flags.scope
321
- console.log(`\n--- candidate ---`)
322
- console.log(`[${c.type}] ${c.title} (scope: ${c.scope})`)
323
- console.log(c.content)
324
- if (c.type === "rule") {
325
- console.log("(rule 类知识通常建议手动撰写;确认提取吗?)")
326
- }
327
- const accept = flags.yes ? true : await askPermission("distill-save", { title: c.title })
328
- if (!accept) {
329
- console.log("skipped")
330
- continue
331
- }
332
- const where = await saveCandidate(memory, c, opts)
333
- console.log(`saved -> ${where}`)
334
- saved++
335
- }
336
- console.log(`\nDistilled ${saved}/${candidates.length} entries.`)
204
+ const code = await distillCommand(args, exitSoon)
205
+ if (code) exitSoon(code)
337
206
  break
338
207
  }
339
208
 
@@ -350,7 +219,7 @@ switch (command) {
350
219
  }
351
220
  const team = teamConfig(config)
352
221
  if (team) {
353
- const { ensureClone } = await import("../src/gitmem.mjs")
222
+ const { ensureClone } = await import("../src/git/gitmem.mjs")
354
223
  await ensureClone(team)
355
224
  const s = await syncDir(memory, { layer: "team", dir: team.dir })
356
225
  total.added += s.added
@@ -401,6 +270,7 @@ switch (command) {
401
270
  case "upgrade": {
402
271
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
403
272
  const local = pkg.version
273
+ const { execSync } = await import("node:child_process")
404
274
  let remote
405
275
  try {
406
276
  remote = execSync("npm view thincoder version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()
@@ -437,192 +307,3 @@ switch (command) {
437
307
  exitSoon(1)
438
308
  }
439
309
  }
440
-
441
- // ---------------------------------------------------------------- memory 子命令
442
-
443
- async function memoryCommand(memory, args) {
444
- const [sub, ...rest] = args
445
-
446
- const flags = {}
447
- const positional = []
448
- for (const a of rest) {
449
- const m = a.match(/^--([\w-]+)=(.*)$/)
450
- if (m) flags[m[1]] = m[2]
451
- else positional.push(a)
452
- }
453
-
454
- switch (sub) {
455
- case "list": {
456
- const entries = await list(memory, { type: flags.type })
457
- printEntries(entries)
458
- break
459
- }
460
- case "search": {
461
- const query = positional.join(" ")
462
- if (!query) {
463
- console.error("Usage: thincoder memory search <query>")
464
- exitSoon(1)
465
- break
466
- }
467
- printEntries(await search(memory, query, { limit: 10 }))
468
- break
469
- }
470
- case "put": {
471
- if (!flags.type || !flags.title || !flags.content) {
472
- console.error("Usage: thincoder memory put --type=<rule|knowledge|decision|pattern> --title=<t> --content=<c> [--tags=<t>]")
473
- exitSoon(1)
474
- break
475
- }
476
- const id = await put(memory, { type: flags.type, title: flags.title, content: flags.content, tags: flags.tags ?? "" })
477
- console.log(`Saved (id=${id})`)
478
- break
479
- }
480
- case "remove": {
481
- const id = Number(positional[0])
482
- if (!id) {
483
- console.error("Usage: thincoder memory remove <id>")
484
- exitSoon(1)
485
- break
486
- }
487
- console.log((await remove(memory, id)) ? `Removed #${id}` : `No entry #${id}`)
488
- break
489
- }
490
- default:
491
- console.error("Usage: thincoder memory <list|search|put|remove>")
492
- exitSoon(1)
493
- }
494
- }
495
-
496
- function printEntries(entries) {
497
- if (entries.length === 0) {
498
- console.log("(no entries)")
499
- return
500
- }
501
- for (const e of entries) {
502
- console.log(`#${e.id} [${e.type}] ${e.title}${e.tags ? ` (${e.tags})` : ""}`)
503
- console.log(` ${e.content.split("\n")[0].slice(0, 100)}`)
504
- }
505
- }
506
-
507
- // ---------------------------------------------------------------- 工具函数
508
-
509
- function summarize(toolArgs) {
510
- const s = JSON.stringify(toolArgs)
511
- return s.length > 120 ? s.slice(0, 120) + "..." : s
512
- }
513
-
514
- /** 语义化版本比较:a<b 返回 -1,相等 0,a>b 返回 1;非数字段按字符串比 */
515
- function compareVersions(a, b) {
516
- const pa = String(a).split("."), pb = String(b).split(".")
517
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
518
- const xa = pa[i] ?? "0", xb = pb[i] ?? "0"
519
- const na = Number(xa), nb = Number(xb)
520
- if (!Number.isNaN(na) && !Number.isNaN(nb)) {
521
- if (na !== nb) return na < nb ? -1 : 1
522
- } else if (xa !== xb) {
523
- return xa < xb ? -1 : 1
524
- }
525
- }
526
- return 0
527
- }
528
-
529
- /** 权限请求的关键信息(按工具定制),与 TUI 的 formatPermission 对齐。name 可能带子 agent 前缀("coder/bash"),取基名匹配 */
530
- function formatPermission(name, args) {
531
- const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(共 ${s.length} 字符)` : s)
532
- const base = name.includes("/") ? name.split("/").pop() : name
533
- if (base === "bash") return cap(args.command ?? "")
534
- if (base === "write") return `${args.path}(写入 ${(args.content ?? "").length} 字符)\n${cap(args.content ?? "", 1000)}`
535
- if (base === "edit") {
536
- const oldLines = cap(args.old_string ?? "", 500).split("\n").map((l) => `- ${l}`).join("\n")
537
- const newLines = cap(args.new_string ?? "", 500).split("\n").map((l) => `+ ${l}`).join("\n")
538
- return `${args.path}\n${oldLines}\n ↓\n${newLines}`
539
- }
540
- if (base === "delete") return `${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`
541
- if (base === "subagent") return cap(args.task ?? "", 500)
542
- if (base === "memory_put") return `[${args.type ?? ""}] ${args.title ?? ""}\n${cap(args.content ?? "", 500)}`
543
- return cap(summarize(args), 300)
544
- }
545
-
546
- /** 权限确认:TTY 下交互询问 y/n;非交互环境默认拒绝(安全优先) */
547
- async function askPermission(name, toolArgs) {
548
- if (!process.stdin.isTTY) {
549
- console.error(`\n[deny] ${name} (non-interactive, side-effect tools require a TTY)`)
550
- return false
551
- }
552
- const rl = createInterface({ input: process.stdin, output: process.stderr })
553
- try {
554
- const answer = await new Promise((resolve) => {
555
- rl.question(`\n[allow?] ${name}\n${formatPermission(name, toolArgs)}\n(y/N) `, resolve)
556
- })
557
- return answer.trim().toLowerCase() === "y"
558
- } finally {
559
- rl.close()
560
- }
561
- }
562
-
563
- /** 首次使用(TTY 下的 chat/distill):问答式配置一个 provider 并落盘,返回运行时 provider;取消返回 null */
564
- async function setupWizard() {
565
- // 自带缓冲的提问器:rl.question 在输入被管道/快速粘贴时会丢行(问题注册前 line 已到达)
566
- const rl = createInterface({ input: process.stdin, terminal: false })
567
- const buffered = []
568
- let waiter = null
569
- rl.on("line", (line) => {
570
- if (waiter) {
571
- const w = waiter
572
- waiter = null
573
- w(line)
574
- } else {
575
- buffered.push(line)
576
- }
577
- })
578
- const ask = (q) =>
579
- new Promise((resolve) => {
580
- process.stderr.write(q)
581
- if (buffered.length) resolve(buffered.shift())
582
- else waiter = resolve
583
- })
584
- try {
585
- const presets = Object.entries(PROVIDER_PRESETS)
586
- console.error("首次使用,先配置一个模型提供商:")
587
- presets.forEach(([n, p], i) => console.error(` ${i + 1}. ${n.padEnd(10)} ${p.desc}`))
588
- console.error(` ${presets.length + 1}. 自定义端点`)
589
- const choice = Number((await ask(`选择 [1-${presets.length + 1}]: `)).trim())
590
- let name, baseURL, model
591
- if (choice === presets.length + 1) {
592
- name = (await ask("名称(如 my-openai): ")).trim()
593
- baseURL = (await ask("baseURL(如 https://api.openai.com/v1): ")).trim().replace(/\/+$/, "")
594
- model = (await ask("模型(如 gpt-4o): ")).trim()
595
- if (!name || !/^https?:\/\//.test(baseURL) || !model) {
596
- console.error("输入不完整或 baseURL 不合法,已取消")
597
- return null
598
- }
599
- } else if (choice >= 1 && choice <= presets.length) {
600
- name = presets[choice - 1][0]
601
- baseURL = presets[choice - 1][1].baseURL
602
- model = presets[choice - 1][1].model
603
- } else {
604
- console.error("无效选择,已取消")
605
- return null
606
- }
607
- const apiKey = (await ask(`${name} 的 API key: `)).trim()
608
- if (!apiKey) {
609
- console.error("key 不能为空,已取消")
610
- return null
611
- }
612
- const embedKey = (await ask("可选:embedding API key(SiliconFlow,向量检索用;回车跳过): ")).trim()
613
- const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
614
- const providers = raw.providers?.length ? raw.providers : []
615
- const existing = providers.find((p) => p.name === name)
616
- if (existing) Object.assign(existing, { baseURL, model, apiKey })
617
- else providers.push({ name, baseURL, model, apiKey })
618
- raw.providers = providers
619
- raw.activeProvider = name
620
- if (embedKey) raw.embedding = { ...(raw.embedding ?? {}), apiKey: embedKey }
621
- saveConfig(raw)
622
- console.error(`配置完成:${name} / ${model}(已写入 ${configPath})`)
623
- console.error(embedKey ? "向量检索已启用\n" : "(未配 embedding key:记忆为纯文本检索,之后在 config.json 的 embedding.apiKey 补上即可开启向量检索)\n")
624
- return { name, baseURL, model, apiKey }
625
- } finally {
626
- rl.close()
627
- }
628
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.7.8",
3
+ "version": "0.8.1",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "type": "module",
16
16
  "bin": {
17
- "thincoder": "./bin/thincoder.mjs"
17
+ "thincoder": "./bin/thincoder.js"
18
18
  },
19
19
  "engines": {
20
20
  "node": ">=24"
@@ -0,0 +1,98 @@
1
+ /**
2
+ * agent/dispatch.mjs — 工具调用两段式执行
3
+ */
4
+ import { offloadToolResult } from "./helpers.mjs"
5
+
6
+ /**
7
+ * 两段式执行:
8
+ * 阶段一(串行):逐个解析参数 + planMode 检查 + 权限确认(有副作用工具)
9
+ * 阶段二(保序执行):严格按模型调用顺序——连续的只读/parallel 工具并发成组,
10
+ * 有副作用工具在原位置逐个串行(写后读同一文件的一批调用,读必须看到写后的内容)。
11
+ * 返回按调用顺序排列的结果数组(每项含 ok 标记执行成败)。
12
+ */
13
+ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth = 0, signal) {
14
+ // ---- 阶段一:串行准备 ----
15
+ const prepared = []
16
+ for (const toolCall of toolCalls) {
17
+ const tool = toolByName.get(toolCall.name)
18
+ let args = {}
19
+ try {
20
+ args = JSON.parse(toolCall.arguments || "{}")
21
+ } catch {
22
+ prepared.push({ toolCall, tool: null, error: `Invalid tool arguments JSON: ${toolCall.arguments}` })
23
+ continue
24
+ }
25
+
26
+ if (!tool) {
27
+ prepared.push({ toolCall, tool: null, error: `Unknown tool: ${toolCall.name}` })
28
+ continue
29
+ }
30
+
31
+ if (agent.planMode && !tool.readonly) {
32
+ prepared.push({ toolCall, tool, denied: true, reason: "plan mode" })
33
+ continue
34
+ }
35
+
36
+ if (!tool.readonly) {
37
+ // autoApprove 短路:agent 已标记自动批准时不再询问
38
+ const allowed = agent.autoApprove
39
+ ? true
40
+ : callbacks.onPermissionRequest
41
+ ? await callbacks.onPermissionRequest(toolCall.name, args)
42
+ : false
43
+ if (!allowed) {
44
+ prepared.push({ toolCall, tool, denied: true, reason: callbacks.onPermissionRequest ? "denied by user" : "no permission handler" })
45
+ continue
46
+ }
47
+ }
48
+
49
+ callbacks.onToolCall?.(toolCall.name, args)
50
+ prepared.push({ toolCall, tool, args })
51
+ }
52
+
53
+ // ---- 阶段二:保序执行 ----
54
+ const runOne = async (item) => {
55
+ if (item.error) return { ...item, result: `Error: ${item.error}`, ok: false }
56
+ if (item.denied) {
57
+ const reason = item.reason === "plan mode"
58
+ ? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
59
+ : "Error: permission denied by user"
60
+ return { ...item, result: reason, ok: false }
61
+ }
62
+ try {
63
+ const raw = String(await item.tool.execute(item.args, {
64
+ cwd: agent.cwd,
65
+ agent,
66
+ depth,
67
+ signal,
68
+ callbacks,
69
+ onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk),
70
+ onQuestion: callbacks.onQuestion,
71
+ onPermissionRequest: callbacks.onPermissionRequest,
72
+ }))
73
+ const result = item.toolCall.name === "read_image" ? raw : await offloadToolResult(raw, item.toolCall.id)
74
+ callbacks.onToolResult?.(item.toolCall.name, result)
75
+ return { ...item, result, ok: true }
76
+ } catch (error) {
77
+ return { ...item, result: `Error: ${error.message}`, ok: false }
78
+ }
79
+ }
80
+
81
+ const results = []
82
+ let batch = []
83
+ const flush = async () => {
84
+ if (batch.length === 0) return
85
+ results.push(...await Promise.all(batch.map(runOne)))
86
+ batch = []
87
+ }
88
+ for (const item of prepared) {
89
+ if (item.tool && !item.tool.readonly && !item.tool.parallel) {
90
+ await flush()
91
+ results.push(await runOne(item))
92
+ } else {
93
+ batch.push(item)
94
+ }
95
+ }
96
+ await flush()
97
+ return results
98
+ }