thincoder 0.7.6 → 0.7.8

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 CHANGED
@@ -2,133 +2,134 @@
2
2
 
3
3
  **Sharp Code, Zero Bloat.**
4
4
 
5
- **一个"" AI 编程 agent:纯** `.mjs`**、无构建、零 npm 依赖、Node.js 原生。**
5
+ **A "thin" AI coding agent: pure `.mjs`, no build step, zero npm dependencies, native Node.js.**
6
6
 
7
- ThinCoder "Thin" 不是"功能单薄",而是**思维锐利、直击要害**——像刀刃。
8
- AI agent 都在卷"全能"的今天,ThinCoder 打的是反面那张牌:**克制、精准、不废话**。
9
- 它的人设是一个"话不多、一刀见血"的极客工程师:你给它复杂需求,它还你干净实现。
7
+ The "Thin" in ThinCoder doesn't mean "feature-poor" — it means **sharp thinking, straight to the point** — like a blade.
8
+ While every AI agent races to be "all-powerful", ThinCoder plays the opposite card: **restraint, precision, no filler**.
9
+ Its persona is a geek engineer of few words who cuts to the bone: give it a complex requirement, get back a clean implementation.
10
10
 
11
- 设计哲学(也是这个名字的全部含义):只用 Node 标准库能实现的功能,就绝不引入依赖。整个项目的 `node_modules` 是空的。
11
+ Design philosophy (the entire meaning of the name): if the Node standard library can do it, no dependency is allowed. The project's `node_modules` is empty.
12
12
 
13
- ## 特性
13
+ ## Features
14
14
 
15
- - **Agent 主循环**:LLM ↔ 工具调用循环,上限 100 轮防失控,完成守卫拦截未验证的改动
16
- - **代码库理解** ⭐0.5.0:`repo_outline`(依赖大纲,启动自动注入)、`code_search`(源码 FTS5 + 向量 + JSDoc 提取)、`doc_search`(文档按 ## 标题分块检索)——后台索引、写文件自动增量更新、三工具按"结构→意图→细节"引导
17
- - **模型适配** ⭐:只跟顶流、只跟最新。内置 DeepSeek / Kimi / GLM / Qwen / MiniMax 五家国内顶流厂商的旗舰模型,不做老旧模型兼容、不做本地模型适配。自动匹配上下文窗口、截断续写协议(prefix/partial)、思考模式 API(thinking.type / reasoning_effort)、输出上限。
18
- - **工具集**:`read` / `write` / `edit` / `bash` / `glob`(支持 `**`) / `grep` / `websearch` / `ls` / `fetch` + `read_image`(图片/视频粘贴) + 三个检索工具 + MCP,全部零依赖,文件工具目录隔离
19
- - **记忆系统**:三层(personal/project/team),FTS5 + 向量 RRF 混合检索,markdown 格式 git 友好
20
- - **两段式工具调度**:权限确认串行,只读工具并行,副作用工具串行
21
- - **会话持久化** ⭐0.5.0:最多 5 个归档槽位,`/session` 随时切换,恢复时工具结果可见
22
- - **子 agent 并发**:`explore`/`plan`/`coder` 三种角色,并行派发,流式输出可见,报告进对话区
23
- - **Plan Mode**:只读探索 + 方案设计,用户确认后实现
24
- - **AUTO 模式**:`/auto` 完全授权,长任务免确认
25
- - **任务跟踪**:`task` 工具拆解多步任务,状态栏 ✓n/m 实时进度,自动过滤已完成项
26
- - **Goal/Verify/Skills**:长目标跟踪、完成验证、可复用技能
27
- - **流式 TUI**:裸 ANSI,权限预览紧挨输入框,write/edit 自动展示 diff,多模态模型时输入框右上角显示粘贴快捷键提示(Win: Alt+V / Mac/Linux: Ctrl+V
15
+ - **Agent loop**: LLM ↔ tool-call loop, 100-turn cap against runaway, completion guard blocks unverified changes
16
+ - **Codebase understanding** ⭐0.5.0: `repo_outline` (dependency outline, auto-injected at startup), `code_search` (source FTS5 + vectors + JSDoc extraction), `doc_search` (docs chunked by ## headings) — background indexing, auto-incremental updates on file writes, three tools guided by "structure → intent → details"
17
+ - **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from five leading Chinese vendors — DeepSeek / Kimi / GLM / Qwen / MiniMax. No legacy model compatibility, no local model support. Auto-matched context windows, truncation-resume protocols (prefix/partial), thinking-mode APIs (thinking.type / reasoning_effort), reasoning_content echo strategies (reasoningEcho), output limits, temperature range clamping — all five deeply adapted.
18
+ - **Toolset**: `read` / `write` / `edit` / `bash` / `glob` (supports `**`) / `grep` / `websearch` / `ls` / `fetch` + `read_image` (image/video paste) + three retrieval tools + MCP — all zero-dependency, file tools confined to the working directory
19
+ - **Memory system**: three layers (personal/project/team), FTS5 + vector RRF hybrid retrieval, git-friendly markdown format
20
+ - **Two-phase tool scheduling**: permission prompts serialized, read-only tools parallelized, side-effect tools serialized
21
+ - **Session persistence** ⭐0.5.0: up to 5 archive slots, `/session` to switch anytime, tool results visible after restore
22
+ - **Concurrent subagents**: three roles — `explore`/`plan`/`coder` — dispatched in parallel, streaming output visible, reports land in the conversation
23
+ - **Plan Mode**: read-only exploration + design, implement after user approval
24
+ - **AUTO mode**: `/auto` full authorization, no confirmations on long tasks
25
+ - **Task tracking**: `task` tool breaks down multi-step work, status bar ✓n/m live progress, auto-filters completed items
26
+ - **Goal/Verify/Skills**: long-goal tracking, completion verification, reusable skills
27
+ - **Streaming TUI**: bare ANSI, permission preview right above the input box, write/edit auto-shows diffs, paste shortcut hint in the input box corner for multimodal models (Win: Alt+V / Mac/Linux: Ctrl+V)
28
28
 
29
- ## 记忆系统:一人学到,全队皆知
29
+ ## Memory: What One Learns, the Whole Team Knows
30
30
 
31
- 三层记忆,全部"有就查、没有就跳过",统一混合检索:
31
+ Three layers, all "query if present, skip if absent", unified hybrid retrieval:
32
32
 
33
- | | 位置 | 同步方式 |
34
- | ---------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- |
35
- | **Personal** | `~/.thincoder/memory.db`(sqlite| 不同步,私有 |
36
- | **Project** | 项目仓库 `.thincoder/memory/*.md` | 随项目 gitThinCoder **只写文件,绝不替你 commit**) |
37
- | **Team**(可选) | 独立记忆仓库,clone `~/.thincoder/teams/<name>/` | `thincoder sync`(pull --rebase);写入时自动 commit + push(专用设施,可选启用) |
33
+ | Layer | Location | Sync method |
34
+ | ---------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
35
+ | **Personal** | `~/.thincoder/memory.db` (sqlite) | Not synced, private |
36
+ | **Project** | `.thincoder/memory/*.md` in the project repo | With the project git (ThinCoder **only writes files, never commits for you**) |
37
+ | **Team** (opt.) | Dedicated memory repo, cloned to `~/.thincoder/teams/<name>/` | `thincoder sync` (pull --rebase); auto commit + push on write (dedicated facility, opt-in) |
38
38
 
39
- - **混合检索**:FTS5BM25,中文逐字索引,双字词可命中)+ embedding 向量(暴力余弦)+ RRF(k=60) 融合排序
40
- - **embedding**:OpenAI 兼容 `/v1/embeddings`,默认 SiliconFlow `BAAI/bge-m3`(免费额度,中文好);Ollama 本地可作离线选项。向量惰性生成——写入不算,首次搜索时补算落库
41
- - **条目格式**:Markdown + frontmattertype/title/tags/author/created),GitHub 上直接可读可 review;每条目一个文件,天然规避合并冲突;真冲突时诚实报错,绝不自动合并
42
- - **双轨沉淀**:规范靠手动写(`memory_put`),经验靠 `/distill` 从会话提取——**LLM 出候选、逐条人工 y/n 确认**才入库,绝不做全自动沉淀
43
- - **检索隔离**:Project 层按项目路径隔离,A 项目的记忆不会漏进 B 项目
39
+ - **Hybrid retrieval**: FTS5 (BM25, per-character CJK indexing, bigrams matchable) + embedding vectors (brute-force cosine) + RRF(k=60) fusion ranking
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
+ - **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
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
46
46
 
47
47
  - Node.js >= 24
48
- - 一个 OpenAI 兼容端点的 API key
49
- - 可选:embedding 服务的 key(不配置则退化为纯 FTS 检索)
48
+ - An API key for any OpenAI-compatible endpoint
49
+ - Optional: an embedding service key (without it, retrieval degrades to pure FTS)
50
50
 
51
- ## 快速开始
51
+ ## Quick Start
52
52
 
53
53
  ```bash
54
- # 安装
54
+ # Install
55
55
  npm install -g thincoder
56
56
 
57
- # 启动 TUI(默认命令)
57
+ # Launch the TUI (default command)
58
58
  thincoder
59
59
  ```
60
60
 
61
- 首次启动会自动进入初始配置向导:方向键选提供商(内置预设或自定义端点)→ 输入 API key → 可选填 embedding keySiliconFlow,开启记忆向量检索,可跳过)→ 方向键选模型,全程不用手编配置文件。之后随时可用 `/provider`、`/model`、`/config embedkey` 调整。`chat`/`distill` 在终端下没配 key 时也会就地问答式配置(管道/CI 环境则报错退出并提示)。
61
+ First launch walks you through a setup wizard: arrow keys to pick a provider (built-in presets or a custom endpoint) → enter API key → optionally enter an embedding key (SiliconFlow, enables vector memory search, skippable) → arrow keys to pick a model — no hand-editing config files. Adjust anytime with `/provider`, `/model`, `/config embedkey`. `chat`/`distill` also offer in-place interactive setup when no key is configured in a terminal (in pipes/CI they exit with an error and instructions).
62
62
 
