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