thincoder 0.7.8 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +32 -13
  2. package/bin/thincoder.js +4 -0
  3. package/bin/thincoder.mjs +27 -346
  4. package/package.json +2 -2
  5. package/src/agent/dispatch.mjs +98 -0
  6. package/src/agent/helpers.mjs +185 -0
  7. package/src/agent/setup.mjs +117 -0
  8. package/src/agent-tools/goal.mjs +71 -0
  9. package/src/agent-tools/plan.mjs +31 -0
  10. package/src/agent-tools/recent-changes.mjs +23 -0
  11. package/src/agent-tools/skill.mjs +46 -0
  12. package/src/agent-tools/subagent.mjs +113 -0
  13. package/src/agent-tools/task.mjs +67 -0
  14. package/src/agent-tools/verify.mjs +198 -0
  15. package/src/agent-tools.mjs +12 -0
  16. package/src/agent.mjs +90 -1040
  17. package/src/cli/distill-command.mjs +85 -0
  18. package/src/cli/make-agent.mjs +85 -0
  19. package/src/cli/memory-command.mjs +63 -0
  20. package/src/cli/permission.mjs +41 -0
  21. package/src/cli/setup-wizard.mjs +70 -0
  22. package/src/config.mjs +5 -8
  23. package/src/context.mjs +10 -13
  24. package/src/distill.mjs +4 -3
  25. package/src/embedding.mjs +4 -2
  26. package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
  27. package/src/mcp/helpers.mjs +37 -0
  28. package/src/mcp/transport-http.mjs +176 -0
  29. package/src/mcp/transport-stdio.mjs +84 -0
  30. package/src/mcp/transport-ws.mjs +87 -0
  31. package/src/mcp.mjs +4 -428
  32. package/src/memory/code-index.mjs +211 -0
  33. package/src/memory/code-sync.mjs +306 -0
  34. package/src/memory/core.mjs +277 -0
  35. package/src/memory/docs.mjs +262 -0
  36. package/src/memory/schema.mjs +426 -0
  37. package/src/memory.mjs +12 -1403
  38. package/src/provider/core.mjs +239 -0
  39. package/src/provider/index.mjs +6 -0
  40. package/src/provider/rate.mjs +104 -0
  41. package/src/session.mjs +18 -5
  42. package/src/tools/bash.mjs +144 -0
  43. package/src/tools/file.mjs +205 -0
  44. package/src/tools/git.mjs +166 -0
  45. package/src/tools/glob.mjs +51 -0
  46. package/src/tools/grep.mjs +100 -0
  47. package/src/tools/index.mjs +22 -0
  48. package/src/tools/ls.mjs +36 -0
  49. package/src/tools/patch.mjs +226 -0
  50. package/src/tools/repomap-parse.mjs +168 -0
  51. package/src/tools/shared.mjs +257 -0
  52. package/src/tools/system.mjs +336 -0
  53. package/src/tools/web.mjs +121 -0
  54. package/src/tools.mjs +2 -1194
  55. package/src/tui/agent-turn.mjs +254 -0
  56. package/src/tui/ansi.mjs +32 -0
  57. package/src/tui/clipboard.mjs +48 -0
  58. package/src/tui/cmd-auto.mjs +21 -0
  59. package/src/tui/cmd-clear.mjs +26 -0
  60. package/src/tui/cmd-config.mjs +72 -0
  61. package/src/tui/cmd-exit.mjs +5 -0
  62. package/src/tui/cmd-extract.mjs +5 -0
  63. package/src/tui/cmd-goal.mjs +47 -0
  64. package/src/tui/cmd-help.mjs +25 -0
  65. package/src/tui/cmd-init.mjs +91 -0
  66. package/src/tui/cmd-mcp.mjs +146 -0
  67. package/src/tui/cmd-model.mjs +7 -0
  68. package/src/tui/cmd-new.mjs +18 -0
  69. package/src/tui/cmd-plan.mjs +21 -0
  70. package/src/tui/cmd-reindex.mjs +44 -0
  71. package/src/tui/cmd-restore.mjs +39 -0
  72. package/src/tui/cmd-session.mjs +42 -0
  73. package/src/tui/cmd-skills.mjs +17 -0
  74. package/src/tui/cmd-think.mjs +56 -0
  75. package/src/tui/config-helpers.mjs +34 -0
  76. package/src/tui/distill-cmd.mjs +45 -0
  77. package/src/tui/index.mjs +330 -0
  78. package/src/tui/interaction.mjs +79 -0
  79. package/src/tui/key-handler.mjs +267 -0
  80. package/src/tui/layout.mjs +115 -0
  81. package/src/tui/pickers.mjs +279 -0
  82. package/src/tui/render-frame.mjs +304 -0
  83. package/src/tui/render.mjs +205 -0
  84. package/src/tui/slash-commands.mjs +138 -0
  85. package/src/tui/startup.mjs +113 -0
  86. package/src/tui/wizard.mjs +168 -0
  87. package/src/tui-render.mjs +4 -0
  88. package/src/tui.mjs +3 -2566
  89. package/src/provider.mjs +0 -383
  90. /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
  91. /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
  92. /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
  93. /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
  94. /package/src/{main-overlay.md → prompts/main.md} +0 -0
  95. /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
  96. /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
  97. /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
