thincoder 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ThinCoder contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # ThinCoder
2
+
3
+ **Sharp Code, Zero Bloat.**
4
+
5
+ **一个"薄"的 AI 编程 agent:纯 `.mjs`、无构建、零 npm 依赖、Node.js 原生。**
6
+
7
+ ThinCoder 的 "Thin" 不是"功能单薄",而是**思维锐利、直击要害**——像刀刃。
8
+ 在 AI agent 都在卷"全能"的今天,ThinCoder 打的是反面那张牌:**克制、精准、不废话**。
9
+ 它的人设是一个"话不多、一刀见血"的极客工程师:你给它复杂需求,它还你干净实现。
10
+
11
+ 设计哲学(也是这个名字的全部含义):只用 Node 标准库能实现的功能,就绝不引入依赖。整个项目的 `node_modules` 是空的。
12
+
13
+ ## 特性
14
+
15
+ - **Agent 主循环**:LLM ↔ 工具调用循环,直到任务完成(上限 50 轮防失控)
16
+ - **工具集**:`read` / `write` / `edit` / `bash` / `glob` / `grep` / `websearch` / `ls` / `fetch`,全部零依赖实现
17
+ - **两段式工具调度**:权限确认串行(一个一个问),只读工具并行执行,有副作用工具串行
18
+ - **会话持久化**:退出自动保存,启动自动恢复(按项目目录隔离),`/new` 开始新会话
19
+ - **子 agent 并发**:`subagent` 工具派发独立子任务(隔离上下文,只带回报告),一批多个走并行通道;普通模式子 agent 只读,AUTO 模式全放行
20
+ - **AUTO 模式**:`/auto` 或 `chat --auto` 完全授权,长任务免确认,状态栏黄色 AUTO 标识
21
+ - **任务跟踪**:`task` 工具让 agent 拆解多步任务并跟踪进度(pending/in_progress/done),TUI 状态栏实时显示 ▶n/m
22
+ - **流式 TUI**:裸 ANSI 实现(无 UI 库),对话流 / 流式输出 / 权限确认 / 翻页 / 输入历史 / 斜杠命令
23
+ - **上下文压缩**:对话超阈值时自动摘要(保留最早 2 条 + 最近 10 条,中间 LLM 摘要)
24
+ - **三层记忆 + 团队共享**(见下)
25
+ - **LLM 调用**:原生 `fetch` 直连 OpenAI 兼容协议(OpenAI / DeepSeek / Moonshot / Ollama),流式 SSE,指数退避重试,支持 `reasoning_content` 思考流
26
+
27
+ ## 记忆系统:一人学到,全队皆知
28
+
29
+ 三层记忆,全部"有就查、没有就跳过",统一混合检索:
30
+
31
+ | 层 | 位置 | 同步方式 |
32
+ |---|---|---|
33
+ | **Personal** | `~/.thincoder/memory.db`(sqlite) | 不同步,私有 |
34
+ | **Project** | 项目仓库 `.thincoder/memory/*.md` | 随项目 git(ThinCoder **只写文件,绝不替你 commit**) |
35
+ | **Team**(可选) | 独立记忆仓库,clone 到 `~/.thincoder/teams/<name>/` | `thincoder sync`(pull --rebase);写入时自动 commit + push(专用设施,可选启用) |
36
+
37
+ - **混合检索**:FTS5(BM25,中文逐字索引,双字词可命中)+ embedding 向量(暴力余弦)+ RRF(k=60) 融合排序
38
+ - **embedding**:OpenAI 兼容 `/v1/embeddings`,默认 SiliconFlow `BAAI/bge-m3`(免费额度,中文好);Ollama 本地可作离线选项。向量惰性生成——写入不算,首次搜索时补算落库
39
+ - **条目格式**:Markdown + frontmatter(type/title/tags/author/created),GitHub 上直接可读可 review;每条目一个文件,天然规避合并冲突;真冲突时诚实报错,绝不自动合并
40
+ - **双轨沉淀**:规范靠手动写(`memory_put`),经验靠 `/distill` 从会话提取——**LLM 出候选、逐条人工 y/n 确认**才入库,绝不做全自动沉淀
41
+ - **检索隔离**:Project 层按项目路径隔离,A 项目的记忆不会漏进 B 项目
42
+
43
+ ## 要求
44
+
45
+ - Node.js >= 22(记忆功能用到 `node:sqlite`;推荐 24)
46
+ - 一个 OpenAI 兼容端点的 API key
47
+ - 可选:embedding 服务的 key(不配置则退化为纯 FTS 检索)
48
+
49
+ ## 快速开始
50
+
51
+ ```bash
52
+ # 配置(首次)
53
+ mkdir -p ~/.thincoder
54
+ cat > ~/.thincoder/config.json <<'EOF'
55
+ {
56
+ "provider": {
57
+ "baseURL": "https://api.deepseek.com/v1",
58
+ "apiKey": "sk-...",
59
+ "model": "deepseek-chat"
60
+ }
61
+ }
62
+ EOF
63
+
64
+ # 启动 TUI(默认命令)
65
+ node bin/thincoder.mjs
66
+
67
+ # 一次性问答(管道友好)
68
+ node bin/thincoder.mjs chat "读一下 package.json 并总结"
69
+
70
+ # 记忆管理
71
+ node bin/thincoder.mjs memory put --type=rule --title="代码规范" --content="不加分号"
72
+ node bin/thincoder.mjs memory search "代码规范"
73
+ node bin/thincoder.mjs memory list
74
+ node bin/thincoder.mjs memory remove 1
75
+
76
+ # 团队记忆(可选,配置 memory.team 后可用)
77
+ node bin/thincoder.mjs sync # 拉取团队仓库并重建索引
78
+
79
+ # 从会话记录提取知识(逐条确认后入库)
80
+ node bin/thincoder.mjs distill session.txt
81
+ ```
82
+
83
+ TUI 内斜杠命令:`/help`、`/model`(查看/切换模型)、`/config`(查看配置)、`/distill`(从当前会话提取知识)、`/clear`、`/exit`。输入 `/` 时状态栏实时提示匹配命令。
84
+
85
+ 环境变量:`THINCODER_API_KEY`(或 `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`)、`THINCODER_BASE_URL`、`THINCODER_MODEL`、`SILICONFLOW_API_KEY`。
86
+
87
+ ## 配置
88
+
89
+ `~/.thincoder/config.json`:
90
+
91
+ ```jsonc
92
+ {
93
+ "provider": {
94
+ "baseURL": "https://api.deepseek.com/v1", // 任意 OpenAI 兼容端点
95
+ "apiKey": "sk-...", // 或留空走环境变量
96
+ "model": "deepseek-chat"
97
+ },
98
+ "embedding": { // 可选:不配则纯 FTS 检索
99
+ "baseURL": "https://api.siliconflow.cn/v1",
100
+ "apiKey": "sk-...", // 或 SILICONFLOW_API_KEY
101
+ "model": "BAAI/bge-m3"
102
+ },
103
+ "agent": {
104
+ "maxTurns": 50, // 工具循环上限
105
+ "compactThreshold": 100000 // 上下文压缩阈值(约 token 数)
106
+ },
107
+ "memory": {
108
+ "dbPath": "~/.thincoder/memory.db", // sqlite 索引库路径
109
+ "projectDir": ".thincoder/memory", // Project 层目录(相对项目根)
110
+ "team": { // 可选:不配则 Team 层禁用
111
+ "name": "myteam",
112
+ "repo": "git@github.com:org/team-memory.git"
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ ## 架构
119
+
120
+ ```
121
+ bin/thincoder.mjs 命令入口(tui / chat / memory / sync / distill)
122
+ src/
123
+ provider.mjs LLM 调用(fetch, SSE 流式, 重试)
124
+ embedding.mjs 向量嵌入(OpenAI 兼容 /v1/embeddings)
125
+ tools.mjs 9 个内置工具 + readonly 调度标记
126
+ agent.mjs 主循环 + 两段式工具执行 + 记忆注入
127
+ context.mjs token 粗估 + 历史压缩
128
+ memory.mjs 记忆核心:三层合并检索(FTS5 + 向量 + RRF)
129
+ markdown.mjs 条目格式(frontmatter 解析/序列化)
130
+ gitmem.mjs Team 层 git 同步(clone/pull --rebase/push,系统 git)
131
+ distill.mjs 会话知识提取(候选 + 人工确认)
132
+ config.mjs 配置加载
133
+ tui.mjs 裸 ANSI 终端 UI(宽字符折行、滚动、权限确认、斜杠命令)
134
+ test/ node:test 离线单测(npm test)
135
+ scripts/ 真实环境验证脚本(压缩、团队同步)
136
+ ```
137
+
138
+ 关键设计:
139
+
140
+ - **工具执行两段式**:阶段一串行做权限确认(有副作用工具逐个问用户);阶段二只读工具 `Promise.all` 并行、有副作用工具串行。结果按 `toolCallId` 配对回喂
141
+ - **权限在 UI 层**:工具只负责执行,"问不问用户"是 TUI/CLI 的事,headless 场景不用改工具
142
+ - **索引是易失品**:sqlite 只是 markdown 真相源的本地索引,`reindex` 随时可重建;git 仓库才是团队记忆的真相源
143
+ - **git 边界**:Project 层只写文件不碰用户的仓库;Team 层是 ThinCoder 自管仓库才可自动 commit+push
144
+ - **中文检索**:FTS5 unicode61 + 写入/查询两侧 CJK 逐字加空格;语义匹配走向量通道
145
+
146
+ ## 开发
147
+
148
+ ```bash
149
+ npm test # 离线单测(node:test,含本地 mock 服务)
150
+ node scripts/verify-compress.mjs # 上下文压缩的真实 API 验证(需要有效配置)
151
+ node scripts/verify-team.mjs # 团队记忆 A->git->B 全链路验证(本地 git,离线)
152
+ ```
153
+
154
+ 代码约定:纯 `.mjs`,不加分号,禁止引入 npm 依赖(包括开发依赖)。
155
+
156
+ ## 路线图
157
+
158
+ - v3 候选:checkpoint 断点恢复、子 agent 并行、MCP 接入
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * thincoder — 命令入口
5
+ * thincoder 启动 TUI
6
+ * thincoder chat "..." 一次性 agent 问答(可调用工具,流式输出)
7
+ * thincoder memory <sub> 记忆管理:list / search / put / remove
8
+ * thincoder --help 显示帮助
9
+ */
10
+
11
+ import { readFileSync } from "node:fs"
12
+ import { createInterface } from "node:readline"
13
+ import { execSync } from "node:child_process"
14
+ import { join } from "node:path"
15
+ import { createAgent, runAgent } from "../src/agent.mjs"
16
+ import { loadConfig, configDir } from "../src/config.mjs"
17
+ import { createMemory, memoryTools, put, remove, search, list, syncDir } from "../src/memory.mjs"
18
+ import { createProvider } from "../src/provider.mjs"
19
+ import { builtinTools } from "../src/tools.mjs"
20
+
21
+ const [command, ...args] = process.argv.slice(2)
22
+
23
+ const USAGE = `thincoder - thin coding agent
24
+
25
+ Usage:
26
+ thincoder Launch the interactive TUI
27
+ thincoder chat <prompt> One-shot agent run (tools enabled), streams reply to stdout
28
+ thincoder memory list [--type=<t>] List memory entries
29
+ thincoder memory search <query> Search memory
30
+ thincoder memory put --type=<t> --title=<t> --content=<c> [--tags=<t>]
31
+ thincoder memory remove <id> Remove an entry
32
+ thincoder sync Sync team memory repo (pull --rebase + reindex)
33
+ thincoder reindex Rebuild the local index from markdown sources
34
+ thincoder distill <file> [--yes] [--scope=<s>]
35
+ Extract knowledge candidates from a session
36
+ transcript file; confirm each before saving
37
+ thincoder upgrade Update to the latest version from npm
38
+
39
+ Config: ~/.thincoder/config.json (provider.baseURL / provider.apiKey / provider.model)
40
+ Env: THINCODER_API_KEY, THINCODER_BASE_URL, THINCODER_MODEL
41
+ `
42
+
43
+ /** 组装一个带记忆的 agent(同步各层索引后返回) */
44
+ async function makeAgent() {
45
+ const config = loadConfig()
46
+ const provider = createProvider(config.provider)
47
+ const memory = createMemory({ dbPath: config.memory.dbPath })
48
+ // 向量检索:配了 embedding 就启用(惰性生成向量,首次搜索时补算)
49
+ if (config.embedding?.apiKey) {
50
+ const { createEmbedder } = await import("../src/embedding.mjs")
51
+ memory.embedder = createEmbedder(config.embedding)
52
+ }
53
+ const cwd = process.cwd()
54
+ // Project 层:启动时同步 .thincoder/memory/ 目录到索引(有就同步,没有就跳过)
55
+ if (config.memory.projectDir) {
56
+ memory.projectOrigin = join(cwd, config.memory.projectDir)
57
+ await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
58
+ }
59
+ // Team 层(可选):首次自动 clone;启动只索引本地目录,拉取远端走显式 thincoder sync
60
+ const team = teamConfig(config)
61
+ if (team) {
62
+ const { ensureClone } = await import("../src/gitmem.mjs")
63
+ await ensureClone(team)
64
+ await syncDir(memory, { layer: "team", dir: team.dir })
65
+ }
66
+ return createAgent({
67
+ provider,
68
+ tools: [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team })],
69
+ config,
70
+ cwd,
71
+ memory,
72
+ })
73
+ }
74
+
75
+ /** 读取 team 配置并补全默认目录;未配置返回 null */
76
+ function teamConfig(config) {
77
+ const team = config.memory?.team
78
+ if (!team?.repo) return null
79
+ const name = team.name ?? "default"
80
+ return { name, repo: team.repo, dir: team.dir ?? join(configDir, "teams", name) }
81
+ }
82
+
83
+ /** 条目作者:git config user.name 兜底 unknown */
84
+ function gitAuthor() {
85
+ try {
86
+ return execSync("git config user.name", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "unknown"
87
+ } catch {
88
+ return "unknown"
89
+ }
90
+ }
91
+
92
+ switch (command) {
93
+ case "chat": {
94
+ const auto = args.includes("--auto")
95
+ const prompt = args.filter((a) => a !== "--auto").join(" ").trim()
96
+ if (!prompt) {
97
+ console.error('Usage: thincoder chat [--auto] "<prompt>"')
98
+ process.exit(1)
99
+ }
100
+
101
+ const agent = await makeAgent()
102
+ if (auto) agent.autoApprove = true
103
+ try {
104
+ await runAgent(agent, prompt, {
105
+ onToken: (text) => process.stdout.write(text),
106
+ onToolCall: (name, toolArgs) => {
107
+ console.error(`\n[tool] ${name} ${summarize(toolArgs)}`)
108
+ },
109
+ onToolResult: (name, result) => {
110
+ const preview = result.length > 200 ? result.slice(0, 200) + "..." : result
111
+ console.error(`[done] ${name} -> ${preview.split("\n")[0]}`)
112
+ },
113
+ onToolOutput: (name, chunk) => process.stderr.write(chunk),
114
+ onPermissionRequest: (name, toolArgs) => (agent.autoApprove ? true : askPermission(name, toolArgs)),
115
+ })
116
+ process.stdout.write("\n")
117
+ } catch (error) {
118
+ console.error(`\n[error] ${error.message}`)
119
+ process.exit(1)
120
+ }
121
+ break
122
+ }
123
+
124
+ case "memory": {
125
+ const config = loadConfig()
126
+ const memory = createMemory({ dbPath: config.memory.dbPath })
127
+ if (config.embedding?.apiKey) {
128
+ const { createEmbedder } = await import("../src/embedding.mjs")
129
+ memory.embedder = createEmbedder(config.embedding)
130
+ }
131
+ if (config.memory.projectDir) {
132
+ memory.projectOrigin = join(process.cwd(), config.memory.projectDir)
133
+ }
134
+ await memoryCommand(memory, args)
135
+ break
136
+ }
137
+
138
+ case "sync": {
139
+ const config = loadConfig()
140
+ const team = teamConfig(config)
141
+ if (!team) {
142
+ console.error("Team memory not configured. Set memory.team in ~/.thincoder/config.json:")
143
+ console.error(' "team": { "name": "myteam", "repo": "git@github.com:org/team-memory.git" }')
144
+ process.exit(1)
145
+ }
146
+ const memory = createMemory({ dbPath: config.memory.dbPath })
147
+ const { ensureClone, pullTeam } = await import("../src/gitmem.mjs")
148
+ try {
149
+ const cloned = await ensureClone(team)
150
+ if (cloned) console.log(`Cloned team repo to ${team.dir}`)
151
+ await pullTeam(team.dir)
152
+ const stats = await syncDir(memory, { layer: "team", dir: team.dir })
153
+ console.log(`Synced. Index: +${stats.added} ~${stats.updated} -${stats.removed}`)
154
+ } catch (error) {
155
+ console.error(`[error] ${error.message}`)
156
+ process.exit(1)
157
+ }
158
+ break
159
+ }
160
+
161
+ case "distill": {
162
+ const flags = {}
163
+ const positional = []
164
+ for (const a of args) {
165
+ const m = a.match(/^--([\w-]+)(?:=(.*))?$/)
166
+ if (m) flags[m[1]] = m[2] ?? true
167
+ else positional.push(a)
168
+ }
169
+ const file = positional[0]
170
+ if (!file) {
171
+ console.error("Usage: thincoder distill <transcript-file> [--yes] [--scope=personal|project|team]")
172
+ process.exit(1)
173
+ }
174
+ const { readFile } = await import("node:fs/promises")
175
+ const transcript = await readFile(file, "utf8")
176
+
177
+ const config = loadConfig()
178
+ const provider = createProvider(config.provider)
179
+ const memory = createMemory({ dbPath: config.memory.dbPath })
180
+ const team = teamConfig(config)
181
+ const { extractCandidates, saveCandidate } = await import("../src/distill.mjs")
182
+
183
+ console.error("[distill] extracting candidates...")
184
+ const candidates = await extractCandidates(provider, transcript)
185
+ if (candidates.length === 0) {
186
+ console.log("No distillable knowledge found in this session.")
187
+ break
188
+ }
189
+
190
+ const opts = {
191
+ projectDir: config.memory.projectDir ? join(process.cwd(), config.memory.projectDir) : null,
192
+ team,
193
+ author: gitAuthor(),
194
+ }
195
+ let saved = 0
196
+ for (const c of candidates) {
197
+ if (flags.scope) c.scope = flags.scope
198
+ console.log(`\n--- candidate ---`)
199
+ console.log(`[${c.type}] ${c.title} (scope: ${c.scope})`)
200
+ console.log(c.content)
201
+ if (c.type === "rule") {
202
+ console.log("(rule 类知识通常建议手动撰写;确认提取吗?)")
203
+ }
204
+ const accept = flags.yes ? true : await askPermission("distill-save", { title: c.title })
205
+ if (!accept) {
206
+ console.log("skipped")
207
+ continue
208
+ }
209
+ const where = await saveCandidate(memory, c, opts)
210
+ console.log(`saved -> ${where}`)
211
+ saved++
212
+ }
213
+ console.log(`\nDistilled ${saved}/${candidates.length} entries.`)
214
+ break
215
+ }
216
+
217
+ case "reindex": {
218
+ const config = loadConfig()
219
+ const memory = createMemory({ dbPath: config.memory.dbPath })
220
+ // files 层(project/team 的 markdown 索引)全量重建;索引是易失品,真相在 markdown
221
+ memory.db.prepare(`DELETE FROM files`).run()
222
+ const cwd = process.cwd()
223
+ let total = { added: 0, removed: 0 }
224
+ if (config.memory.projectDir) {
225
+ const s = await syncDir(memory, { layer: "project", dir: join(cwd, config.memory.projectDir) })
226
+ total.added += s.added
227
+ }
228
+ const team = teamConfig(config)
229
+ if (team) {
230
+ const { ensureClone } = await import("../src/gitmem.mjs")
231
+ await ensureClone(team)
232
+ const s = await syncDir(memory, { layer: "team", dir: team.dir })
233
+ total.added += s.added
234
+ }
235
+ console.log(`Reindexed ${total.added} markdown entries (project${team ? " + team" : ""}). Vectors will be lazily regenerated on next search.`)
236
+ break
237
+ }
238
+
239
+ case "tui":
240
+ case undefined: {
241
+ const agent = await makeAgent()
242
+ const config = loadConfig()
243
+ // 恢复上次的会话(同一项目目录)
244
+ const { loadSession } = await import("../src/session.mjs")
245
+ const restored = loadSession(process.cwd())
246
+ if (restored) {
247
+ agent.history = restored.history
248
+ agent.tasks = restored.tasks ?? []
249
+ }
250
+ const { startTUI } = await import("../src/tui.mjs")
251
+ try {
252
+ await startTUI(agent, {
253
+ projectDir: config.memory.projectDir ? join(process.cwd(), config.memory.projectDir) : null,
254
+ team: teamConfig(config),
255
+ author: gitAuthor(),
256
+ restored,
257
+ })
258
+ } catch (error) {
259
+ console.error(`[error] ${error.message}`)
260
+ process.exit(1)
261
+ }
262
+ break
263
+ }
264
+
265
+ case "upgrade": {
266
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
267
+ const local = pkg.version
268
+ let remote
269
+ try {
270
+ remote = execSync("npm view thincoder version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()
271
+ } catch {
272
+ console.error("[upgrade] 无法查询 npm registry,请确认网络和 npm 已安装")
273
+ process.exit(1)
274
+ }
275
+ if (remote === local) {
276
+ console.log(`ThinCoder ${local} 已是最新。`)
277
+ } else {
278
+ console.log(`升级: ${local} → ${remote}`)
279
+ execSync("npm install -g thincoder@latest", { stdio: "inherit" })
280
+ console.log(`已升级到 ${remote}`)
281
+ }
282
+ break
283
+ }
284
+
285
+ case "--help":
286
+ case "-h": {
287
+ process.stdout.write(USAGE)
288
+ break
289
+ }
290
+
291
+ default: {
292
+ console.error(`Unknown command: ${command}\n`)
293
+ process.stdout.write(USAGE)
294
+ process.exit(1)
295
+ }
296
+ }
297
+
298
+ // ---------------------------------------------------------------- memory 子命令
299
+
300
+ async function memoryCommand(memory, args) {
301
+ const [sub, ...rest] = args
302
+
303
+ const flags = {}
304
+ const positional = []
305
+ for (const a of rest) {
306
+ const m = a.match(/^--([\w-]+)=(.*)$/)
307
+ if (m) flags[m[1]] = m[2]
308
+ else positional.push(a)
309
+ }
310
+
311
+ switch (sub) {
312
+ case "list": {
313
+ const entries = await list(memory, { type: flags.type })
314
+ printEntries(entries)
315
+ break
316
+ }
317
+ case "search": {
318
+ const query = positional.join(" ")
319
+ if (!query) {
320
+ console.error("Usage: thincoder memory search <query>")
321
+ process.exit(1)
322
+ }
323
+ printEntries(await search(memory, query, { limit: 10 }))
324
+ break
325
+ }
326
+ case "put": {
327
+ if (!flags.type || !flags.title || !flags.content) {
328
+ console.error("Usage: thincoder memory put --type=<rule|knowledge|decision|pattern> --title=<t> --content=<c> [--tags=<t>]")
329
+ process.exit(1)
330
+ }
331
+ const id = await put(memory, { type: flags.type, title: flags.title, content: flags.content, tags: flags.tags ?? "" })
332
+ console.log(`Saved (id=${id})`)
333
+ break
334
+ }
335
+ case "remove": {
336
+ const id = Number(positional[0])
337
+ if (!id) {
338
+ console.error("Usage: thincoder memory remove <id>")
339
+ process.exit(1)
340
+ }
341
+ console.log((await remove(memory, id)) ? `Removed #${id}` : `No entry #${id}`)
342
+ break
343
+ }
344
+ default:
345
+ console.error("Usage: thincoder memory <list|search|put|remove>")
346
+ process.exit(1)
347
+ }
348
+ }
349
+
350
+ function printEntries(entries) {
351
+ if (entries.length === 0) {
352
+ console.log("(no entries)")
353
+ return
354
+ }
355
+ for (const e of entries) {
356
+ console.log(`#${e.id} [${e.type}] ${e.title}${e.tags ? ` (${e.tags})` : ""}`)
357
+ console.log(` ${e.content.split("\n")[0].slice(0, 100)}`)
358
+ }
359
+ }
360
+
361
+ // ---------------------------------------------------------------- 工具函数
362
+
363
+ function summarize(toolArgs) {
364
+ const s = JSON.stringify(toolArgs)
365
+ return s.length > 120 ? s.slice(0, 120) + "..." : s
366
+ }
367
+
368
+ /** 权限确认:TTY 下交互询问 y/n;非交互环境默认拒绝(安全优先) */
369
+ async function askPermission(name, toolArgs) {
370
+ if (!process.stdin.isTTY) {
371
+ console.error(`\n[deny] ${name} (non-interactive, side-effect tools require a TTY)`)
372
+ return false
373
+ }
374
+ const rl = createInterface({ input: process.stdin, output: process.stderr })
375
+ try {
376
+ const answer = await new Promise((resolve) => {
377
+ rl.question(`\n[allow?] ${name} ${summarize(toolArgs)} (y/N) `, resolve)
378
+ })
379
+ return answer.trim().toLowerCase() === "y"
380
+ } finally {
381
+ rl.close()
382
+ }
383
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "thincoder",
3
+ "version": "0.1.0",
4
+ "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
+ "keywords": ["ai", "agent", "coding", "cli", "llm", "deepseek", "openai", "tui"],
6
+ "type": "module",
7
+ "bin": {
8
+ "thincoder": "./bin/thincoder.mjs"
9
+ },
10
+ "engines": {
11
+ "node": ">=22"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "src/",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://gitee.com/shanghai-xinbo/thincoder.git"
22
+ },
23
+ "scripts": {
24
+ "test": "node --test",
25
+ "prepublishOnly": "node --test"
26
+ },
27
+ "author": "liwei <liwei@51marine.com> (上海新舶)",
28
+ "license": "MIT"
29
+ }