63
- 也可以直接手写配置 `~/.thincoder/config.json`(见下文"配置"),然后:
63
+ You can also hand-write `~/.thincoder/config.json` (see "Configuration" below), then:
64
64
 
65
65
  ```bash
66
- # 一次性问答(管道友好)
67
- thincoder chat "读一下 package.json 并总结"
66
+ # One-shot Q&A (pipe-friendly)
67
+ thincoder chat "read package.json and summarize it"
68
68
 
69
- # 记忆管理
70
- thincoder memory put --type=rule --title="代码规范" --content="不加分号"
71
- thincoder memory search "代码规范"
69
+ # Memory management
70
+ thincoder memory put --type=rule --title="code style" --content="no semicolons"
71
+ thincoder memory search "code style"
72
72
  thincoder memory list
73
73
  thincoder memory remove 1
74
74
 
75
- # 团队记忆(可选,配置 memory.team 后可用)
76
- thincoder sync # 拉取团队仓库并重建索引
75
+ # Team memory (optional, available after configuring memory.team)
76
+ thincoder sync # pull the team repo and rebuild the index
77
77
 
78
- # 从会话记录提取知识(逐条确认后入库)
78
+ # Extract knowledge from a session transcript (stored after per-item confirmation)
79
79
  thincoder distill session.txt
80
80
 
81
- # 升级
81
+ # Upgrade
82
82
  thincoder upgrade
83
83
  ```
84
84
 
85
- 从源码运行:把上面的 `thincoder` 换成 `node bin/thincoder.mjs`。
85
+ Running from source: replace `thincoder` above with `node bin/thincoder.mjs`.
86
86
 
87
- TUI 内斜杠命令:`/help`、`/model`(方向键选择全部 provider 的全部模型;`/model <名称>` 直接切换)、`/provider`(增/删 provider、配 key,支持自定义端点)、`/think`(思维模式开关与推理强度)、`/config`(查看配置、`/config embedkey` embedding key、`/config set` 改参数)、`/session`(列出/切换归档会话)、`/reindex`(重建索引)、`/distill`(从当前会话提取知识)、`/clear`、`/exit`。输入 `/` 时状态栏实时提示匹配命令。
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.
88
88
 
89
- 环境变量:`THINCODER_API_KEY`(或 `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`)、`THINCODER_BASE_URL`、`THINCODER_MODEL`、`SILICONFLOW_API_KEY`。
89
+ Environment variables: `THINCODER_API_KEY` (or `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`), `THINCODER_BASE_URL`, `THINCODER_MODEL`, `SILICONFLOW_API_KEY`.
90
90
 
91
- ## 配置
91
+ ## Configuration
92
92
 
93
- `~/.thincoder/config.json`:
93
+ `~/.thincoder/config.json`:
94
94
 
95
95
  ```jsonc
96
96
  {
97
97
  "providers": [
98
- // 可配多个,/model <名称> 切换
98
+ // multiple allowed; switch with /model <name>
99
99
  {
100
100
  "name": "deepseek",
101
- "baseURL": "https://api.deepseek.com/v1", // 任意 OpenAI 兼容端点
102
- "apiKey": "sk-...", // 或留空走环境变量
101
+ "baseURL": "https://api.deepseek.com/v1", // any OpenAI-compatible endpoint
102
+ "apiKey": "sk-...", // or leave empty to use env vars
103
103
  "model": "deepseek-chat",
104
- // 可选:主动节流预算(按账户限速等级自配,不配则关闭闸门,429 退避仍生效)。
105
- // 限速是账户级独立计数器(RPM/TPM 60s 窗口),等级查各厂商控制台
106
- // "tpm": 200000, // tokens/分钟(输入+输出总量)
107
- // "rpm": 50, // 请求数/分钟
104
+ // optional: proactive throttling budget (match your account's rate-limit tier;
105
+ // without it the gate is off, 429 backoff still applies).
106
+ // Rate limits are per-account counters (RPM/TPM over a 60s window) — check each vendor's console
107
+ // "tpm": 200000, // tokens/minute (input + output total)
108
+ // "rpm": 50, // requests/minute
108
109
  },
109
110
  ],
110
- "activeProvider": "deepseek", // 当前激活的 provider
111
+ "activeProvider": "deepseek", // currently active provider name
111
112
  "embedding": {
112
- // 可选:不配则纯 FTS 检索
113
+ // optional: without it, retrieval is pure FTS
113
114
  "baseURL": "https://api.siliconflow.cn/v1",
114
- "apiKey": "sk-...", // SILICONFLOW_API_KEY
115
+ "apiKey": "sk-...", // or SILICONFLOW_API_KEY
115
116
  "model": "BAAI/bge-m3",
116
117
  },
117
118
  "agent": {
118
- "maxTurns": 100, // 工具循环上限
119
- "compactThreshold": 100000, // 上下文压缩阈值(约 token 数)
119
+ "maxTurns": 100, // tool-loop cap
120
+ "compactThreshold": 100000, // context compaction threshold (approx. tokens)
120
121
  },
121
122
  "memory": {
122
- "dbPath": "~/.thincoder/memory.db", // sqlite 索引库路径
123
- "projectDir": ".thincoder/memory", // Project 层目录(相对项目根)
123
+ "dbPath": "~/.thincoder/memory.db", // sqlite index path
124
+ "projectDir": ".thincoder/memory", // Project layer directory (relative to project root)
124
125
  "team": {
125
- // 可选:不配则 Team 层禁用
126
+ // optional: Team layer disabled without it
126
127
  "name": "myteam",
127
128
  "repo": "git@github.com:org/team-memory.git",
128
129
  },
129
130
  },
130
131
  "mcp": {
131
- // 可选:MCP server 列表
132
+ // optional: MCP server list
132
133
  "servers": [
133
134
  {
134
135
  "name": "filesystem",
@@ -140,143 +141,154 @@ TUI 内斜杠命令:`/help`、`/model`(方向键选择全部 provider 的全
140
141
  }
141
142
  ```