@@ -0,0 +1,254 @@
1
+ import { runAgent, ContinueError } from "../agent.mjs"
2
+ import { saveSession } from "../session.mjs"
3
+ import { sliceByWidth } from "./render.mjs"
4
+ import { ansi, C } from "./ansi.mjs"
5
+
6
+ /** 执行一轮 agent 对话(从 submit 或队列取出调用)。
7
+ * 从 index.mjs 抽出:agent 循环 + 回调构建 + 错误处理 + 队列处理。
8
+ * ctx: { agent, state, pushLine, pushLabel, render, scheduleRender,
9
+ * ensureAssistantLabel, askPermission, askQuestion,
10
+ * handleSlash, summarize } */
11
+ export async function runAgentTurn(ctx, text) {
12
+ const { agent, state, pushLine, pushLabel, render, scheduleRender, ensureAssistantLabel, askPermission, askQuestion, handleSlash, summarize } = ctx
13
+ pushLabel(`❯ You:`, ansi.bold + C.user)
14
+ pushLine(text, C.text)
15
+
16
+ // 任务开始前自动打存档点 (git 仓库内;失败静默,不挡任务)
17
+ try {
18
+ const { createCheckpoint } = await import("../git/checkpoint.mjs")
19
+ await createCheckpoint(agent.cwd)
20
+ } catch {
21
+ // 存档失败不影响任务
22
+ }
23
+
24
+ ctx.assistantLabeled = false
25
+ state.processing = true
26
+ state.status = "Processing..."
27
+ state.streaming = ""
28
+ state.reasoning = ""
29
+ state.subTasks = {}
30
+ state.currentTool = null
31
+ state.processingStarted = Date.now()
32
+ state.controller = new AbortController()
33
+ // 处理中每秒刷新一次状态栏 (运行计时)
34
+ const ticker = setInterval(() => {
35
+ if (state.processing) render()
36
+ }, 1000)
37
+ render()
38
+
39
+ const flushStream = () => {
40
+ if (state.reasoning) {
41
+ pushLine(state.reasoning, C.reason)
42
+ state.reasoning = ""
43
+ }
44
+ if (state.streaming) {
45
+ pushLine(state.streaming, C.text)
46
+ state.streaming = ""
47
+ }
48
+ }
49
+
50
+ const callbacks = {
51
+ onToken: (t) => {
52
+ // 子 agent 流式输出:前缀格式 role#id/ → 提取 id,更新对应 subTask 的流式文本
53
+ const subMatch = t.match(/^(\w+)#(\d+)\//)
54
+ if (subMatch) {
55
+ const key = `${subMatch[1]}#${subMatch[2]}`
56
+ const payload = t.slice(subMatch[0].length)
57
+ if (!state.subTasks[key]) {
58
+ state.subTasks[key] = { key, role: subMatch[1], text: "", tool: null, done: false, started: Date.now() }
59
+ }
60
+ state.subTasks[key].text += payload
61
+ if (state.subTasks[key].text.length > 2000) {
62
+ state.subTasks[key].text = state.subTasks[key].text.slice(-2000)
63
+ }
64
+ scheduleRender()
65
+ return
66
+ }
67
+ ensureAssistantLabel()
68
+ state.streaming += t
69
+ scheduleRender()
70
+ },
71
+ onReasoning: (t) => {
72
+ // 子 agent 的思考 token 同样带 role#id/ 前缀,进 subTasks 面板
73
+ const subMatch = t.match(/^(\w+)#(\d+)\//)
74
+ if (subMatch) {
75
+ const key = `${subMatch[1]}#${subMatch[2]}`
76
+ if (!state.subTasks[key]) {
77
+ state.subTasks[key] = { key, role: subMatch[1], text: "", tool: null, done: false, started: Date.now() }
78
+ }
79
+ scheduleRender()
80
+ return
81
+ }
82
+ ensureAssistantLabel()
83
+ state.reasoning += t
84
+ scheduleRender()
85
+ },
86
+ onToolCall: (name, args) => {
87
+ // 子 agent 工具调用:前缀 role#id/toolName → 更新对应 subTask 的当前工具
88
+ const subMatch = name.match(/^(\w+)#(\d+)\//)
89
+ if (subMatch) {
90
+ const key = `${subMatch[1]}#${subMatch[2]}`
91
+ const toolName = name.slice(subMatch[0].length)
92
+ if (!state.subTasks[key]) {
93
+ state.subTasks[key] = { key, role: subMatch[1], text: "", tool: null, done: false, started: Date.now() }
94
+ }
95
+ state.subTasks[key].tool = toolName
96
+ state.subTasks[key].toolArgs = args
97
+ state.subTasks[key].text = ""
98
+ scheduleRender()
99
+ return
100
+ }
101
+ flushStream()
102
+ ensureAssistantLabel()
103
+ state.currentTool = name
104
+ pushLine(` [tool] ${name} ${summarize(args)}`, C.tool)
105
+ },
106
+ onToolResult: (name, result) => {
107
+ state.currentTool = null
108
+ // 子 agent 结束:标记最早创建的 running subTask 为 done
109
+ const isSubagent = name === "subagent"
110
+ if (isSubagent) {
111
+ const running = Object.entries(state.subTasks)
112
+ .filter(([, s]) => !s.done)
113
+ .sort(([, a], [, b]) => a.started - b.started)
114
+ if (running.length > 0) {
115
+ const [key] = running[0]
116
+ state.subTasks[key].done = true
117
+ state.subTasks[key].tool = null
118
+ }
119
+ // 子 agent 报告摘要 (最多 8 行)直接展示在对话区
120
+ const lines = result.split("\n")
121
+ const preview = lines.slice(0, 8).map((l) => l.slice(0, 120)).join("\n")
122
+ if (preview) pushLine(preview, C.dim)
123
+ if (lines.length > 8) pushLine(` ... (${lines.length - 8} more lines)`, C.dim)
124
+ // 3 秒后清除面板中 done 的条目
125
+ setTimeout(() => {
126
+ for (const key of Object.keys(state.subTasks)) {
127
+ if (state.subTasks[key].done) delete state.subTasks[key]
128
+ }
129
+ if (state.processing) render()
130
+ }, 3000)
131
+ }
132
+ const stream = state.toolStreams[name]
133
+ if (stream) {
134
+ const tail = stream.trimEnd().slice(-4000)
135
+ if (tail) pushLine(tail, C.dim)
136
+ delete state.toolStreams[name]
137
+ }
138
+ if (!isSubagent) {
139
+ const first = result.split("\n")[0]
140
+ pushLine(` [done] ${name} → ${sliceByWidth(first, 100)}`, C.dim)
141
+ }
142
+ },
143
+ onToolOutput: (name, chunk) => {
144
+ state.toolStreams[name] = (state.toolStreams[name] ?? "") + chunk
145
+ scheduleRender()
146
+ },
147
+ onPermissionRequest: (name, args) => askPermission(name, args),
148
+ onQuestion: (text, options) => askQuestion(text, options),
149
+ onCompress: () => {
150
+ pushLine(" [context] Context too long, auto-compacted (early conversation summarized by LLM, task state preserved)", C.warn)
151
+ },
152
+ onUsage: (usage) => {
153
+ state.tokens.prompt += usage.prompt_tokens ?? 0
154
+ state.tokens.completion += usage.completion_tokens ?? 0
155
+ state.tokens.cacheHit += usage.prompt_cache_hit_tokens ?? 0
156
+ state.tokens.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
157
+ },
158
+ // 节流等待 (主动闸门 / 429 退避):状态栏明示,防用户以为卡死
159
+ onWait: ({ phase, seconds }) => {
160
+ state.status = phase === "gate" ? `TPM throttle wait ~${seconds}s` : `Rate-limited 429, retry in ${seconds}s`
161
+ render()
162
+ },
163
+ onTaskUpdate: (items) => {
164
+ state.tasks = items
165
+ const done = items.filter((i) => i.status === "done").length
166
+ // 留痕带上current任务标题:回看历史时知道进行到哪一项
167
+ const current = items.find((i) => i.status === "in_progress")
168
+ pushLine(` [task] ${done}/${items.length}${current ? ` ▶ ${current.title}` : ""}`, C.dim)
169
+ render()
170
+ },
171
+ // 增量保存:每 5 个工具 turn 落一次盘,中途崩溃丢失窗口从一整轮缩到几轮
172
+ onTurnEnd: (() => {
173
+ let n = 0
174
+ return () => {
175
+ if (++n % 5 !== 0) return
176
+ try { saveSession(agent, state.lines) } catch {}
177
+ }
178
+ })(),
179
+ }
180
+
181
+ for (let resume = false; ; resume = true) {
182
+ try {
183
+ await runAgent(agent, text, callbacks, { signal: state.controller.signal, resume })
184
+ flushStream()
185
+ break // 正常完成,退出循环
186
+ } catch (error) {
187
+ flushStream()
188
+ if (error.name === "AbortError" || state.controller?.signal.aborted) {
189
+ pushLine("[stopped]", C.warn)
190
+ break
191
+ }
192
+ if (error instanceof ContinueError) {
193
+ pushLabel(`❯ Continue`, ansi.bold + C.warn)
194
+ pushLine(`Ran ${error.turn} turns (limit ${error.turn}). Continue?`, C.warn)
195
+ // 暂停询问:复用 permission 机制
196
+ const willContinue = await new Promise((resolve) => {
197
+ state.permission = {
198
+ name: "continue",
199
+ args: { turns: error.turn },
200
+ resolve,
201
+ }
202
+ state.status = `Continue after ${error.turn} turns?`
203
+ render()
204
+ })
205
+ state.permission = null
206
+ if (!willContinue) {
207
+ pushLine("[continue cancelled]", C.warn)
208
+ break
209
+ }
210
+ pushLine("[continuing…]", C.tool)
211
+ // 重创新 AbortController:旧 signal 一旦 abort 过,resume 会立即失败 (防御性,current路径不可达但耦合紧)
212
+ state.controller = new AbortController()
213
+ continue
214
+ }
215
+ pushLine(`[error] ${error.message}`, C.error)
216
+ break
217
+ }
218
+ }
219
+
220
+ clearInterval(ticker)
221
+ state.processing = false
222
+ state.subTasks = {}
223
+ state.controller = null
224
+ state.status = "Ready"
225
+ // 全部完成时自动收起 todo 面板 (对齐 kimi-code TUI;agent.tasks 本身保留)
226
+ if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
227
+ state.tasks = []
228
+ }
229
+ // 每轮结束后保存会话 (崩溃也不丢)
230
+ try {
231
+ saveSession(agent, state.lines)
232
+ } catch {
233
+ // 存失败不打断使用
234
+ }
235
+ render()
236
+
237
+ // 队列里有待执行消息:自动取下一条执行
238
+ if (state.queue.length > 0) {
239
+ const next = state.queue.shift()
240
+ // 队列里的斜杠命令直接执行
241
+ if (next.text.startsWith("/")) {
242
+ await handleSlash(next.text)
243
+ render()
244
+ // 斜杠命令执行完也继续检查队列
245
+ if (state.queue.length > 0 && !state.processing) {
246
+ const next2 = state.queue.shift()
247
+ await runAgentTurn(ctx, next2.text)
248
+ }
249
+ } else {
250
+ pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
251
+ await runAgentTurn(ctx, next.text)
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * tui-ansi.mjs — ANSI 转义序列与终端颜色常量
3
+ * 零依赖,纯常量导出。
4
+ */
5
+
6
+ export const ESC = "\x1b"
7
+ export const ansi = {
8
+ hideCursor: `${ESC}[?25l`,
9
+ showCursor: `${ESC}[?25h`,
10
+ altBuffer: `${ESC}[?1049h`,
11
+ mainBuffer: `${ESC}[?1049l`,
12
+ mouseOn: `${ESC}[?1000h${ESC}[?1006h`,
13
+ mouseOff: `${ESC}[?1000l${ESC}[?1006l`,
14
+ home: `${ESC}[H`,
15
+ clearLine: `${ESC}[K`,
16
+ reset: `${ESC}[0m`,
17
+ dim: `${ESC}[2m`,
18
+ bold: `${ESC}[1m`,
19
+ fg: (n) => `${ESC}[${30 + n}m`,
20
+ gray: `${ESC}[90m`,
21
+ }
22
+
23
+ export const C = {
24
+ user: ansi.fg(4),
25
+ assistant: ansi.fg(2),
26
+ text: ansi.fg(7),
27
+ reason: `${ESC}[2m${ESC}[3m`,
28
+ tool: ansi.fg(6),
29
+ error: ansi.fg(1),
30
+ dim: ansi.gray,
31
+ warn: ansi.fg(3),
32
+ }
@@ -0,0 +1,48 @@
1
+ import { C } from "./ansi.mjs"
2
+
3
+ /** Ctrl+V / Alt+V:读取剪贴板图片 → 写入工作目录临时文件 → 输入框插入 read_image 命令。
4
+ * 从 index.mjs 抽出。
5
+ * ctx: { agent, state, pushLine, render } */
6
+ export async function pasteClipboardImage(ctx) {
7
+ const { agent, state, pushLine, render } = ctx
8
+ const { execFile } = await import("node:child_process")
9
+ const { mkdir, stat, unlink } = await import("node:fs/promises")
10
+ const { join } = await import("node:path")
11
+
12
+ const run = (cmd, args) => new Promise((resolve, reject) => {
13
+ execFile(cmd, args, { timeout: 10000 }, (err, stdout) => { if (err) reject(err); else resolve(stdout) })
14
+ })
15
+
16
+ const dest = join(agent.cwd, `.thincoder-paste-${Date.now()}.png`)
17
+ const isWin = process.platform === "win32"
18
+ const isMac = process.platform === "darwin"
19
+
20
+ try {
21
+ if (isWin) {
22
+ const psScript = `Add-Type -AssemblyName System.Windows.Forms; if ([System.Windows.Forms.Clipboard]::ContainsImage()) { [System.Windows.Forms.Clipboard]::GetImage().Save('${dest.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png); exit 0 } else { exit 1 }`
23
+ await run("powershell", ["-NoProfile", "-Command", psScript])
24
+ } else if (isMac) {
25
+ const script = `try; set f to (POSIX file "${dest}"); set img to the clipboard as «class PNGf»; set fd to open for access f with write permission; write img to fd; close access fd; end try`
26
+ await run("osascript", ["-e", script])
27
+ } else {
28
+ await run("bash", ["-c", `xclip -selection clipboard -t image/png -o > "${dest}" 2>/dev/null || { which wl-paste >/dev/null 2>&1 && wl-paste -t image/png > "${dest}" 2>/dev/null; } || exit 1`])
29
+ }
30
+ } catch {
31
+ pushLine("Clipboard does not contain an image, or clipboard access failed", C.dim)
32
+ try { await unlink(dest) } catch {}
33
+ return
34
+ }
35
+
36
+ const st = await stat(dest).catch(() => null)
37
+ if (!st || st.size === 0) {
38
+ pushLine("Clipboard does not contain an image, or clipboard access failed", C.dim)
39
+ try { await unlink(dest) } catch {}
40
+ return
41
+ }
42
+
43
+ const cmd = `read_image ${dest}`
44
+ state.input.splice(state.cursor, 0, ...[...cmd])
45
+ state.cursor += cmd.length
46
+ pushLine(`[image pasted → ${dest}]`, C.tool)
47
+ render()
48
+ }
@@ -0,0 +1,21 @@
1
+ import { ansi, C } from "./ansi.mjs"
2
+
3
+ /** /auto 命令:切换 auto-approve 模式。
4
+ * ctx: { agent, pushLine, pushLabel } */
5
+ export async function handleAutoCommand(ctx) {
6
+ const { agent, pushLine, pushLabel } = ctx
7
+ agent.autoApprove = !agent.autoApprove
8
+ agent._pendingReminders = agent._pendingReminders ?? []
9
+ if (agent.autoApprove) {
10
+ agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved — you may write, edit, and run commands without asking. Use this for long autonomous tasks. The user can still interrupt.]")
11
+ } else {
12
+ agent._pendingReminders.push("[System reminder: AUTO mode is now OFF. Destructive tool calls now require user approval again. Confirm before writing files, running commands, or spawning subagents.]")
13
+ }
14
+ pushLabel(`❯ Auto`, ansi.bold + (agent.autoApprove ? C.warn : C.tool))
15
+ pushLine(
16
+ agent.autoApprove
17
+ ? `AUTO ON: all tool calls (write/bash/subagent) auto-approved. For long tasks. /auto to disable.`
18
+ : `AUTO OFF: destructive tool calls require per-use approval again.`,
19
+ agent.autoApprove ? C.warn : C.dim,
20
+ )
21
+ }
@@ -0,0 +1,26 @@
1
+ /** /clear 命令:清屏(二次确认防误触)。
2
+ * ctx: { state, openPicker, render } */
3
+ export async function handleClearCommand(ctx) {
4
+ const { state, openPicker, render } = ctx
5
+ if (state.lines.length > 0) {
6
+ openPicker({
7
+ title: "Clear screen?",
8
+ entries: [
9
+ { type: "item", text: "Yes, clear all conversation output", action: "yes" },
10
+ { type: "item", text: "Cancel", action: "no" },
11
+ ],
12
+ defaultIndex: 1,
13
+ onSelect: (e) => {
14
+ if (e.action === "yes") {
15
+ state.lines = []
16
+ state.streaming = ""
17
+ render()
18
+ }
19
+ },
20
+ })
21
+ return
22
+ }
23
+ state.lines = []
24
+ state.streaming = ""
25
+ render()
26
+ }
@@ -0,0 +1,72 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { ansi, C } from "./ansi.mjs"
3
+
4
+ /** /config 命令:设置 embedding key / 高级 path=value 配置。
5
+ * 从 slash-commands.mjs 抽出。
6
+ * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey, ansi, C } */
7
+ export async function handleConfigCommand(ctx) {
8
+ const { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey } = ctx
9
+ const { configPath } = await import("../config.mjs")
10
+ const cp = configPath
11
+ const ac = agent.config?.agent ?? {}
12
+ const entries = [
13
+ { type: "header", text: "Current config" },
14
+ { type: "item", text: "Set embedding key (vector search)", action: "embedkey" },
15
+ { type: "item", text: "Advanced (set path value)", action: "set" },
16
+ ]
17
+ openPicker({
18
+ title: "Config",
19
+ entries,
20
+ onSelect: async (e) => {
21
+ if (e.action === "view") {
22
+ pushLabel(`❯ Config`, ansi.bold + C.tool)
23
+ pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
24
+ pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
25
+ const tn = `${ac.compactThreshold ?? 100000}${ac.compactThresholdAuto ? " (auto)" : ""}`
26
+ pushLine(`agent: maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn}`, C.dim)
27
+ pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${agent.config?.embedding?.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
28
+ pushLine(`Config file: ${cp}`, C.dim)
29
+ return
30
+ }
31
+ if (e.action === "embedkey") {
32
+ const embKey = await askQuestion("Enter embedding API key (default: SiliconFlow bge-m3):")
33
+ if (!embKey) return
34
+ agent.config.embedding ??= {}
35
+ agent.config.embedding.apiKey = embKey
36
+ await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: embKey } })
37
+ if (agent.memory) {
38
+ const { createEmbedder } = await import("../embedding.mjs")
39
+ agent.memory.embedder = createEmbedder(agent.config.embedding)
40
+ }
41
+ pushLabel(`❯ Config`, ansi.bold + C.tool)
42
+ pushLine(`Embedding key saved, vector search enabled`, C.tool)
43
+ return
44
+ }
45
+ if (e.action === "set") {
46
+ const settext = await askQuestion("Enter: <path> <value> (e.g. agent.maxTurns 80, supports a.b nesting):")
47
+ if (!settext) return
48
+ const parts = settext.split(/\s+/)
49
+ const [path, value] = [parts[0], parts.slice(1).join(" ")]
50
+ if (!path || !value) { pushLine("Usage: <path> <value> e.g. agent.maxTurns 80", C.error); return }
51
+ try {
52
+ const { configPath, loadConfig, saveConfig } = await import("../config.mjs")
53
+ const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
54
+ const keys = path.split(".")
55
+ let obj = raw
56
+ for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
57
+ obj[keys[keys.length - 1]] = isNaN(value) ? value : Number(value)
58
+ saveConfig(raw)
59
+ const cfg = loadConfig()
60
+ agent.provider = cfg.provider
61
+ agent.providers = cfg.providersList
62
+ agent.activeProvider = cfg.activeProvider
63
+ agent.config = cfg
64
+ pushLabel(`❯ Config`, ansi.bold + C.tool)
65
+ pushLine(`Saved: ${path} = ${value}`, C.tool)
66
+ } catch (error) {
67
+ pushLine(`Save failed: ${error.message}`, C.error)
68
+ }
69
+ }
70
+ },
71
+ })
72
+ }
@@ -0,0 +1,5 @@
1
+ /** /exit 命令:退出 TUI。
2
+ * ctx: { exit } — exit 是 index.mjs 注入的 cleanup + process.exit 回调 */
3
+ export async function handleExitCommand(ctx) {
4
+ ctx.exit()
5
+ }
@@ -0,0 +1,5 @@
1
+ /** /extract 命令:从当前会话提取知识。
2
+ * ctx: { runDistill } */
3
+ export async function handleExtractCommand(ctx) {
4
+ await ctx.runDistill()
5
+ }
@@ -0,0 +1,47 @@
1
+ import { ansi, C } from "./ansi.mjs"
2
+
3
+ /** /goal 命令:设置/查看/取消长期目标。
4
+ * 从 slash-commands.mjs 抽出。
5
+ * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion } */
6
+ export async function handleGoalCommand(ctx) {
7
+ const { agent, pushLine, pushLabel, openPicker, askQuestion } = ctx
8
+ const entries = [
9
+ { type: "header", text: agent.goal ? `Current goal: ${agent.goal.objective.slice(0, 60)}` : "Actions" },
10
+ { type: "item", text: "Set new goal", action: "set" },
11
+ ]
12
+ if (agent.goal) {
13
+ entries.push({ type: "item", text: "Cancel goal", action: "cancel" })
14
+ entries.push({ type: "item", text: "View details", action: "view" })
15
+ }
16
+ openPicker({
17
+ title: "Goal",
18
+ entries,
19
+ onSelect: async (e) => {
20
+ if (e.action === "view") {
21
+ const statusText = { active: "active", complete: "completed", blocked: "blocked" }[agent.goal.status] ?? agent.goal.status
22
+ pushLabel(`❯ Goal`, ansi.bold + C.warn)
23
+ pushLine(`Goal: ${agent.goal.objective}`, C.tool)
24
+ if (agent.goal.criteria) pushLine(` Criteria: ${agent.goal.criteria}`, C.dim)
25
+ pushLine(` Status: ${statusText} │ Turns used: ${agent.goal.turnsUsed ?? 0} │ Set at: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
26
+ return
27
+ }
28
+ if (e.action === "cancel") {
29
+ agent.goal = null
30
+ pushLabel(`❯ Goal`, ansi.bold + C.dim)
31
+ pushLine(`Goal cancelled.`, C.dim)
32
+ return
33
+ }
34
+ // set — 需要输入目标文本
35
+ const goalText = await askQuestion("Enter goal description (; separates criteria)")
36
+ if (!goalText) return
37
+ const semi = goalText.indexOf(";") >= 0 ? ";" : goalText.indexOf(";") >= 0 ? ";" : null
38
+ const objective = semi ? goalText.slice(0, semi).trim() : goalText.trim()
39
+ const criteria = semi ? goalText.slice(semi + 1).trim() : ""
40
+ agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
41
+ pushLabel(`❯ Goal`, ansi.bold + C.warn)
42
+ pushLine(`Goal set: ${objective}`, C.tool)
43
+ if (criteria) pushLine(` Criteria: ${criteria}`, C.dim)
44
+ else pushLine(` ⚠ No criteria — agent will be asked to provide verifiable criteria when using goal set`, C.warn)
45
+ },
46
+ })
47
+ }
@@ -0,0 +1,25 @@
1
+ import { ansi, C } from "./ansi.mjs"
2
+
3
+ /** /help 命令:列出所有斜杠命令及别名。
4
+ * ctx: { pushLine, pushLabel, SLASH_COMMANDS } */
5
+ export async function handleHelpCommand(ctx) {
6
+ const { pushLine, pushLabel, SLASH_COMMANDS } = ctx
7
+ const aliasList = { "/help": "/h", "/exit": "/x", "/model": "/m", "/plan": "/p", "/think": "/t", "/clear": "/c", "/new": "/n" }
8
+ const order = ["Agent", "Session", "Tools", "Config"]
9
+ const byGroup = new Map()
10
+ for (const c of SLASH_COMMANDS) {
11
+ if (!c.group) continue
12
+ if (!byGroup.has(c.group)) byGroup.set(c.group, [])
13
+ byGroup.get(c.group).push(c)
14
+ }
15
+ pushLabel(`❯ Help`, ansi.bold + C.tool)
16
+ for (const group of order) {
17
+ const cmds = byGroup.get(group)
18
+ if (!cmds) continue
19
+ pushLine(` ${group}:`, C.dim)
20
+ for (const c of cmds) {
21
+ const alias = aliasList[c.name]
22
+ pushLine(` ${c.name.padEnd(12)}${alias ? ` (${alias})`.padEnd(8) : " "} ${c.desc}`, C.text)
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,91 @@
1
+ import { existsSync } from "node:fs"
2
+ import { basename } from "node:path"
3
+ import { ansi, C } from "./ansi.mjs"
4
+
5
+ /** /init 命令:探测项目类型,生成 AGENTS.md 骨架。
6
+ * 从 slash-commands.mjs 抽出。
7
+ * ctx: { agent, pushLine, pushLabel } */
8
+ export async function handleInitCommand(ctx) {
9
+ const { agent, pushLine, pushLabel } = ctx
10
+ const { writeFile, readFile } = await import("node:fs/promises")
11
+ const { join } = await import("node:path")
12
+ const agPath = join(agent.cwd, "AGENTS.md")
13
+ if (existsSync(agPath)) {
14
+ pushLine(`AGENTS.md already exists: ${agPath}`, C.warn)
15
+ return
16
+ }
17
+
18
+ // 探测项目类型与关键信息
19
+ let name = basename(agent.cwd)
20
+ let lang = "", cmds = ""
21
+
22
+ // Node.js
23
+ try {
24
+ const pkg = JSON.parse(await readFile(join(agent.cwd, "package.json"), "utf8"))
25
+ if (pkg.name) name = pkg.name
26
+ lang = "Node.js"
27
+ const ks = Object.keys(pkg.scripts ?? {})
28
+ if (ks.length) cmds = ks.slice(0, 5).map(k => `- \`npm run ${k}\``).join("\n")
29
+ } catch {}
30
+
31
+ // Python
32
+ if (!lang) {
33
+ for (const f of ["requirements.txt", "pyproject.toml", "setup.py", "setup.cfg"]) {
34
+ if (existsSync(join(agent.cwd, f))) { lang = "Python"; break }
35
+ }
36
+ if (lang) cmds = "- `pip install -r requirements.txt`\n- `python -m pytest`"
37
+ }
38
+
39
+ // Go
40
+ if (!lang) {
41
+ if (existsSync(join(agent.cwd, "go.mod"))) {
42
+ lang = "Go"
43
+ cmds = "- `go build ./...`\n- `go test ./...`"
44
+ }
45
+ }
46
+
47
+ // Rust
48
+ if (!lang) {
49
+ if (existsSync(join(agent.cwd, "Cargo.toml"))) {
50
+ lang = "Rust"
51
+ cmds = "- `cargo build`\n- `cargo test`"
52
+ }
53
+ }
54
+
55
+ // Java / Kotlin
56
+ if (!lang) {
57
+ if (existsSync(join(agent.cwd, "pom.xml"))) { lang = "Java (Maven)"; cmds = "- `mvn test`" }
58
+ else if (existsSync(join(agent.cwd, "build.gradle")) || existsSync(join(agent.cwd, "build.gradle.kts"))) { lang = "Java/Kotlin (Gradle)"; cmds = "- `./gradlew test`" }
59
+ }
60
+
61
+ const lines = [
62
+ `# AGENTS.md — ${name} Project Guide`,
63
+ ``,
64
+ `## Project Overview`,
65
+ ``,
66
+ `Brief description of what this project does.`,
67
+ ``,
68
+ `## Tech Stack`,
69
+ ``,
70
+ lang ? `- Language: ${lang}` : `- Language: (detect and fill in)`,
71
+ `- Framework: (fill in if applicable)`,
72
+ ``,
73
+ `## Common Commands`,
74
+ ``,
75
+ cmds || `- (fill in build/test/run commands)`,
76
+ ``,
77
+ `## Coding Conventions`,
78
+ ``,
79
+ `- (fill in: naming, formatting, testing patterns)`,
80
+ ``,
81
+ `## Architecture Notes`,
82
+ ``,
83
+ `- (fill in: key modules, data flow, design decisions)`,
84
+ ]
85
+
86
+ const template = lines.join("\n")
87
+ await writeFile(agPath, template, "utf8")
88
+ pushLabel(`❯ Init`, ansi.bold + C.tool)
89
+ pushLine(`Generated AGENTS.md → ${agPath}${lang ? ` (${lang})` : ""}`, C.tool)
90
+ if (lang) pushLine("Tell me more about the project and I will fill in conventions and structure", C.dim)
91
+ }