thincoder 0.7.8 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -13
- package/bin/thincoder.js +4 -0
- package/bin/thincoder.mjs +27 -346
- package/package.json +2 -2
- package/src/agent/dispatch.mjs +98 -0
- package/src/agent/helpers.mjs +185 -0
- package/src/agent/setup.mjs +117 -0
- package/src/agent-tools/goal.mjs +71 -0
- package/src/agent-tools/plan.mjs +31 -0
- package/src/agent-tools/recent-changes.mjs +23 -0
- package/src/agent-tools/skill.mjs +46 -0
- package/src/agent-tools/subagent.mjs +113 -0
- package/src/agent-tools/task.mjs +67 -0
- package/src/agent-tools/verify.mjs +198 -0
- package/src/agent-tools.mjs +12 -0
- package/src/agent.mjs +90 -1040
- package/src/cli/distill-command.mjs +85 -0
- package/src/cli/make-agent.mjs +85 -0
- package/src/cli/memory-command.mjs +63 -0
- package/src/cli/permission.mjs +41 -0
- package/src/cli/setup-wizard.mjs +70 -0
- package/src/config.mjs +5 -8
- package/src/context.mjs +10 -13
- package/src/distill.mjs +4 -3
- package/src/embedding.mjs +4 -2
- package/src/{checkpoint.mjs → git/checkpoint.mjs} +1 -1
- package/src/mcp/helpers.mjs +37 -0
- package/src/mcp/transport-http.mjs +176 -0
- package/src/mcp/transport-stdio.mjs +84 -0
- package/src/mcp/transport-ws.mjs +87 -0
- package/src/mcp.mjs +4 -428
- package/src/memory/code-index.mjs +211 -0
- package/src/memory/code-sync.mjs +306 -0
- package/src/memory/core.mjs +277 -0
- package/src/memory/docs.mjs +262 -0
- package/src/memory/schema.mjs +426 -0
- package/src/memory.mjs +12 -1403
- package/src/provider/core.mjs +239 -0
- package/src/provider/index.mjs +6 -0
- package/src/provider/rate.mjs +104 -0
- package/src/session.mjs +18 -5
- package/src/tools/bash.mjs +144 -0
- package/src/tools/file.mjs +205 -0
- package/src/tools/git.mjs +166 -0
- package/src/tools/glob.mjs +51 -0
- package/src/tools/grep.mjs +100 -0
- package/src/tools/index.mjs +22 -0
- package/src/tools/ls.mjs +36 -0
- package/src/tools/patch.mjs +226 -0
- package/src/tools/repomap-parse.mjs +168 -0
- package/src/tools/shared.mjs +257 -0
- package/src/tools/system.mjs +336 -0
- package/src/tools/web.mjs +121 -0
- package/src/tools.mjs +2 -1194
- package/src/tui/agent-turn.mjs +254 -0
- package/src/tui/ansi.mjs +32 -0
- package/src/tui/clipboard.mjs +48 -0
- package/src/tui/cmd-auto.mjs +21 -0
- package/src/tui/cmd-clear.mjs +26 -0
- package/src/tui/cmd-config.mjs +72 -0
- package/src/tui/cmd-exit.mjs +5 -0
- package/src/tui/cmd-extract.mjs +5 -0
- package/src/tui/cmd-goal.mjs +47 -0
- package/src/tui/cmd-help.mjs +25 -0
- package/src/tui/cmd-init.mjs +91 -0
- package/src/tui/cmd-mcp.mjs +146 -0
- package/src/tui/cmd-model.mjs +7 -0
- package/src/tui/cmd-new.mjs +18 -0
- package/src/tui/cmd-plan.mjs +21 -0
- package/src/tui/cmd-reindex.mjs +44 -0
- package/src/tui/cmd-restore.mjs +39 -0
- package/src/tui/cmd-session.mjs +42 -0
- package/src/tui/cmd-skills.mjs +17 -0
- package/src/tui/cmd-think.mjs +56 -0
- package/src/tui/config-helpers.mjs +34 -0
- package/src/tui/distill-cmd.mjs +45 -0
- package/src/tui/index.mjs +330 -0
- package/src/tui/interaction.mjs +79 -0
- package/src/tui/key-handler.mjs +267 -0
- package/src/tui/layout.mjs +115 -0
- package/src/tui/pickers.mjs +279 -0
- package/src/tui/render-frame.mjs +304 -0
- package/src/tui/render.mjs +205 -0
- package/src/tui/slash-commands.mjs +138 -0
- package/src/tui/startup.mjs +113 -0
- package/src/tui/wizard.mjs +168 -0
- package/src/tui-render.mjs +4 -0
- package/src/tui.mjs +3 -2566
- package/src/provider.mjs +0 -383
- /package/src/{gitmem.mjs → git/gitmem.mjs} +0 -0
- /package/src/{coder-overlay.md → prompts/coder.md} +0 -0
- /package/src/{discipline-rules.md → prompts/discipline.md} +0 -0
- /package/src/{explore-overlay.md → prompts/explore.md} +0 -0
- /package/src/{main-overlay.md → prompts/main.md} +0 -0
- /package/src/{plan-overlay.md → prompts/plan.md} +0 -0
- /package/src/{SYSTEM_PROMPT.md → prompts/system.md} +0 -0
- /package/src/{repomap.mjs → tools/repomap.mjs} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
|
|
3
|
+
/** /mcp 命令处理器:查看/添加/删除/重连 MCP server。
|
|
4
|
+
* 从 slash-commands.mjs 抽出,含 /mcp 专用的 parseHeaders / addAndConnect helper。
|
|
5
|
+
* ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, ansi, C } */
|
|
6
|
+
|
|
7
|
+
function parseHeaders(pairs) {
|
|
8
|
+
const headers = {}
|
|
9
|
+
for (const pair of pairs) {
|
|
10
|
+
const eq = pair.indexOf("=")
|
|
11
|
+
if (eq > 0) headers[pair.slice(0, eq)] = pair.slice(eq + 1).replace(/^["']|["']$/g, "")
|
|
12
|
+
}
|
|
13
|
+
return headers
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** /mcp 共享 helper: 保存 Config + Connecting (persistRaw 从 ctx 获取) */
|
|
17
|
+
async function addAndConnect(ctx, srv) {
|
|
18
|
+
const { agent, pushLine, pushLabel, persistRaw } = ctx
|
|
19
|
+
await persistRaw((raw) => {
|
|
20
|
+
raw.mcp ??= { servers: [] }
|
|
21
|
+
const entry = { name: srv.name }
|
|
22
|
+
if (srv.url) { entry.url = srv.url; if (srv.headers) entry.headers = srv.headers }
|
|
23
|
+
else if (srv.wsUrl) { entry.wsUrl = srv.wsUrl; if (srv.headers) entry.headers = srv.headers }
|
|
24
|
+
else { entry.command = srv.command; if (srv.args) entry.args = srv.args }
|
|
25
|
+
raw.mcp.servers.push(entry)
|
|
26
|
+
})
|
|
27
|
+
agent.config ??= {}
|
|
28
|
+
agent.config.mcp ??= { servers: [] }
|
|
29
|
+
agent.config.mcp.servers.push(srv)
|
|
30
|
+
try {
|
|
31
|
+
pushLine(`[mcp] Connecting ${srv.name}...`, C.dim)
|
|
32
|
+
const { connectMcpServer } = await import("../mcp.mjs")
|
|
33
|
+
const tools = await connectMcpServer(srv)
|
|
34
|
+
agent.tools.push(...tools)
|
|
35
|
+
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
36
|
+
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
37
|
+
pushLine(`${srv.name} (${desc}) connected, ${tools.length} tools:`, C.tool)
|
|
38
|
+
for (const t of tools) pushLine(` ${t.name}: ${t.description.slice(0, 100)}`, C.dim)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
pushLine(`[mcp] ${srv.name}: ${error.message} (config saved, retry after restart)`, C.error)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function handleMcpCommand(ctx) {
|
|
45
|
+
const { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw } = ctx
|
|
46
|
+
const servers = agent.config?.mcp?.servers ?? []
|
|
47
|
+
const entries = [
|
|
48
|
+
{ type: "header", text: `${servers.length} MCP servers configured` },
|
|
49
|
+
{ type: "item", text: "View list", action: "list" },
|
|
50
|
+
{ type: "item", text: "Add server", action: "add" },
|
|
51
|
+
]
|
|
52
|
+
if (servers.length > 0) {
|
|
53
|
+
entries.push(
|
|
54
|
+
{ type: "item", text: "Remove server", action: "remove" },
|
|
55
|
+
{ type: "item", text: "Reconnect server", action: "connect" },
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
openPicker({
|
|
59
|
+
title: "MCP",
|
|
60
|
+
entries,
|
|
61
|
+
onSelect: async (e) => {
|
|
62
|
+
if (e.action === "list") {
|
|
63
|
+
pushLabel(`❯ MCP Servers`, ansi.bold + C.tool)
|
|
64
|
+
if (servers.length === 0) {
|
|
65
|
+
pushLine(" (no MCP server configured)", C.dim)
|
|
66
|
+
}
|
|
67
|
+
for (const srv of servers) {
|
|
68
|
+
const connected = agent.tools.some((t) => t._mcpName === srv.name)
|
|
69
|
+
const mark = connected ? "●" : "○"
|
|
70
|
+
const color = connected ? C.tool : C.dim
|
|
71
|
+
const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
|
|
72
|
+
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
73
|
+
pushLine(` ${mark} ${srv.name} (${desc})${connected ? ` — ${toolCount} tools` : ""}`, color)
|
|
74
|
+
}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (e.action === "remove") {
|
|
78
|
+
const removeEntries = [
|
|
79
|
+
{ type: "header", text: "Select server to remove" },
|
|
80
|
+
...servers.map((s) => ({ type: "item", text: `${s.name} (${s.wsUrl ?? s.url ?? s.command})`, name: s.name })),
|
|
81
|
+
]
|
|
82
|
+
openPicker({
|
|
83
|
+
title: "Remove MCP Server",
|
|
84
|
+
entries: removeEntries,
|
|
85
|
+
onSelect: async (se) => {
|
|
86
|
+
// 从配置删除
|
|
87
|
+
agent.config.mcp.servers = servers.filter((s) => s.name !== se.name)
|
|
88
|
+
await persistRaw((raw) => { raw.mcp.servers = agent.config.mcp.servers })
|
|
89
|
+
// 从工具列表删除
|
|
90
|
+
const { removeMcpTools } = await import("../mcp.mjs")
|
|
91
|
+
removeMcpTools(agent, se.name)
|
|
92
|
+
pushLine(`[mcp] ${se.name} removed`, C.tool)
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (e.action === "connect") {
|
|
98
|
+
const connectEntries = [
|
|
99
|
+
{ type: "header", text: "Select server to reconnect" },
|
|
100
|
+
...servers.map((s) => ({ type: "item", text: `${s.name} (${s.wsUrl ?? s.url ?? s.command})`, name: s.name })),
|
|
101
|
+
]
|
|
102
|
+
openPicker({
|
|
103
|
+
title: "Reconnect MCP",
|
|
104
|
+
entries: connectEntries,
|
|
105
|
+
onSelect: async (se) => {
|
|
106
|
+
const srv = servers.find((s) => s.name === se.name)
|
|
107
|
+
const { removeMcpTools, connectMcpServer } = await import("../mcp.mjs")
|
|
108
|
+
removeMcpTools(agent, se.name)
|
|
109
|
+
try {
|
|
110
|
+
pushLine(`[mcp] Reconnecting ${se.name}...`, C.dim)
|
|
111
|
+
const tools = await connectMcpServer(srv)
|
|
112
|
+
agent.tools.push(...tools)
|
|
113
|
+
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
114
|
+
pushLine(`${se.name} reconnected, ${tools.length} tools available.`, C.tool)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
pushLine(`[mcp] ${se.name}: ${error.message}`, C.error)
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
if (e.action === "add") {
|
|
123
|
+
const mcpInput = await askQuestion("Enter: <name> <URL|command> [args...]\nURL auto-detect: https://… → HTTP, ws://… → WebSocket, other → stdio command")
|
|
124
|
+
if (!mcpInput) return
|
|
125
|
+
const parts = mcpInput.split(/\s+/)
|
|
126
|
+
if (parts.length < 2) { pushLine("Usage: <name> <URL|command> [args...]", C.error); return }
|
|
127
|
+
const [name, second, ...extras] = parts
|
|
128
|
+
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
129
|
+
if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
|
|
130
|
+
const isWS = /^wss?:\/\//.test(second)
|
|
131
|
+
const isHTTP = /^https?:\/\//.test(second)
|
|
132
|
+
let srv
|
|
133
|
+
if (isWS) {
|
|
134
|
+
const headers = parseHeaders(extras)
|
|
135
|
+
srv = { name, wsUrl: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
136
|
+
} else if (isHTTP) {
|
|
137
|
+
const headers = parseHeaders(extras)
|
|
138
|
+
srv = { name, url: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
139
|
+
} else {
|
|
140
|
+
srv = { name, command: second, args: extras.length > 0 ? extras : undefined }
|
|
141
|
+
}
|
|
142
|
+
await addAndConnect(ctx, srv)
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { clearSession } from "../session.mjs"
|
|
2
|
+
import { C } from "./ansi.mjs"
|
|
3
|
+
|
|
4
|
+
/** /new 命令:新建会话(旧会话归档到槽位)。
|
|
5
|
+
* ctx: { agent, state, pushLine } */
|
|
6
|
+
export async function handleNewCommand(ctx) {
|
|
7
|
+
const { agent, state, pushLine } = ctx
|
|
8
|
+
agent.history = []
|
|
9
|
+
agent.tasks = []
|
|
10
|
+
agent.planMode = false
|
|
11
|
+
agent.goal = null
|
|
12
|
+
agent._pendingReminders = []
|
|
13
|
+
state.tasks = []
|
|
14
|
+
state.lines = []
|
|
15
|
+
state.streaming = ""
|
|
16
|
+
clearSession(agent.cwd)
|
|
17
|
+
pushLine("New session started (old session archived to slot; /session to view)", C.dim)
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
|
|
3
|
+
/** /plan 命令:切换 plan mode(只读探索 → 设计 → 实现)。
|
|
4
|
+
* ctx: { agent, pushLine, pushLabel } */
|
|
5
|
+
export async function handlePlanCommand(ctx) {
|
|
6
|
+
const { agent, pushLine, pushLabel } = ctx
|
|
7
|
+
agent.planMode = !agent.planMode
|
|
8
|
+
agent._pendingReminders = agent._pendingReminders ?? []
|
|
9
|
+
if (agent.planMode) {
|
|
10
|
+
agent._pendingReminders.push("[System reminder: plan mode is now ON. You are restricted to READ-ONLY tools — explore, search, read, analyze. DO NOT write, edit, or run mutation commands. Present your design to the user first.]")
|
|
11
|
+
} else {
|
|
12
|
+
agent._pendingReminders.push("[System reminder: plan mode is now OFF. You may edit files, run commands, and implement changes.]")
|
|
13
|
+
}
|
|
14
|
+
pushLabel(`❯ Plan`, ansi.bold + (agent.planMode ? C.tool : C.dim))
|
|
15
|
+
pushLine(
|
|
16
|
+
agent.planMode
|
|
17
|
+
? `Plan mode ON: read-only tools only. Design first, then implement. /plan again to exit.`
|
|
18
|
+
: `Plan mode OFF: you may now edit files and run commands.`,
|
|
19
|
+
agent.planMode ? C.tool : C.dim,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { C } from "./ansi.mjs"
|
|
2
|
+
|
|
3
|
+
/** /reindex 命令:重建记忆索引(files + code_chunks + doc_chunks)。
|
|
4
|
+
* ctx: { agent, distillOpts, pushLine } */
|
|
5
|
+
export async function handleReindexCommand(ctx) {
|
|
6
|
+
const { agent, distillOpts, pushLine } = ctx
|
|
7
|
+
const { syncDir, codeSync, docSync } = await import("../memory.mjs")
|
|
8
|
+
pushLine("[reindex] Rebuilding index...", C.tool)
|
|
9
|
+
agent.memory.db.prepare("DELETE FROM files").run()
|
|
10
|
+
agent.memory.db.prepare("DELETE FROM code_chunks").run()
|
|
11
|
+
agent.memory.db.prepare("DELETE FROM doc_chunks").run()
|
|
12
|
+
let total = 0
|
|
13
|
+
if (distillOpts.projectDir) {
|
|
14
|
+
const s = await syncDir(agent.memory, { layer: "project", dir: distillOpts.projectDir })
|
|
15
|
+
total += s.added
|
|
16
|
+
pushLine(` project: +${s.added} ~${s.updated} -${s.removed}`, C.dim)
|
|
17
|
+
}
|
|
18
|
+
if (distillOpts.team?.dir) {
|
|
19
|
+
const s = await syncDir(agent.memory, { layer: "team", dir: distillOpts.team.dir })
|
|
20
|
+
total += s.added
|
|
21
|
+
pushLine(` team: +${s.added} ~${s.updated} -${s.removed}`, C.dim)
|
|
22
|
+
}
|
|
23
|
+
// 重建代码索引和文档索引并行(读写不同表,WAL 支持)
|
|
24
|
+
pushLine(` [code+doc] Rebuilding indexes...`, C.tool)
|
|
25
|
+
const [cr, dr] = await Promise.all([
|
|
26
|
+
codeSync(agent.memory, agent.cwd, {
|
|
27
|
+
onProgress: (p) => {
|
|
28
|
+
if (p.phase === "index" && p.current % 20 === 0) {
|
|
29
|
+
pushLine(` code: ${p.current}/${p.total}`, C.dim)
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
docSync(agent.memory, agent.cwd, {
|
|
34
|
+
onProgress: (p) => {
|
|
35
|
+
if (p.phase === "index" && p.current % 5 === 0) {
|
|
36
|
+
pushLine(` doc: ${p.current}/${p.total}`, C.dim)
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
])
|
|
41
|
+
pushLine(` code: ${cr.total} files, +${cr.updated} ~${cr.skipped} -${cr.removed}`, C.dim)
|
|
42
|
+
pushLine(` doc: ${dr.total} files, +${dr.updated} ~${dr.skipped} -${dr.removed}`, C.dim)
|
|
43
|
+
pushLine(`[reindex] Done, ${total} entries total. Vectors will be lazily generated on next search.`, C.tool)
|
|
44
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
|
|
3
|
+
/** /restore 命令:列出 git checkpoint 并回滚到选中快照。
|
|
4
|
+
* ctx: { agent, openPicker, pushLine, pushLabel } */
|
|
5
|
+
export async function handleRestoreCommand(ctx) {
|
|
6
|
+
const { agent, openPicker, pushLine, pushLabel } = ctx
|
|
7
|
+
const { listCheckpoints, rewind, isGitRepo } = await import("../git/checkpoint.mjs")
|
|
8
|
+
if (!isGitRepo(agent.cwd)) {
|
|
9
|
+
pushLine("[rewind] not a git repository, checkpoints unavailable", C.error)
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
const cps = await listCheckpoints(agent.cwd)
|
|
13
|
+
if (cps.length === 0) {
|
|
14
|
+
pushLine("(no checkpoints — created automatically before each task)", C.dim)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const entries = [
|
|
18
|
+
{ type: "header", text: "Checkpoints (↑↓ select, Enter restore, Esc cancel)" },
|
|
19
|
+
...cps.slice(0, 12).map((cp) => ({
|
|
20
|
+
type: "item",
|
|
21
|
+
text: `${cp.id} ${new Date(cp.time).toLocaleString()} (+${cp.untracked} untracked files)`,
|
|
22
|
+
id: cp.id,
|
|
23
|
+
})),
|
|
24
|
+
]
|
|
25
|
+
openPicker({
|
|
26
|
+
title: "Restore Checkpoint",
|
|
27
|
+
entries,
|
|
28
|
+
onSelect: async (e) => {
|
|
29
|
+
try {
|
|
30
|
+
const summary = await rewind(agent.cwd, e.id)
|
|
31
|
+
pushLabel(`❯ Rewind`, ansi.bold + C.warn)
|
|
32
|
+
pushLine(`Restored to ${e.id}: patch ${summary.patchApplied ? "applied" : "none"}, deleted ${summary.deleted} new files, restored ${summary.restored} file(s)`, C.tool)
|
|
33
|
+
pushLine("(current state saved as new checkpoint; /restore again to go back)", C.dim)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
pushLine(`[rewind] ${error.message}`, C.error)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { listSlots, switchToSlot, applySession } from "../session.mjs"
|
|
2
|
+
import { ansi, C } from "./ansi.mjs"
|
|
3
|
+
|
|
4
|
+
/** /session 命令:列出/切换归档会话槽位。
|
|
5
|
+
* ctx: { agent, state, openPicker, pushLine, pushLabel, render } */
|
|
6
|
+
export async function handleSessionCommand(ctx) {
|
|
7
|
+
const { agent, state, openPicker, pushLine, pushLabel, render } = ctx
|
|
8
|
+
const slots = listSlots(agent.cwd)
|
|
9
|
+
if (slots.length === 0) {
|
|
10
|
+
pushLine("No archived sessions (use /new and old sessions auto-archive to slots)", C.dim)
|
|
11
|
+
} else {
|
|
12
|
+
const entries = [
|
|
13
|
+
{ type: "header", text: `Archived sessions (↑↓ select, Enter switch, Esc cancel)` },
|
|
14
|
+
...slots.map((s) => ({
|
|
15
|
+
type: "item",
|
|
16
|
+
text: `Slot ${s.slot} — ${s.date}`,
|
|
17
|
+
slot: s.slot,
|
|
18
|
+
})),
|
|
19
|
+
]
|
|
20
|
+
openPicker({
|
|
21
|
+
title: "Sessions",
|
|
22
|
+
entries,
|
|
23
|
+
onSelect: (e) => {
|
|
24
|
+
const data = switchToSlot(agent.cwd, e.slot)
|
|
25
|
+
if (!data) {
|
|
26
|
+
pushLine(`Slot ${e.slot} not found`, C.dim)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
applySession(agent, data)
|
|
30
|
+
state.lines = data.display.length
|
|
31
|
+
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
32
|
+
: []
|
|
33
|
+
state.tasks = agent.tasks ?? []
|
|
34
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
35
|
+
state.tasks = []
|
|
36
|
+
}
|
|
37
|
+
pushLabel(`── Switched to slot ${e.slot} (${data.history.length} messages) ──`, C.warn)
|
|
38
|
+
render()
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
|
|
3
|
+
/** /skills 命令:列出项目技能。
|
|
4
|
+
* ctx: { agent, pushLine, pushLabel } */
|
|
5
|
+
export async function handleSkillsCommand(ctx) {
|
|
6
|
+
const { agent, pushLine, pushLabel } = ctx
|
|
7
|
+
const { loadSkills } = await import("../skills.mjs")
|
|
8
|
+
const skills = await loadSkills(agent.cwd)
|
|
9
|
+
pushLabel(`❯ Skills`, ansi.bold + C.tool)
|
|
10
|
+
if (skills.length === 0) {
|
|
11
|
+
pushLine(" (no project skills — create .md files under .thincoder/skills/ to add some)", C.dim)
|
|
12
|
+
}
|
|
13
|
+
for (const s of skills) {
|
|
14
|
+
pushLine(` ${s.name}: ${s.description.slice(0, 100)}`, C.dim)
|
|
15
|
+
}
|
|
16
|
+
pushLine("Activate: tell the agent \"load the <name> skill\"", C.dim)
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
|
|
3
|
+
/** /think 命令:切换思考模式、设置 reasoning effort。
|
|
4
|
+
* 从 slash-commands.mjs 抽出。
|
|
5
|
+
* ctx: { agent, pushLine, pushLabel, openPicker, syncProviderField } */
|
|
6
|
+
export async function handleThinkCommand(ctx) {
|
|
7
|
+
const { agent, pushLine, pushLabel, openPicker, syncProviderField } = ctx
|
|
8
|
+
const cur = agent.provider
|
|
9
|
+
const thinkingEnabled = cur.thinking?.type === "enabled" || cur.thinking?.type === undefined
|
|
10
|
+
const { specForModel } = await import("../config.mjs")
|
|
11
|
+
const spec = specForModel(cur.model)
|
|
12
|
+
const isEffortOnly = spec.thinkApi === "effort"
|
|
13
|
+
const entries = []
|
|
14
|
+
if (!isEffortOnly) {
|
|
15
|
+
entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
|
|
16
|
+
}
|
|
17
|
+
if (spec.reasoningEffortEnum) {
|
|
18
|
+
for (const level of spec.reasoningEffortEnum) {
|
|
19
|
+
const mark = cur.reasoningEffort === level ? "▸ " : " "
|
|
20
|
+
entries.push({ type: "item", text: `${mark}effort: ${level}`, action: "effort", level })
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
entries.push({ type: "item", text: "effort: high", action: "effort", level: "high" })
|
|
24
|
+
entries.push({ type: "item", text: "effort: max", action: "effort", level: "max" })
|
|
25
|
+
}
|
|
26
|
+
openPicker({
|
|
27
|
+
title: "Think",
|
|
28
|
+
entries,
|
|
29
|
+
onSelect: async (e) => {
|
|
30
|
+
if (e.action === "effort") {
|
|
31
|
+
cur.reasoningEffort = e.level
|
|
32
|
+
await syncProviderField("reasoningEffort", e.level)
|
|
33
|
+
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
34
|
+
pushLine(`Reasoning effort set to ${e.level}`, C.tool)
|
|
35
|
+
} else {
|
|
36
|
+
const enable = e.action === "on"
|
|
37
|
+
if (isEffortOnly) {
|
|
38
|
+
if (!enable) delete cur.reasoningEffort
|
|
39
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
40
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
41
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
42
|
+
} else {
|
|
43
|
+
cur.thinking = enable ? { type: "enabled" } : { type: "disabled" }
|
|
44
|
+
if (!enable) delete cur.reasoningEffort
|
|
45
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
46
|
+
await syncProviderField("thinking", cur.thinking)
|
|
47
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
48
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
49
|
+
}
|
|
50
|
+
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
51
|
+
pushLine(`Thinking mode ${enable ? "On" : "Off"}`, C.tool)
|
|
52
|
+
if (enable) pushLine(`Reasoning effort: ${cur.reasoningEffort}`, C.dim)
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
|
|
3
|
+
/** persistRaw / syncProviderField / maskKey:配置读写辅助。
|
|
4
|
+
* 从 index.mjs 抽出,供 slash-commands / wizard / pickers 共用。
|
|
5
|
+
* createConfigHelpers(agent) 返回 { persistRaw, syncProviderField, maskKey } */
|
|
6
|
+
export function createConfigHelpers(agent) {
|
|
7
|
+
/** 读取 config.json → mutate → 落盘 */
|
|
8
|
+
async function persistRaw(mutate) {
|
|
9
|
+
const { saveConfig, configPath } = await import("../config.mjs")
|
|
10
|
+
const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
|
|
11
|
+
mutate(raw)
|
|
12
|
+
saveConfig(raw)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 同步当前 provider 的某个字段到 providers 数组并落盘 */
|
|
16
|
+
async function syncProviderField(field, value) {
|
|
17
|
+
const target = agent.providers.find((p) => p.name === agent.activeProvider)
|
|
18
|
+
if (!target) return
|
|
19
|
+
if (value === undefined) delete target[field]
|
|
20
|
+
else target[field] = value
|
|
21
|
+
await persistRaw((raw) => {
|
|
22
|
+
raw.providers = agent.providers
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** API key 脱敏显示 */
|
|
27
|
+
function maskKey(key) {
|
|
28
|
+
if (!key) return "(none)"
|
|
29
|
+
if (key.length <= 8) return "***"
|
|
30
|
+
return key.slice(0, 5) + "\u2026" + key.slice(-4)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { persistRaw, syncProviderField, maskKey }
|
|
34
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { C } from "./ansi.mjs"
|
|
2
|
+
|
|
3
|
+
/** /distill 命令:从当前会话提取候选,逐条 y/n 确认后入库。
|
|
4
|
+
* 从 index.mjs 抽出。
|
|
5
|
+
* ctx: { agent, state, pushLine, render, askPermission, distillOpts } */
|
|
6
|
+
export async function runDistill(ctx) {
|
|
7
|
+
const { agent, state, pushLine, render, askPermission, distillOpts } = ctx
|
|
8
|
+
if (agent.history.length === 0) {
|
|
9
|
+
pushLine("[distill] Current session is empty, nothing to extract", C.dim)
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
state.processing = true
|
|
13
|
+
state.status = "Distilling..."
|
|
14
|
+
render()
|
|
15
|
+
try {
|
|
16
|
+
const { extractCandidates, historyToTranscript, saveCandidate } = await import("../distill.mjs")
|
|
17
|
+
pushLine("[distill] Analyzing session...", C.tool)
|
|
18
|
+
const candidates = await extractCandidates(agent.provider, historyToTranscript(agent.history))
|
|
19
|
+
if (candidates.length === 0) {
|
|
20
|
+
pushLine("[distill] No knowledge worth saving from this session", C.dim)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
let saved = 0
|
|
24
|
+
for (const c of candidates) {
|
|
25
|
+
pushLine(`── Candidate [${c.type}] ${c.title} (scope: ${c.scope ?? "personal"})`, C.warn)
|
|
26
|
+
for (const line of c.content.split("\n").slice(0, 6)) pushLine(` ${line}`, C.dim)
|
|
27
|
+
if (c.type === "rule") pushLine(" (rule type — consider writing manually; press y to extract)", C.warn)
|
|
28
|
+
const accept = await askPermission("distill-save", { title: c.title })
|
|
29
|
+
if (!accept) {
|
|
30
|
+
pushLine(" skipped", C.dim)
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
const where = await saveCandidate(agent.memory, c, distillOpts)
|
|
34
|
+
pushLine(` saved -> ${where}`, C.tool)
|
|
35
|
+
saved++
|
|
36
|
+
}
|
|
37
|
+
pushLine(`[distill] Done: saved ${saved}/${candidates.length} item(s)`, C.tool)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
pushLine(`[distill] error: ${error.message}`, C.error)
|
|
40
|
+
} finally {
|
|
41
|
+
state.processing = false
|
|
42
|
+
state.status = "Ready"
|
|
43
|
+
render()
|
|
44
|
+
}
|
|
45
|
+
}
|