142
143
 
143
- ## 架构
144
+ ## Architecture
144
145
 
145
146
  ```
146
- bin/thincoder.mjs 命令入口(tui / chat / memory / sync / distill
147
+ bin/thincoder.mjs command entry (tui / chat / memory / sync / distill)
147
148
  src/
148
- provider.mjs LLM 调用(fetch, SSE 流式, 重试)
149
- embedding.mjs 向量嵌入(OpenAI 兼容 /v1/embeddings
150
- tools.mjs 16 个内置工具 + MCP 包装 + readonly 调度标记
151
- mcp.mjs MCP 客户端(JSON-RPC + stdio transport,零依赖)
152
- agent.mjs 主循环 + 两段式工具执行 + plan/task/goal/skill/subagent/verify 工具
153
- + 增量索引(write/edit/delete 后自动 reindexFile)
154
- repomap.mjs 仓库依赖大纲(import/export regex 解析,工具按需调用)
155
- context.mjs token 粗估 + 历史压缩 + task 回注
156
- memory.mjs 记忆核心:三层合并检索 + 代码/文档索引(code_chunks/doc_chunks
157
- + FTS5 + 向量 RRF + JSDoc 提取 + 单文件增量索引
158
- session.mjs 会话持久化(最多 5 个归档槽位,按项目 cwd 隔离)
159
- skills.mjs 技能发现/加载(.thincoder/skills/*.md
160
- markdown.mjs 条目格式(frontmatter 解析/序列化)
161
- gitmem.mjs Team git 同步(clone/pull --rebase/push,系统 git
162
- distill.mjs 会话知识提取(候选 + 人工确认)
163
- checkpoint.mjs git patch 快照 / 回滚
164
- config.mjs 配置加载
165
- tui.mjs ANSI 终端 UI(宽字符折行、滚动、权限确认、斜杠命令)
166
- test/ node:test 离线单测(npm test
167
- scripts/ 真实环境验证脚本(压缩、团队同步)
149
+ provider.mjs LLM calls (fetch, SSE streaming, retries)
150
+ 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
154
+ + incremental indexing (auto reindexFile after write/edit/delete)
155
+ repomap.mjs repo dependency outline (import/export regex parsing, on-demand via tool)
156
+ 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
159
+ session.mjs session persistence (up to 5 archive slots, isolated by project cwd)
160
+ skills.mjs skill discovery/loading (.thincoder/skills/*.md)
161
+ markdown.mjs entry format (frontmatter parse/serialize)
162
+ gitmem.mjs Team layer git sync (clone/pull --rebase/push, system git)
163
+ distill.mjs session knowledge extraction (candidates + human confirmation)
164
+ checkpoint.mjs git patch snapshots / rewind
165
+ config.mjs config loading
166
+ tui.mjs bare-ANSI terminal UI (wide-char wrapping, scrolling, permission prompts, slash commands)
167
+ test/ node:test offline unit tests (npm test)
168
+ scripts/ real-environment verification scripts (compaction, team sync)
168
169
  ```
169
170
 
170
- 关键设计:
171
+ Key design decisions:
171
172
 
172
- - **工具执行两段式**:阶段一串行做权限确认(有副作用工具逐个问用户);阶段二只读工具 `Promise.all` 并行、有副作用工具串行。结果按 `toolCallId` 配对回喂
173
- - **权限在 UI 层**:工具只负责执行,"问不问用户" TUI/CLI 的事,headless 场景不用改工具
174
- - **索引是易失品**:sqlite 只是代码/文档/记忆的本地索引,`reindex` 随时可重建
175
- - **代码/文档分离索引**:源码和 markdown 文档分表索引,LLM 通过不同工具检索——避免模型把旧代码模式当做设计规范
176
- - **git 边界**:Project 层只写文件不碰用户的仓库;Team 层是 ThinCoder 自管仓库才可自动 commit+push
177
- - **中文检索**:FTS5 unicode61 + 写入/查询两侧 CJK 逐字加空格;语义匹配走向量通道
173
+ - **Two-phase tool execution**: phase one serializes permission prompts (each side-effect tool asks the user); phase two runs read-only tools in parallel (`Promise.all`) and side-effect tools serially. Results are fed back paired by `toolCallId`
174
+ - **Permissions live in the UI layer**: tools only execute; "ask the user or not" is the TUI/CLI's business, so headless scenarios need no tool changes
175
+ - **The index is disposable**: sqlite is just a local index of code/docs/memories — `reindex` rebuilds it anytime
176
+ - **Separate code/doc indexes**: source and markdown docs are indexed in separate tables and searched through different tools — keeps the model from mistaking old code patterns for design conventions
177
+ - **git boundaries**: the Project layer only writes files and never touches your repo's commits; the Team layer is a ThinCoder-managed repo where auto commit+push is allowed
178
+ - **CJK retrieval**: FTS5 unicode61 + per-character CJK spacing on both write and query sides; semantic matching goes through the vector channel
178
179
 
179
- ## 开发
180
+ ## Development
180
181
 
181
182
  ```bash
182
- npm test # 离线单测(node:test,含本地 mock 服务)
183
- node scripts/verify-compress.mjs # 上下文压缩的真实 API 验证(需要有效配置)
184
- node scripts/verify-team.mjs # 团队记忆 A->git->B 全链路验证(本地 git,离线)
183
+ npm test # offline unit tests (node:test, with local mock servers)
184
+ node scripts/verify-compress.mjs # real-API verification of context compaction (needs valid config)
185
+ node scripts/verify-team.mjs # team memory A->git->B full-chain verification (local git, offline)
185
186
  ```
186
187
 
