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,113 @@
1
+ import { listSlots } from "../session.mjs"
2
+ import { sliceByWidth } from "./render.mjs"
3
+ import { ansi, C } from "./ansi.mjs"
4
+
5
+ /** 启动画面 + 会话恢复 + 后台索引。
6
+ * 从 index.mjs 抽出。
7
+ * ctx: { agent, state, opts, pushLine, pushLabel, render, startWizard } */
8
+ export function showStartup(ctx) {
9
+ const { agent, state, opts, pushLine, pushLabel, render, startWizard } = ctx
10
+
11
+ // 启动画面
12
+ if (!agent.provider.apiKey) {
13
+ pushLabel(`Welcome to ThinCoder!`, ansi.bold + C.tool)
14
+ pushLine("No API key configured yet — entering initial setup (Esc to skip anytime)", C.text)
15
+ startWizard()
16
+ } else {
17
+ pushLine(`Welcome to ThinCoder. Provider: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
18
+ }
19
+ pushLine(`Tools: ${agent.tools.map((t) => t.name).join(", ")}`, C.dim)
20
+
21
+ // 恢复上次会话:重建对话区显示 (tool 结果行省略,保持清爽)
22
+ if (opts.restored?.display?.length) {
23
+ // 用户视角的恢复:display 是退出前对话区的原样快照,所见即所得
24
+ state.lines = [...opts.restored.display.map((l) => ({ text: l.text, color: l.color })), ...state.lines]
25
+ pushLabel(`── Restored previous session; /new for a fresh session ──`, C.warn)
26
+ } else if (opts.restored?.history?.length) {
27
+ // 重建对话区:user/assistant 消息逐条展示,tool 结果行只保留首行摘要
28
+ for (let i = 0; i < opts.restored.history.length; i++) {
29
+ const m = opts.restored.history[i]
30
+ if (m.role === "user") {
31
+ if (typeof m.content === "string" && m.content.startsWith("[System reminder:")) continue
32
+ pushLabel(`❯ You:`, ansi.bold + C.user)
33
+ if (typeof m.content === "string" && m.content) pushLine(m.content, C.text)
34
+ } else if (m.role === "assistant") {
35
+ pushLabel(`❯ ThinCoder:`, ansi.bold + C.assistant)
36
+ if (typeof m.content === "string" && m.content) pushLine(m.content, C.text)
37
+ for (const tc of m.tool_calls ?? []) {
38
+ // 找到下一条对应的 tool 结果,显示首行摘要
39
+ const toolResult = opts.restored.history[i + 1]
40
+ const hasResult = toolResult?.role === "tool" && toolResult?.tool_call_id === tc.id
41
+ const summary = hasResult ? " → " + sliceByWidth(String(toolResult.content).split("\n")[0], 80) : ""
42
+ pushLine(` [tool] ${tc.function?.name ?? "?"}${summary}`, C.tool)
43
+ }
44
+ }
45
+ // tool 消息本身不单独渲染——已在 assistant 的 tool_calls 后以摘要形式展示
46
+ }
47
+ pushLabel(`── Restored previous session (${opts.restored.history.length} messages); /new for a fresh session ──`, C.warn)
48
+ }
49
+
50
+ // 有归档槽位时给个提示
51
+ if (listSlots(agent.cwd).length > 0) {
52
+ pushLine("Tip: archived sessions available — /session to view/switch", C.dim)
53
+ }
54
+ render()
55
+ }
56
+
57
+ /** 后台索引 (进界面后再跑,不阻塞启动);进度走底部状态栏,不往对话区塞行。
58
+ * 优先用 git diff 增量(快),git 不可用或首次运行时退到全量扫描。 */
59
+ export async function backgroundIndex(ctx) {
60
+ const { agent, state, render } = ctx
61
+ const { codeSync, docSync, gitSync } = await import("../memory.mjs")
62
+ const cwd = agent.cwd
63
+ let codeFiles = 0, docFiles = 0
64
+
65
+ state.status = "Indexing..."
66
+ render()
67
+
68
+ const gitRes = await gitSync(agent.memory, cwd, {
69
+ onProgress: (p) => {
70
+ if (p.phase === "index" && p.current % 5 === 0) {
71
+ state.status = `Indexing... ${p.current}/${p.total}`
72
+ render()
73
+ }
74
+ }
75
+ })
76
+
77
+ if (gitRes !== null) {
78
+ // git 增量成功,直接统计
79
+ codeFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM code_chunks`).get()?.n ?? 0
80
+ docFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM doc_chunks`).get()?.n ?? 0
81
+ } else {
82
+ // 退到全量扫描(codeSync 和 docSync 并行——读写不同表,SQLite WAL 天然支持)
83
+ const [codeRes, docRes] = await Promise.allSettled([
84
+ codeSync(agent.memory, cwd, {
85
+ onProgress: (p) => {
86
+ if (p.phase === "index" && p.current % 30 === 0) {
87
+ state.status = `Indexing code... ${p.current}/${p.total}`
88
+ render()
89
+ }
90
+ }
91
+ }),
92
+ docSync(agent.memory, cwd, {
93
+ onProgress: (p) => {
94
+ if (p.phase === "index" && p.current % 10 === 0) {
95
+ state.status = `Indexing docs... ${p.current}/${p.total}`
96
+ render()
97
+ }
98
+ }
99
+ }),
100
+ ])
101
+ if (codeRes.status === "fulfilled") {
102
+ codeFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM code_chunks`).get()?.n ?? 0
103
+ }
104
+ if (docRes.status === "fulfilled") {
105
+ docFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM doc_chunks`).get()?.n ?? 0
106
+ }
107
+ }
108
+
109
+ state.status = codeFiles || docFiles
110
+ ? `Ready — idx code ${codeFiles} doc ${docFiles}`
111
+ : "Ready"
112
+ render()
113
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * wizard.mjs — 首次启动 Config 向导
3
+ * 从 index.mjs 抽出:provider 选择 → 逐项输入 (name/baseURL/model/key/embedkey) → 持久化 → 接模型选择器。
4
+ * 通过 ctx 对象访问 startTUI 闭包中的共享状态与 UI 函数。
5
+ * ctx: { agent, state, pushLine, pushLabel, render, persistRaw, openModelPicker }
6
+ */
7
+
8
+ import { PROVIDER_PRESETS as PRESETS } from "../config.mjs"
9
+ import { ansi, C } from "./ansi.mjs"
10
+
11
+ /**
12
+ * 创建向导控制器。
13
+ * 返回 { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard }
14
+ */
15
+ export function createWizard(ctx) {
16
+ const { agent, state, pushLine, pushLabel, render, persistRaw } = ctx
17
+
18
+ /** 菜单步的候选项:已有 provider (no key 的标注)+ 未添加的预设 + 自定义 */
19
+ function wizardProviderItems() {
20
+ const items = []
21
+ for (const p of agent.providers) {
22
+ items.push({ kind: "existing", name: p.name, baseURL: p.baseURL, model: p.model, label: `${p.name} (added${p.apiKey ? "" : ", no key"})` })
23
+ }
24
+ for (const [name, p] of Object.entries(PRESETS)) {
25
+ if (!agent.providers.some((x) => x.name === name)) {
26
+ items.push({ kind: "preset", name, baseURL: p.baseURL, model: p.model, label: `${name} (${p.desc})` })
27
+ }
28
+ }
29
+ items.push({ kind: "custom", name: null, label: "Custom endpoint…" })
30
+ return items
31
+ }
32
+
33
+ /** 文本步骤定义:提示语 + 校验 (通过返回 true,否则返回错误文案) */
34
+ const WIZARD_STEPS = {
35
+ name: {
36
+ prompt: "Name this provider (alphanumeric/-/_ e.g. my-openai)",
37
+ validate: (v) =>
38
+ (/^[\w-]+$/.test(v) && !agent.providers.some((p) => p.name === v)) || "Name must be alphanumeric/-/_ and unique",
39
+ },
40
+ baseURL: {
41
+ prompt: "Enter baseURL (e.g. https://api.openai.com/v1)",
42
+ validate: (v) => /^https?:\/\/.+/.test(v) || "baseURL must start with http(s)://",
43
+ },
44
+ model: {
45
+ prompt: "Enter model name (e.g. gpt-4o)",
46
+ validate: (v) => v.length > 0 || "Model name required",
47
+ },
48
+ key: {
49
+ prompt: "Enter API key",
50
+ validate: (v) => v.length > 0 || "Key must not be empty",
51
+ },
52
+ embedkey: {
53
+ prompt: "Optional: embedding API key (SiliconFlow, for memory vector search; press Enter to skip)",
54
+ validate: () => true, // 可跳过
55
+ },
56
+ }
57
+ const WIZARD_NEXT = { name: "baseURL", baseURL: "model", model: "key", key: "embedkey", embedkey: null }
58
+
59
+ function startWizard() {
60
+ state.wizard = { step: "provider", index: 0, scroll: 0, selectedLine: 0, fields: {}, error: null, lines: [] }
61
+ renderWizard()
62
+ }
63
+
64
+ function renderWizard() {
65
+ const w = state.wizard
66
+ if (!w) return
67
+ const lines = []
68
+ if (w.step === "provider") {
69
+ lines.push({ text: " Choose a model provider:", color: C.text })
70
+ wizardProviderItems().forEach((it, i) => {
71
+ if (i === w.index) w.selectedLine = lines.length
72
+ lines.push({
73
+ text: `${i === w.index ? " ▸ " : " "}${it.label}`,
74
+ color: i === w.index ? ansi.bold + C.text : C.dim,
75
+ })
76
+ })
77
+ } else {
78
+ const f = w.fields
79
+ if (f.name) lines.push({ text: ` Provider: ${f.name}`, color: C.dim })
80
+ if (f.baseURL) lines.push({ text: ` baseURL: ${f.baseURL}`, color: C.dim })
81
+ if (f.model) lines.push({ text: ` Model: ${f.model}`, color: C.dim })
82
+ lines.push({ text: ` ❯ ${WIZARD_STEPS[w.step].prompt}`, color: ansi.bold + C.text })
83
+ lines.push({ text: " (type in input box below)", color: C.dim })
84
+ w.selectedLine = 0
85
+ }
86
+ if (w.error) lines.push({ text: ` ${w.error}`, color: C.error })
87
+ w.lines = lines
88
+ render()
89
+ }
90
+
91
+ function wizardChooseProvider(item) {
92
+ const w = state.wizard
93
+ if (item.kind === "custom") {
94
+ w.step = "name"
95
+ } else {
96
+ w.fields = { name: item.name, baseURL: item.baseURL, model: item.model }
97
+ w.step = "key"
98
+ }
99
+ renderWizard()
100
+ }
101
+
102
+ function wizardSubmitText() {
103
+ const w = state.wizard
104
+ const value = state.input.join("").trim()
105
+ const ok = WIZARD_STEPS[w.step].validate(value)
106
+ if (ok !== true) {
107
+ w.error = ok
108
+ renderWizard()
109
+ return
110
+ }
111
+ w.error = null
112
+ state.input = []
113
+ state.cursor = 0
114
+ w.fields[w.step === "key" ? "key" : w.step] = w.step === "baseURL" ? value.replace(/\/+$/, "") : value
115
+ const next = WIZARD_NEXT[w.step]
116
+ if (next) {
117
+ w.step = next
118
+ renderWizard()
119
+ } else {
120
+ finishWizard().catch((e) => pushLine(`[error] ${e.message}`, C.error))
121
+ }
122
+ }
123
+
124
+ function cancelWizard() {
125
+ state.wizard = null
126
+ pushLine("Skipped initial setup. Add a provider anytime via /provider add, set its key via /provider key.", C.dim)
127
+ render()
128
+ }
129
+
130
+ /** 向导完成:写入 provider (有则更新)、设为激活、持久化,然后接模型选择器 */
131
+ async function finishWizard() {
132
+ const f = state.wizard.fields
133
+ state.wizard = null
134
+ const existing = agent.providers.find((p) => p.name === f.name)
135
+ if (existing) Object.assign(existing, { baseURL: f.baseURL, model: f.model, apiKey: f.key })
136
+ else agent.providers.push({ name: f.name, baseURL: f.baseURL, model: f.model, apiKey: f.key })
137
+ agent.activeProvider = f.name
138
+ agent.provider = { ...agent.providers.find((p) => p.name === f.name) }
139
+ if (agent.config?.agent?.compactThresholdAuto) {
140
+ const { resolveCompactThreshold } = await import("../config.mjs")
141
+ agent.config.agent.compactThreshold = resolveCompactThreshold(null, f.model).value
142
+ }
143
+ await persistRaw((raw) => {
144
+ raw.providers = agent.providers
145
+ raw.activeProvider = f.name
146
+ })
147
+ agent.config.activeProvider = f.name
148
+ pushLabel(`❯ Setup`, ansi.bold + C.tool)
149
+ pushLine(`Setup complete: ${f.name} / ${f.model} (saved to config)`, C.tool)
150
+ // embedding key:配了就启用向量检索,没配提示事后通道
151
+ if (f.embedkey) {
152
+ agent.config.embedding ??= {}
153
+ agent.config.embedding.apiKey = f.embedkey
154
+ await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: f.embedkey } })
155
+ if (agent.memory && !agent.memory.embedder) {
156
+ const { createEmbedder } = await import("../embedding.mjs")
157
+ agent.memory.embedder = createEmbedder(agent.config.embedding)
158
+ }
159
+ pushLine(`Vector search enabled (${agent.config.embedding.model ?? "BAAI/bge-m3"})`, C.tool)
160
+ } else {
161
+ pushLine(`Vector search disabled (memory falls back to text-only search). Run /config embedkey <key> to enable.`, C.dim)
162
+ }
163
+ pushLine(`Select model (Esc to keep ${f.model})`, C.dim)
164
+ ctx.openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
165
+ }
166
+
167
+ return { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard }
168
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * tui-render.mjs — 终端显示工具(重新导出)
3
+ */
4
+ export { charWidth, stringWidth, sliceByWidth, formatTables, layoutInput, sanitizeDisplay, wrapText } from "./tui/render.mjs"