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,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,7 @@
1
+ import { C } from "./ansi.mjs"
2
+
3
+ /** /model 命令:打开模型选择器。
4
+ * ctx: { openModelPicker, pushLine } */
5
+ export async function handleModelCommand(ctx) {
6
+ ctx.openModelPicker().catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
7
+ }
@@ -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
+ }