187
- 代码约定:纯 `.mjs`,不加分号,禁止引入 npm 依赖(包括开发依赖)。
188
-
189
- ## 路线图
190
-
191
- - MCP HTTP transport(当前仅 stdio)
192
- - 更多内置 skills
193
-
194
- ## 更新日志
195
-
196
- ### 0.7.6(2026-07)
197
- - **SYSTEM_PROMPT 拆分为 core + discipline**:核心规则(所有 agent 通用)与编码/测试/调试纪律(主 agent + coder)分离,explore/plan agent 不再被无关的写代码条款消耗注意力——单一真相源,修一条规则只改一个文件
198
- - **git 驱动增量索引**:新增 `gitSync`,启动时用 `git diff` 找出上次索引以来的变更文件,只重建这些文件的 FTS5 块。非 git 仓库 / 首次运行 / 大范围变更(>200 文件)自动退到全量扫描。`codeSync` + `docSync` 启动并行化
199
- - **reindexFile 后立即补向量**:agent 每次 write/edit 后的增量索引不再留向量 NULL,直接调 `ensureEmbeddings`——刚改的文件立刻有语义搜索能力
200
- - **项目指令防注入**:AGENTS.md 内容用 `escapeXml` + `<untrusted_project_instructions>` 包裹,堵住恶意项目指令的提示注入漏洞
201
- - **压缩阈值 cap**:1M 窗口模型的压缩阈值从 80 token 降到 30 万(`COMPACT_CAP_TOKENS`),防历史涨到打爆 TPM 预算、压缩请求本身 429
202
- - **readSSE tool_calls name 去重**:个别 API(GLM 偶尔)流里重发完整 name 而非增量,`+=` 累加会导致 `readread`。改为只取第一次非空值
203
- - **边缘场景思考覆盖全 prompt 层**:plan/explore/coder/main 四层 overlay 各加边缘场景识别规则(开放式提问,不枚举具体场景)
204
- - **测试纪律优化**:全量测试触发条件从"改了核心设施文件"改为"改了核心设施行为"——碰 memory.mjs 加个工具函数不再触发全量
205
-
206
- ### 0.7.5(2026-07)
207
- - **提示词复合指令拆分**:SYSTEM_PROMPT / main-overlay / coder-overlay 共 8 处复合句拆为独立 bullet(每条指令一个注意力节点),提升 DeepSeek/GLM/Qwen 等模型的指令遵从度——尤其"改完代码补测试"这类兜底条款不再被漏读
208
- - **测试纪律强化**:SYSTEM_PROMPT Testing discipline 新增独立 hard rule(改行为/加代码必须补测试);main-overlay 自检清单新增"现有测试是否覆盖变更";coder-overlay 最终检查清单新增补测试项
209
- - **plan mode 工作流**:main-overlay plan mode 指令从单条复合句拆为 3 步编号流程
210
-
211
- ### 0.7.4(2026-07)
212
- - **verify 分层自检**:默认 quick 模式(语法检查变更文件 + git diff + 自检清单,毫秒级),`full=true` 才跑全量 npm test——不再每改一行都等十几秒全量;quick 即满足完成守卫,收尾/改核心设施时再用 full
213
- - **提示词纪律强化**:SYSTEM_PROMPT 新增测试纪律(何时跑哪层验证)与调试策略(先诊断再治、一次只改一处);coder/plan/main overlay 补自检清单(最简方案、匹配项目模式、不碰无关文件)
214
- - **修复**:quick 模式下语法检查失败被误标为验证通过,完成守卫被架空
215
-
216
- ### 0.7.3(2026-07)
217
- - **图片粘贴**:新增 `read_image` 工具,支持从剪贴板粘贴图片/视频,多模态模型可直接理解截图、UI 设计稿、架构图(Win: `Alt+V` / Mac/Linux: `Ctrl+V`)
218
- - **TUI 粘贴提示**:使用多模态模型时,输入框右上角自动显示操作系统对应的粘贴快捷键,纯文本模型不显示
219
-
220
- ### 0.7.2(2026-07)
221
- - **TPM/RPM 主动节流闸门**:provider 配置 `tpm`/`rpm` 预算后,发请求前本地滑动窗口记账(60s,输入+输出),超预算先睡到窗口腾出空间而不是打 429 碰运气;主循环/压缩摘要/子 agent/截断续写全覆盖。等待时状态栏显示 `TPM 节流等待 ~Ns`,不配的 provider 闸门关闭
222
- - **429 专项退避**:尊重 `Retry-After` 响应头,无则按 15s/30s/60s(60s 窗口,秒级退避无意义);配额/余额错误(`exceeded_current_quota_error`)与限速区分,不再无效重试
223
- - **依赖注入改为紧凑摘要**:`buildSummary`(目录级依赖 + 枢纽文件 + 入口,天然 ~1-2k 字符)替代全量大纲注入,详细 import/export `repo_outline` 按需查
224
- - **TUI 菜单化**:`/model` `/config` `/provider` `/think` `/mcp` `/goal` `/session` `/rewind` 统一改为选择器菜单
225
- - **会话健壮性**:归档/切换时文件损坏或磁盘异常不再崩,静默放弃
226
-
227
- ### 0.7.1(2026-07
228
- - **修复上下文爆炸(紧急)**:依赖大纲开局注入不再无界——多仓库父目录(索引数千文件)的全量大纲实测达 140 万字符 35 token,且每轮对话重复注入累积,几轮即打爆上下文并触发 TPM 限流。现截断到 6000 字符(超出指引用 `repo_outline` 聚焦查询)且每会话只注一次
229
- - **压缩逃逸口**:历史太短(≤13 条)切不出中间段时压缩永远不发生,一条巨型消息(大段粘贴/超大注入)即可卡死。现走确定性瘦身:超长 user/tool 正文截断换桩,不动 reasoning_content tool_calls 配对
230
- - **修复 docSync ReferenceError**:`failed`/`errors` 未声明导致文档索引同步每次调用必抛错(两个测试挂红)
231
- - **apply_patch 工具**:统一 diff 多文件原子打补丁(任一 hunk 不上整体不写盘),权限预览直接展示 diff
232
- - **checkpoint 工具**:`list`/`create`/`rewind` 快照能力暴露给模型(此前只接 TUI 自动快照 + /rewind,模型无法自救);bash 销毁性 git 护栏升级为分段检测(`&&`/`;`/`|`/命令替换链式写法不再绕过)
233
- - **bash 进程树杀**:超时/中断整树杀(POSIX 进程组 / Windows taskkill /T),不再残留孙进程
234
- - **子 agent 显示契约**:只 relay 正文/思考 token TUI 滚动区,内部工具调用不再刷屏
235
- - **路径安全**:`resolveInCwd` symlink 逃逸(realpath 二次校验);edit 拒绝空 old_string;单文件增量索引跳过隐藏目录与 node_modules
236
- - **其他**:SQLite WAL + busy_timeout、schema 迁移单事务、升级语义化版本比较、MCP cmd.exe 引号翻倍转义、gitmem 无变更不提交
237
-
238
- ### 0.7.0(2026-07
239
- - **模型协议深度适配**:reasoning_content 回传按模型区分(`reasoningEcho` 规格表字段)——DeepSeek/Kimi 必须回传,GLM 不回传;reasoning_effort 枚举校验(`reasoningEffortEnum`);temperature 范围裁剪(`tempRange`)
240
- - **Qwen/MiniMax 规格补齐**:reasoning_effort 枚举(Qwen 3.8-max-preview)、temperature 范围(Qwen [0,2)、MiniMax [0,2])、MiniMax M3 thinking 模式
241
- - **grep 上下文行**:`before`/`after` 参数(对应 grep -B/-A),匹配行用 `:`、上下文行用 `-`,同文件相邻区间去重合并
242
- - **系统提示词边界规则**:不修改工作目录外文件,不许用 bash 绕过 read/write/edit 的目录限制
243
- - **question 工具输入框标题**:固定为 ` Question `,提问文本走对话区(不再塞进输入框标题)
244
-
245
- ### 0.5.0(2026-07)
246
- - **代码库理解**:`repo_outline`(依赖大纲,启动自动注入)、`code_search`(FTS5 + 向量 + JSDoc)、`doc_search`(按 ## 标题分块),写文件自动增量索引
247
- - **模型适配**:5 家内置预设(DeepSeek/Kimi/GLM/Qwen/MiniMax),maxTokens 拉满、截断续写、思考模式 API 自动匹配
248
- - 会话 5 槽位归档、`/session` 切换、恢复时展示工具结果
249
- - agent 流式输出可见、最终报告进对话区
250
- - 文件工具目录隔离、权限预览紧挨输入框
251
- - write/edit 自动附 git diff、edit 错误信息增强提示
252
- - task 自动过滤已完成项、全部 done 时主动提醒
253
- - 提示词引导"查官方文档 不一致就存项目记忆"
188
+ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (including devDependencies).
189
+
190
+ ## Roadmap
191
+
192
+ - More builtin skills
193
+
194
+ ## Changelog
195
+
196
+ ### 0.7.8 (2026-07)
197
+ - **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
+ - **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
199
+
200
+ ### 0.7.7 (2026-07)
201
+ - **Code review fixes (4 critical bugs)**:
202
+ - `gitSync` anchor never set after full `codeSync` fallback fast path was dead in production; now `codeSync`/`docSync` write the anchor on success
203
+ - `gitSync` skipped deleted files stale chunks remained in index forever; `--diff-filter` now includes `D`, and `ENOENT` is distinguished from other errors (failed files don't advance the anchor)
204
+ - Completion guard was a one-shot latch → after firing once, further mutations could finish unverified; now re-armed with a pushback counter (max 2 pushes, 3rd passes through)
205
+ - Verify-failure exhaustion returned raw model text without honesty framing → now injects a system reminder forcing the model to state what's still failing, what was tried, and that the work is unfinished
206
+ - **Input queue during processing**: messages typed while the agent is processing are queued and auto-executed when processing ends. Queue preview shown as a single line above the input box (no collision with subagent panel). `Ctrl+D` deletes the last queued item. `/cancel` and `/exit` bypass the queue
207
+
208
+ ### 0.7.6 (2026-07)
209
+ - **SYSTEM_PROMPT split into core + discipline**: core rules (shared by all agents) separated from coding/testing/debugging discipline (main agent + coder), so explore/plan subagents no longer burn attention on irrelevant coding clauses — single source of truth, one rule changed in one file
210
+ - **git-driven incremental indexing**: new `gitSync` uses `git diff` at startup to find files changed since the last index and rebuilds only their FTS5 chunks. Non-git repos / first run / large changesets (>200 files) automatically fall back to full scans. `codeSync` + `docSync` startup parallelized
211
+ - **Embeddings backfilled right after reindexFile**: incremental indexing after each agent write/edit no longer leaves vector NULLs — `ensureEmbeddings` runs immediately, so freshly changed files are semantically searchable at once
212
+ - **Project-instruction injection hardening**: AGENTS.md content wrapped with `escapeXml` + `<untrusted_project_instructions>`, closing the prompt-injection hole from malicious project instructions
213
+ - **Compaction threshold cap**: for 1M-window models the compaction threshold drops from 800K to 300K tokens (`COMPACT_CAP_TOKENS`), preventing history from blowing the TPM budget and the compaction request itself from hitting 429
214
+ - **readSSE tool_calls name dedup**: some APIs (GLM occasionally) resend the full name instead of deltas in the stream, and `+=` produced `readread`. Now only the first non-empty value is taken
215
+ - **Edge-case thinking across all prompt layers**: plan/explore/coder/main overlays each gained an edge-case recognition rule (open-ended, no scenario enumeration)
216
+ - **Testing discipline refined**: full-test trigger changed from "touched core infrastructure files" to "changed core infrastructure behavior" — adding a helper to memory.mjs no longer triggers the full suite
217
+
218
+ ### 0.7.5 (2026-07)
219
+ - **Compound prompt instructions split**: 8 compound sentences across SYSTEM_PROMPT / main-overlay / coder-overlay split into independent bullets (one attention node per instruction), improving instruction-following on DeepSeek/GLM/Qwen — fallback clauses like "add tests after changing code" no longer get skipped
220
+ - **Testing discipline strengthened**: SYSTEM_PROMPT Testing discipline gained an independent hard rule (changing behavior/adding code requires tests); main-overlay self-review checklist gained "do existing tests cover the change"; coder-overlay final checklist gained a test item
221
+ - **Plan mode workflow**: main-overlay's plan mode instruction split from one compound sentence into a 3-step numbered flow
222
+
223
+ ### 0.7.4 (2026-07)
224
+ - **verify tiered self-check**: default quick mode (syntax-check changed files + git diff + self-review checklist, milliseconds); `full=true` also runs the full npm test suite — no more waiting ten-plus seconds per line changed; quick satisfies the completion guard, use full when wrapping up or touching core infrastructure
225
+ - **Prompt discipline strengthened**: SYSTEM_PROMPT gained Testing discipline (when to run which tier) and Debugging strategy (diagnose before treating, one change at a time); coder/plan/main overlays gained self-review checklists (simplest solution, match project patterns, don't touch unrelated files)
226
+ - **Fix**: quick mode marked verification passed even when syntax checks failed, gutting the completion guard
227
+
228
+ ### 0.7.3 (2026-07)
229
+ - **Image paste**: new `read_image` tool paste images/videos from the clipboard, multimodal models directly understand screenshots, UI mockups, architecture diagrams (Win: `Alt+V` / Mac/Linux: `Ctrl+V`)
230
+ - **TUI paste hint**: with a multimodal model, the input box corner shows the OS-appropriate paste shortcut; hidden for text-only models
231
+
232
+ ### 0.7.2 (2026-07)
233
+ - **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
+ - **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
+ - **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
237
+ - **Session robustness**: corrupted files or disk errors during archive/switch no longer crash — silently abandoned
238
+
239
+ ### 0.7.1 (2026-07)
240
+ - **Context explosion fix (urgent)**: the startup dependency-outline injection is no longer unbounded — a multi-repo parent directory (thousands of indexed files) produced a 1.4M-char ≈ 350K-token outline, re-injected every turn, blowing up context within a few turns and tripping TPM limits. Now truncated to 6000 chars (with a pointer to `repo_outline` for focused queries) and injected only once per session
241
+ - **Compaction escape hatch**: when history was too short (≤13 messages) to slice a middle section, compaction never happened — one giant message (huge paste/oversized injection) could deadlock. Now a deterministic slimming path: oversized user/tool bodies are truncated to stubs, reasoning_content and tool_calls pairing untouched
242
+ - **docSync ReferenceError fix**: undeclared `failed`/`errors` made every doc index sync throw (two tests red)
243
+ - **apply_patch tool**: unified-diff multi-file atomic patching (any failed hunk → nothing written), permission preview shows the diff directly
244
+ - **checkpoint tool**: `list`/`create`/`rewind` snapshot abilities exposed to the model (previously only wired to TUI auto-snapshots + /rewind, so the model couldn't save itself); the bash destructive-git guard upgraded to per-segment detection (chained forms like `&&`/`;`/`|`/command substitution no longer slip through)
245
+ - **bash process-tree kill**: timeout/interrupt kills the whole tree (POSIX process groups / Windows taskkill /T) — no orphaned grandchildren
246
+ - **Subagent display contract**: only content/thinking tokens relay to the TUI scrolling area; internal tool calls no longer flood the screen
247
+ - **Path safety**: `resolveInCwd` prevents symlink escapes (realpath double-check); edit rejects empty old_string; single-file incremental indexing skips hidden directories and node_modules
248
+ - **Misc**: SQLite WAL + busy_timeout, single-transaction schema migrations, semantic version comparison for upgrades, MCP cmd.exe quote-doubling escape, gitmem skips commits when nothing changed
249
+
250
+ ### 0.7.0 (2026-07)
251
+ - **Deep model protocol adaptation**: reasoning_content echo differentiated per model (`reasoningEcho` spec field) — DeepSeek/Kimi must echo, GLM must not; reasoning_effort enum validation (`reasoningEffortEnum`); temperature range clamping (`tempRange`)
252
+ - **Qwen/MiniMax spec completion**: reasoning_effort enums (Qwen 3.8-max-preview), temperature ranges (Qwen [0,2), MiniMax [0,2]), MiniMax M3 thinking mode
253
+ - **grep context lines**: `before`/`after` params (grep -B/-A equivalents), matches marked with `:`, context with `-`, adjacent ranges in the same file merged and deduped
254
+ - **System prompt boundary rule**: never modify files outside the working directory; never use bash to bypass the read/write/edit directory confinement
255
+ - **question tool input box title**: fixed to ` Question `, question text goes to the conversation area (no longer crammed into the box title)
256
+
257
+ ### 0.5.0 (2026-07)
258
+ - **Codebase understanding**: `repo_outline` (dependency outline, auto-injected at startup), `code_search` (FTS5 + vectors + JSDoc), `doc_search` (chunked by ## headings), auto-incremental indexing on file writes
259
+ - **Model adaptation**: 5 built-in presets (DeepSeek/Kimi/GLM/Qwen/MiniMax), maxTokens maxed out, truncation resume, thinking-mode APIs auto-matched
260
+ - Session 5-slot archiving, `/session` switching, tool results shown after restore
261
+ - Subagent streaming output visible, final reports in the conversation area
262
+ - File tools confined to the working directory, permission preview above the input box
263
+ - write/edit auto-attach git diffs, edit error messages with better hints
264
+ - task auto-filters completed items, proactive reminder when all done
265
+ - Prompt guidance: "check official docs → save discrepancies to project memory"
254
266
 
255
267
  ### 0.4.0
256
- - 权限审批展示文件内容预览(write 内容、edit diffbash 命令)
257
- - todo 面板进度可视、状态栏 token 用量与上下文利用率
258
- - 项目指令双层合并(全局 + 项目 AGENTS.md
268
+ - Permission approval shows content previews (write content, edit diff, bash command)
269
+ - Todo panel progress visualization, status bar token usage and context utilization
270
+ - Two-layer project instructions merge (global + project AGENTS.md)
259
271
 
260
272
  ### 0.3.0
261
- - MCP 客户端(JSON-RPC + stdio,零依赖)
262
- - Skills 系统(`.thincoder/skills/*.md`)
263
- - Plan/Goal/Question 工具
264
- - 提示词外部化到 `.md` 文件、子 agent 角色 overlay
265
- - task 严格纪律(keep ONE in_progress)、完成守卫(改文件未 verify 拦截)
266
- - DeepSeek thinking 回传、system prompt 前缀缓存
267
- - checkpoint 存档点 + `/rewind` 回滚
273
+ - MCP client (JSON-RPC + stdio, zero-dependency)
274
+ - Skills system (`.thincoder/skills/*.md`)
275
+ - Plan/Goal/Question tools
276
+ - Prompts externalized to `.md` files, subagent role overlays
277
+ - Strict task discipline (keep ONE in_progress), completion guard (file changes blocked without verify)
278
+ - DeepSeek thinking echo, system prompt prefix caching
279
+ - checkpoint snapshots + `/rewind` rollback
268
280
 
269
281
  ### 0.2.0
270
- - multi-provider 配置(支持多端点切换)
271
- - 初始配置向导(方向键选模型、配 key
272
- - `/think` 思维模式开关与推理强度
273
- - `/model` 模型选择器
274
- - bash 流式输出实时透传
282
+ - multi-provider configuration (switch between endpoints)
283
+ - Initial setup wizard (arrow-key model picker, key entry)
284
+ - `/think` thinking mode toggle and reasoning effort
285
+ - `/model` model picker
286
+ - bash streaming output passthrough
275
287
 
276
288
  ### 0.1.0
277
- - Agent 主循环、14 个内置工具、零依赖 TUI
278
- - 三层记忆(personal/project/team)、FTS5 检索
279
- - 会话持久化、上下文压缩、流式 SSE
289
+ - Agent main loop, 14 builtin tools, zero-dependency TUI
290
+ - Three-layer memory (personal/project/team), FTS5 retrieval
291
+ - Session persistence, context compaction, streaming SSE
280
292
 
281
293
  ## License
282
294
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
package/src/agent.mjs CHANGED
@@ -909,7 +909,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
909
909
  agent._touchedFiles = []
910
910
  agent._verifyRetries = 0 // 修复-验证循环计数,每个新 run 从头开始
911
911
  const MAX_VERIFY_RETRIES = 3
912
- let completionGuardFired = false
912
+ let guardPushbacks = 0 // 完成守卫推回次数(最多推 2 次:第三次直接放行,避免无限循环)
913
+ let honestReminderInjected = false // verify 耗尽后注入了诚实提醒,下一轮直接放行
913
914
  const recentCallSigs = [] // 停滞检测:最近的工具调用签名(同一调用连续 3 次即提醒)
914
915
 
915
916
  for (let turn = 0; turn < maxTurns; turn++) {
@@ -966,9 +967,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
966
967
  if (!response.content) {
967
968
  throw new Error("LLM 返回了空回复(可能是思考耗尽或被截断)。可 /think effort 降低推理强度后重试")
968
969
  }
969
- // 完成守卫:本轮改过文件却没跑过 verify,推回去验证一次
970
- if (depth === 0 && agent._mutatedThisRun && !agent._verifiedThisRun && !completionGuardFired) {
971
- completionGuardFired = true
970
+ // 完成守卫:本轮改过文件却没跑过 verify,推回去验证。
971
+ // 可重武装但最多推 2 次——第三次直接放行(避免 agent 死活不调 verify 时无限循环)
972
+ if (depth === 0 && agent._mutatedThisRun && !agent._verifiedThisRun && guardPushbacks < 2) {
973
+ guardPushbacks++
972
974
  agent.history.push({ role: "assistant", content: response.content })
973
975
  agent.history.push({
974
976
  role: "user",
@@ -987,10 +989,20 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
987
989
  })
988
990
  continue
989
991
  }
990
- // 重试用尽:测试仍然失败,诚实收尾
992
+ // 重试用尽:测试仍然失败,注入诚实提醒后给模型最后一轮总结
991
993
  if (depth === 0 && agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
994
+ if (honestReminderInjected) {
995
+ // 已经注入过诚实提醒且模型又给了最终回答 → 放行返回
996
+ agent.history.push({ role: "assistant", content: response.content })
997
+ return response.content
998
+ }
999
+ honestReminderInjected = true
992
1000
  agent.history.push({ role: "assistant", content: response.content })
993
- return response.content
1001
+ agent.history.push({
1002
+ role: "user",
1003
+ content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted and tests are still failing. In your response to the user, you MUST state explicitly: (1) what tests are still failing, (2) what you tried, (3) what you believe the root cause is. Do not present this as complete — the user needs to know the work is unfinished.]`,
1004
+ })
1005
+ continue
994
1006
  }
995
1007
  agent.history.push({ role: "assistant", content: response.content })
996
1008
  return response.content
package/src/memory.mjs CHANGED
@@ -837,9 +837,9 @@ export async function gitSync(memory, dir, { onProgress } = {}) {
837
837
  // 取两个 diff 的并集:已提交的变更(pull/merge)+ 工作区脏文件(用户在外部编辑器改的)
838
838
  let diffOut
839
839
  try {
840
- // --diff-filter 只取增/改/重命名,不取删(删文件由 reindexFile 自己检测)
841
- const committed = execSync(`git diff --name-only --diff-filter=ACMRT ${stored} HEAD`, opts).trim()
842
- const dirty = execSync(`git diff --name-only --diff-filter=ACMRT`, opts).trim()
840
+ // 包含 D(删除):外部删文件也要清理索引块
841
+ const committed = execSync(`git diff --name-only --diff-filter=ACMRTD ${stored} HEAD`, opts).trim()
842
+ const dirty = execSync(`git diff --name-only --diff-filter=ACMRTD`, opts).trim()
843
843
  const lines = [...new Set([...committed.split("\n").filter(Boolean), ...dirty.split("\n").filter(Boolean)])]
844
844
  diffOut = lines
845
845
  } catch {
@@ -852,7 +852,8 @@ export async function gitSync(memory, dir, { onProgress } = {}) {
852
852
  return null
853
853
  }
854
854
 
855
- let updated = 0, removed = 0, skipped = 0
855
+ let updated = 0, removed = 0, skipped = 0, failed = 0
856
+ const errors = []
856
857
  for (let i = 0; i < diffOut.length; i++) {
857
858
  const rel = diffOut[i].replaceAll("\\", "/")
858
859
  const abs = join(dir, rel)
@@ -879,22 +880,31 @@ export async function gitSync(memory, dir, { onProgress } = {}) {
879
880
  }
880
881
  updated++
881
882
  } catch (e) {
882
- // 文件已被删清理索引
883
- if (CODE_EXTS.has(ext)) memory.db.prepare(`DELETE FROM code_chunks WHERE origin = ? AND path = ?`).run(dir, rel)
884
- else memory.db.prepare(`DELETE FROM doc_chunks WHERE origin = ? AND path = ?`).run(dir, rel)
885
- removed++
883
+ // readFile 失败 = 文件已删 清理索引块;
884
+ // 其他错误(权限/SQLite)不推进锚点——下次 gitSync 会重试
885
+ const isDeleted = e.code === "ENOENT"
886
+ if (isDeleted) {
887
+ if (CODE_EXTS.has(ext)) memory.db.prepare(`DELETE FROM code_chunks WHERE origin = ? AND path = ?`).run(dir, rel)
888
+ else memory.db.prepare(`DELETE FROM doc_chunks WHERE origin = ? AND path = ?`).run(dir, rel)
889
+ removed++
890
+ } else {
891
+ failed++
892
+ if (errors.length < 5) errors.push(`${rel}: ${e.message}`)
893
+ }
886
894
  }
887
895
  if (onProgress && i % 5 === 0) {
888
896
  onProgress({ phase: "index", current: i + 1, total: diffOut.length, updated, removed, skipped })
889
897
  }
890
898
  }
891
899
 
892
- // 更新锚点
893
- memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('last_indexed_commit', ?)
894
- ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(head)
900
+ // 更新锚点(有 failed 时不推进——下次 gitSync 会重试这些文件)
901
+ if (failed === 0) {
902
+ memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('last_indexed_commit', ?)
903
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(head)
904
+ }
895
905
 
896
- onProgress?.({ phase: "done", total: diffOut.length, updated, removed, skipped })
897
- return { updated, removed, skipped }
906
+ onProgress?.({ phase: "done", total: diffOut.length, updated, removed, skipped, failed })
907
+ return { updated, removed, skipped, failed, errors }
898
908
  }
899
909
 
900
910
  /**
@@ -969,9 +979,21 @@ export async function codeSync(memory, dir, { onProgress } = {}) {
969
979
  }
970
980
 
971
981
  onProgress?.({ phase: "done", total: files.length, updated, removed, skipped, failed })
982
+ // 全量同步成功后记录 git 锚点(下次启动 gitSync 用它做增量 diff 基准)
983
+ markIndexedCommit(memory, dir)
972
984
  return { updated, removed, skipped, failed, errors, total: files.length }
973
985
  }
974
986
 
987
+ /** 记录当前 HEAD 作为索引锚点(gitSync 增量 diff 基准);非 git 仓库静默跳过 */
988
+ async function markIndexedCommit(memory, dir) {
989
+ try {
990
+ const { execSync } = await import("node:child_process")
991
+ const head = execSync("git rev-parse HEAD", { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim()
992
+ memory.db.prepare(`INSERT INTO meta (key, value) VALUES ('last_indexed_commit', ?)
993
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(head)
994
+ } catch { /* 非 git 仓库或 git 不可用,跳过 */ }
995
+ }
996
+
975
997
  /**
976
998
  * 代码检索:FTS5(BM25) + 可选向量余弦,RRF 合并。
977
999
  * 无 embedder 时退化为纯 FTS;ftsQuery 为空(纯标点查询)且有 embedder 时退化为纯向量。
@@ -1203,6 +1225,7 @@ export async function docSync(memory, dir, { onProgress } = {}) {
1203
1225
  }
1204
1226
 
1205
1227
  onProgress?.({ phase: "done", total: files.length, updated, removed, skipped, failed })
1228
+ markIndexedCommit(memory, dir)
1206
1229
  return { updated, removed, skipped, failed, errors, total: files.length }
1207
1230
  }
1208
1231
 
package/src/tools/bash.md CHANGED
@@ -25,3 +25,4 @@ Notes:
25
25
  - Do NOT run destructive commands (rm -rf, force-push, drop table) without explicit user confirmation
26
26
  - After commands that change files (git checkout, npm install, etc.), repo_outline and code_search may be stale — re-run them to get current results.
27
27
  - Prefer read/glob/grep/ls for file operations inside the project — bash has no directory confinement.
28
+ - NEVER use bash to write or modify files (echo/sed/printf > file, cat << EOF, etc.). Use write/edit/insert_after/apply_patch instead — they handle encoding, escaping, and directory confinement correctly.
package/src/tools.mjs CHANGED
@@ -600,6 +600,11 @@ const bashTool = {
600
600
  try { child.kill("SIGKILL") } catch {} // 组杀失败时兜底杀本体
601
601
  }
602
602
  }
603
+ // Windows 中文系统默认代码页是 GBK (CP936),cmd.exe 重定向写文件时用 ANSI 代码页,
604
+ // chcp 65001 也改不了重定向的编码。bash 工具写含 CJK 的文件会产生 GBK——
605
+ // 提示词层已禁止用 bash 写文件(用 write/edit 工具替代),这里设 PYTHONIOENCODING
606
+ // 覆盖 Python 脚本的 stdout 编码(Python 是唯一可能正确响应环境变量的子进程)
607
+ const winCmd = process.platform === "win32"
603
608
  const child = spawn(args.command, {
604
609
  cwd: ctx.cwd,
605
610
  shell: true,
@@ -614,6 +619,7 @@ const bashTool = {
614
619
  GIT_PAGER: "cat",
615
620
  PAGER: "cat",
616
621
  TERM: "dumb",
622
+ ...(winCmd ? { PYTHONIOENCODING: "utf-8" } : {}),
617
623
  },
618
624
  })
619
625
  // stdout / stderr 各自独立解码(同进程通常同编码,但分开收集更干净,
package/src/tui.mjs CHANGED
@@ -279,6 +279,7 @@ export async function startTUI(agent, opts = {}) {
279
279
  currentTool: null, // 正在执行的工具名 (状态栏显示)
280
280
  processingStarted: 0, // 本轮处理开始时间 (状态栏计时)
281
281
  status: "Ready",
282
+ queue: [], // 处理中排队的待执行消息:[{ text }],处理完自动取下一条
282
283
  }
283
284
 
284
285
  // 恢复的会话如果所有任务completed,自动收起 todo 面板 (对齐运行时行为)
@@ -553,6 +554,13 @@ export async function startTUI(agent, opts = {}) {
553
554
  }
554
555
  }
555
556
 
557
+ // 队列预览 (暗色,紧挨输入框上方):与子 agent 面板/权限预览共享输入框上方空间
558
+ // 只在 processing 时显示(非 processing 时队列应为空),且最多 1 行预览避免挤压对话区
559
+ if (state.queue.length > 0 && state.processing) {
560
+ const preview = sliceByWidth(state.queue[0].text, W - 20)
561
+ out.push(`${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D del${ansi.reset}${ansi.clearLine}`)
562
+ }
563
+
556
564
  // 输入框 (全边框,宽 W)
557
565
  let borderColor = C.tool
558
566
  let title
@@ -663,7 +671,8 @@ export async function startTUI(agent, opts = {}) {
663
671
  ? ` │ ${ansi.reset}${C.warn}ctx ${ctxPct}%${ansi.reset}${ansi.dim}`
664
672
  : ` │ ctx ${ctxPct}%`
665
673
  : ""
666
- statusLine = ` ${statusText}${taskHint}${tokenHint}${ctxHint}${scrollHint} Enter: send/: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+C: exit`
674
+ const queueHint = state.queue.length > 0 ? `queue: ${state.queue.length}` : ""
675
+ statusLine = ` ${statusText}${taskHint}${tokenHint}${ctxHint}${queueHint}${scrollHint} │ Enter: send${state.processing ? " (queue)" : ""} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+C: exit`
667
676
  }
668
677
  const autoBanner = agent.autoApprove ? `${C.warn} AUTO${ansi.reset}${ansi.dim}│` : ""
669
678
  const planBanner = agent.planMode ? `${C.tool} PLAN${ansi.reset}${ansi.dim}│` : ""
@@ -679,8 +688,8 @@ export async function startTUI(agent, opts = {}) {
679
688
  process.stdout.write(frame)
680
689
  }
681
690
 
682
- // 光标:输入态定位到输入框内 (IME 候选框跟随真实光标);处理中/权限确认/菜单态时隐藏
683
- if (state.processing || state.permission || state.question || state.picker || state.wizard?.step === "provider") {
691
+ // 光标:输入态定位到输入框内 (IME 候选框跟随真实光标);权限确认/菜单态时隐藏
692
+ if (state.permission || state.question || state.picker || state.wizard?.step === "provider") {
684
693
  process.stdout.write(ansi.hideCursor)
685
694
  } else {
686
695
  const cursorRow = 1 + convH + pickerH + taskPanelH + 2 + (layout.cursorLine - inputOffset) // header + 对话区 + todo 面板 + 上边框 + 行偏移
@@ -695,20 +704,43 @@ export async function startTUI(agent, opts = {}) {
695
704
 
696
705
  async function submit() {
697
706
  const text = state.input.join("").trim()
698
- if (!text || state.processing) return
707
+ if (!text) return
699
708
  state.input = []
700
709
  state.cursor = 0
701
710
  state.history.push(text)
702
711
  state.historyIndex = -1
703
712
  state.scroll = 0
704
713
 
705
- // 斜杠Commands:本地处理,不进入 agent
714
+ // 斜杠Commands:本地处理,不进入 agent(处理中也允许执行部分命令如 /cancel)
706
715
  if (text.startsWith("/")) {
716
+ if (state.processing) {
717
+ // 处理中只允许取消当前任务,其他命令排队
718
+ if (text === "/cancel" || text === "/exit") {
719
+ await handleSlash(text)
720
+ } else {
721
+ state.queue.push({ text })
722
+ render()
723
+ }
724
+ return
725
+ }
707
726
  await handleSlash(text)
708
727
  return
709
728
  }
710
729
 
711
- pushLabel(`❯ You:`, ansi.bold + C.user)
730
+ // 处理中:入队等待,不立即执行
731
+ if (state.processing) {
732
+ state.queue.push({ text })
733
+ pushLabel(`❯ You: (queued #${state.queue.length})`, ansi.bold + C.user)
734
+ pushLine(text, C.dim)
735
+ render()
736
+ return
737
+ }
738
+
739
+ await runAgentTurn(text)
740
+ }
741
+
742
+ /** 执行一轮 agent 对话(从 submit 或队列取出调用) */
743
+ async function runAgentTurn(text) {
712
744
  pushLine(text, C.text)
713
745
 
714
746
  // 任务开始前自动打存档点 (git 仓库内;失败静默,不挡任务)
@@ -894,6 +926,24 @@ export async function startTUI(agent, opts = {}) {
894
926
  // 存失败不打断使用
895
927
  }
896
928
  render()
929
+
930
+ // 队列里有待执行消息:自动取下一条执行
931
+ if (state.queue.length > 0) {
932
+ const next = state.queue.shift()
933
+ // 队列里的斜杠命令直接执行
934
+ if (next.text.startsWith("/")) {
935
+ await handleSlash(next.text)
936
+ render()
937
+ // 斜杠命令执行完也继续检查队列
938
+ if (state.queue.length > 0 && !state.processing) {
939
+ const next2 = state.queue.shift()
940
+ await runAgentTurn(next2.text)
941
+ }
942
+ } else {
943
+ pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
944
+ await runAgentTurn(next.text)
945
+ }
946
+ }
897
947
  }
898
948
 
899
949
  function flushStream() {
@@ -1611,7 +1661,15 @@ export async function startTUI(agent, opts = {}) {
1611
1661
  await persistRaw((raw) => { raw.providers = agent.providers })
1612
1662
  pushLabel(`❯ Provider`, ansi.bold + C.tool)
1613
1663
  pushLine(`Added ${name} (${baseURL} / ${model})`, C.tool)
1614
- pushLine(`Next: /provider Set Key`, C.dim)
1664
+ // 直接接 key 输入,不让用户再绕一圈
1665
+ askQuestion(`Enter API key for ${name} (留空跳过,之后 /provider → Set Key):`).then(async (key) => {
1666
+ if (key) {
1667
+ await setProviderKey(name, key)
1668
+ pushLine(`Key saved for ${name}`, C.tool)
1669
+ } else {
1670
+ pushLine(`跳过 key。之后 /provider → Set API Key 配置`, C.dim)
1671
+ }
1672
+ })
1615
1673
  })
1616
1674
  return
1617
1675
  }
@@ -1630,7 +1688,10 @@ export async function startTUI(agent, opts = {}) {
1630
1688
  entries: keyEntries,
1631
1689
  onSelect: (se) => {
1632
1690
  askQuestion(`Enter API key for ${se.name}:`).then(async (key) => {
1633
- if (!key) return
1691
+ if (!key) {
1692
+ pushLine(`跳过 key 输入`, C.dim)
1693
+ return
1694
+ }
1634
1695
  await setProviderKey(se.name, key)
1635
1696
  })
1636
1697
  },
@@ -2092,7 +2153,16 @@ export async function startTUI(agent, opts = {}) {
2092
2153
  agent.config.activeProvider = item.provider
2093
2154
  pushLabel(`❯ Model`, ansi.bold + C.tool)
2094
2155
  pushLine(`Switched to ${item.provider} / ${item.model}${thresholdNote} (persisted)`, C.tool)
2095
- if (!agent.provider.apiKey) pushLine(`Provider has no key: /provider → Set Key`, C.warn)
2156
+ if (!agent.provider.apiKey) {
2157
+ pushLine(`Provider has no key`, C.warn)
2158
+ askQuestion(`Enter API key for ${item.provider} (留空跳过):`).then(async (key) => {
2159
+ if (key) {
2160
+ await setProviderKey(item.provider, key)
2161
+ } else {
2162
+ pushLine(`跳过。之后 /provider → Set API Key 配置`, C.dim)
2163
+ }
2164
+ })
2165
+ }
2096
2166
  }
2097
2167
 
2098
2168
  /** /distill:从current会话提取候选,逐条 y/n 确认后入库 */
@@ -2286,7 +2356,19 @@ export async function startTUI(agent, opts = {}) {
2286
2356
  return
2287
2357
  }
2288
2358
 
2289
- if (state.processing) return // 处理中锁定输入
2359
+ if (state.processing) {
2360
+ // 处理中允许输入(排队),但屏蔽方向键历史和 Tab 补全
2361
+ if (key.name === "tab" || key.name === "up" || key.name === "down") return
2362
+ // Ctrl+D:删除队列中最后一条
2363
+ if (key.ctrl && key.name === "d") {
2364
+ if (state.queue.length > 0) {
2365
+ state.queue.pop()
2366
+ render()
2367
+ }
2368
+ return
2369
+ }
2370
+ // 其余可打印字符正常进入输入框
2371
+ }
2290
2372
 
2291
2373
  // Tab:斜杠Commands补全 (循环候选);其余输入忽略 (\t 会顶破输入框,永不直接插入)
2292
2374
  if (key.name === "tab") {