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.
- package/README.md +32 -13
- package/bin/thincoder.js +4 -0
- package/bin/thincoder.mjs +27 -346
- package/package.json +2 -2
- package/src/agent/dispatch.mjs +98 -0
- package/src/agent/helpers.mjs +185 -0
- package/src/agent/setup.mjs +117 -0
- package/src/agent-tools/goal.mjs +71 -0
- package/src/agent-tools/plan.mjs +31 -0
- package/src/agent-tools/recent-changes.mjs +23 -0
- package/src/agent-tools/skill.mjs +46 -0
- package/src/agent-tools/subagent.mjs +113 -0
- package/src/agent-tools/task.mjs +67 -0
- package/src/agent-tools/verify.mjs +198 -0
- package/src/agent-tools.mjs +12 -0
- package/src/agent.mjs +90 -1040
- package/src/cli/distill-command.mjs +85 -0
- package/src/cli/make-agent.mjs +85 -0
- package/src/cli/memory-command.mjs +63 -0
- package/src/cli/permission.mjs +41 -0
- package/src/cli/setup-wizard.mjs +70 -0
- package/src/config.mjs +5 -8
- package/src/context.mjs +10 -13
- package/src/distill.mjs +4 -3
- package/src/embedding.mjs +4 -2
- package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
- package/src/mcp/helpers.mjs +37 -0
- package/src/mcp/transport-http.mjs +176 -0
- package/src/mcp/transport-stdio.mjs +84 -0
- package/src/mcp/transport-ws.mjs +87 -0
- package/src/mcp.mjs +4 -428
- package/src/memory/code-index.mjs +211 -0
- package/src/memory/code-sync.mjs +306 -0
- package/src/memory/core.mjs +277 -0
- package/src/memory/docs.mjs +262 -0
- package/src/memory/schema.mjs +426 -0
- package/src/memory.mjs +12 -1403
- package/src/provider/core.mjs +239 -0
- package/src/provider/index.mjs +6 -0
- package/src/provider/rate.mjs +104 -0
- package/src/session.mjs +18 -5
- package/src/tools/bash.mjs +144 -0
- package/src/tools/file.mjs +205 -0
- package/src/tools/git.mjs +166 -0
- package/src/tools/glob.mjs +51 -0
- package/src/tools/grep.mjs +100 -0
- package/src/tools/index.mjs +22 -0
- package/src/tools/ls.mjs +36 -0
- package/src/tools/patch.mjs +226 -0
- package/src/tools/repomap-parse.mjs +168 -0
- package/src/tools/shared.mjs +257 -0
- package/src/tools/system.mjs +336 -0
- package/src/tools/web.mjs +121 -0
- package/src/tools.mjs +2 -1194
- package/src/tui/agent-turn.mjs +254 -0
- package/src/tui/ansi.mjs +32 -0
- package/src/tui/clipboard.mjs +48 -0
- package/src/tui/cmd-auto.mjs +21 -0
- package/src/tui/cmd-clear.mjs +26 -0
- package/src/tui/cmd-config.mjs +72 -0
- package/src/tui/cmd-exit.mjs +5 -0
- package/src/tui/cmd-extract.mjs +5 -0
- package/src/tui/cmd-goal.mjs +47 -0
- package/src/tui/cmd-help.mjs +25 -0
- package/src/tui/cmd-init.mjs +91 -0
- package/src/tui/cmd-mcp.mjs +146 -0
- package/src/tui/cmd-model.mjs +7 -0
- package/src/tui/cmd-new.mjs +18 -0
- package/src/tui/cmd-plan.mjs +21 -0
- package/src/tui/cmd-reindex.mjs +44 -0
- package/src/tui/cmd-restore.mjs +39 -0
- package/src/tui/cmd-session.mjs +42 -0
- package/src/tui/cmd-skills.mjs +17 -0
- package/src/tui/cmd-think.mjs +56 -0
- package/src/tui/config-helpers.mjs +34 -0
- package/src/tui/distill-cmd.mjs +45 -0
- package/src/tui/index.mjs +330 -0
- package/src/tui/interaction.mjs +79 -0
- package/src/tui/key-handler.mjs +267 -0
- package/src/tui/layout.mjs +115 -0
- package/src/tui/pickers.mjs +279 -0
- package/src/tui/render-frame.mjs +304 -0
- package/src/tui/render.mjs +205 -0
- package/src/tui/slash-commands.mjs +138 -0
- package/src/tui/startup.mjs +113 -0
- package/src/tui/wizard.mjs +168 -0
- package/src/tui-render.mjs +4 -0
- package/src/tui.mjs +3 -2566
- package/src/provider.mjs +0 -383
- /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
- /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
- /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
- /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
- /package/src/{main-overlay.md → prompts/main.md} +0 -0
- /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
- /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
- /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
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 `/
|
|
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), `/
|
|
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
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
158
|
-
|
|
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
|
-
|
|
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
|
|
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` `/
|
|
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)
|
package/bin/thincoder.js
ADDED
package/bin/thincoder.mjs
CHANGED
|
@@ -10,15 +10,16 @@
|
|
|
10
10
|
* thincoder --help 显示帮助
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
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 {
|
|
18
|
-
import { loadConfig,
|
|
19
|
-
import { createMemory,
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
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
|
-
/**
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
266
|
-
|
|
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.
|
|
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.
|
|
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
|
+
}
|