thincoder 0.8.10 → 0.8.12

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 (52) hide show
  1. package/README.md +16 -0
  2. package/bin/thincoder.mjs +115 -0
  3. package/package.json +1 -1
  4. package/src/advisor.mjs +105 -0
  5. package/src/agent/dispatch.mjs +35 -0
  6. package/src/agent/helpers.mjs +1 -1
  7. package/src/agent/setup.mjs +21 -7
  8. package/src/agent-tools/subagent.mjs +1 -1
  9. package/src/agent-tools/timer.mjs +41 -0
  10. package/src/agent-tools/verify.mjs +165 -56
  11. package/src/agent-tools.mjs +1 -0
  12. package/src/agent.mjs +128 -20
  13. package/src/auto-think.mjs +83 -0
  14. package/src/cli/make-agent.mjs +9 -0
  15. package/src/config.mjs +18 -18
  16. package/src/git/checkpoint.mjs +2 -1
  17. package/src/git/gitmem.mjs +8 -2
  18. package/src/markdown.mjs +1 -1
  19. package/src/mcp/transport-http.mjs +8 -2
  20. package/src/memory/code-index.mjs +2 -2
  21. package/src/memory/code-sync.mjs +92 -35
  22. package/src/memory/core.mjs +10 -1
  23. package/src/memory/docs.mjs +24 -26
  24. package/src/memory/schema.mjs +16 -3
  25. package/src/prompts/coder.md +7 -4
  26. package/src/prompts/discipline.md +55 -16
  27. package/src/prompts/main.md +15 -11
  28. package/src/prompts/system.md +33 -7
  29. package/src/provider/core.mjs +186 -20
  30. package/src/provider/index.mjs +1 -1
  31. package/src/rules.mjs +53 -0
  32. package/src/session.mjs +1 -1
  33. package/src/tools/checklist.md +7 -0
  34. package/src/tools/checklist.mjs +114 -0
  35. package/src/tools/file.mjs +82 -1
  36. package/src/tools/hashline_edit.md +12 -0
  37. package/src/tools/index.mjs +7 -3
  38. package/src/tools/linter.md +13 -0
  39. package/src/tools/linter.mjs +146 -0
  40. package/src/tools/read.md +3 -2
  41. package/src/tools/repomap.mjs +14 -9
  42. package/src/tui/agent-turn.mjs +9 -2
  43. package/src/tui/ansi.mjs +1 -0
  44. package/src/tui/cmd-advisor.mjs +68 -0
  45. package/src/tui/cmd-think.mjs +36 -10
  46. package/src/tui/index.mjs +2 -1
  47. package/src/tui/key-handler.mjs +36 -1
  48. package/src/tui/layout.mjs +3 -1
  49. package/src/tui/pickers.mjs +15 -15
  50. package/src/tui/render-frame.mjs +17 -8
  51. package/src/tui/slash-commands.mjs +3 -0
  52. package/src/tools/repomap-parse.mjs +0 -168
@@ -4,20 +4,26 @@
4
4
  export async function handleThinkCommand(ctx) {
5
5
  const { agent, openPicker, syncProviderField } = ctx
6
6
  const cur = agent.provider
7
- const thinkingEnabled = cur.thinking?.type === "enabled" || cur.thinking?.type === undefined
8
7
  const { specForModel } = await import("../config.mjs")
9
8
  const spec = specForModel(cur.model)
10
9
  const isEffortOnly = spec.thinkApi === "effort"
10
+ const thinkOnValue = spec.thinkOnValue ?? "enabled"
11
+ const isCustomThink = thinkOnValue !== "enabled"
12
+ // "enabled" when thinking.type matches the model's enabled value, or when thinking is absent and the model is NOT a custom-think model (defaults to on for standard models)
13
+ const thinkingEnabled = cur.thinking?.type === thinkOnValue || (cur.thinking?.type === undefined && !isCustomThink)
11
14
  const entries = []
15
+ // Auto-think: classify difficulty per-prompt and auto-set reasoning effort
16
+ const autoThinkEnabled = agent.config?.agent?.autoThink === true
17
+ entries.push({ type: "item", text: `Auto: ${autoThinkEnabled ? "ON" : "OFF"}`, action: "auto" })
12
18
  if (!isEffortOnly) {
13
- entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
19
+ if (!autoThinkEnabled) entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
14
20
  }
15
- if (spec.reasoningEffortEnum) {
21
+ if (spec.reasoningEffortEnum && !autoThinkEnabled) {
16
22
  for (const level of spec.reasoningEffortEnum) {
17
23
  const mark = cur.reasoningEffort === level ? "▸ " : " "
18
24
  entries.push({ type: "item", text: `${mark}effort: ${level}`, action: "effort", level })
19
25
  }
20
- } else {
26
+ } else if (!autoThinkEnabled) {
21
27
  entries.push({ type: "item", text: "effort: high", action: "effort", level: "high" })
22
28
  entries.push({ type: "item", text: "effort: max", action: "effort", level: "max" })
23
29
  }
@@ -25,7 +31,19 @@ export async function handleThinkCommand(ctx) {
25
31
  title: "Think",
26
32
  entries,
27
33
  onSelect: async (e) => {
28
- if (e.action === "effort") {
34
+ if (e.action === "auto") {
35
+ const cfg = agent.config.agent ??= {}
36
+ cfg.autoThink = !cfg.autoThink
37
+ agent._pendingReminders = agent._pendingReminders ?? []
38
+ if (cfg.autoThink) {
39
+ // Turn off manual effort — auto will set it per-turn
40
+ delete cur.reasoningEffort
41
+ await syncProviderField("reasoningEffort", undefined)
42
+ agent._pendingReminders.push("[System reminder: Auto-think is now ON. Reasoning effort will be automatically set per-task based on difficulty classification.]")
43
+ } else {
44
+ agent._pendingReminders.push("[System reminder: Auto-think is now OFF. Reasoning effort will remain at its current manual setting.]")
45
+ }
46
+ } else if (e.action === "effort") {
29
47
  cur.reasoningEffort = e.level
30
48
  await syncProviderField("reasoningEffort", e.level)
31
49
  } else {
@@ -36,12 +54,20 @@ export async function handleThinkCommand(ctx) {
36
54
  if (!enable) await syncProviderField("reasoningEffort", undefined)
37
55
  else await syncProviderField("reasoningEffort", cur.reasoningEffort)
38
56
  } else {
39
- cur.thinking = enable ? { type: "enabled" } : { type: "disabled" }
40
- if (!enable) delete cur.reasoningEffort
41
- else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
57
+ if (enable) {
58
+ cur.thinking = { type: thinkOnValue }
59
+ if (!cur.reasoningEffort) cur.reasoningEffort = "high"
60
+ } else {
61
+ // Custom-think models (MiniMax "adaptive") don't support "disabled" — remove the field instead
62
+ cur.thinking = isCustomThink ? undefined : { type: "disabled" }
63
+ delete cur.reasoningEffort
64
+ }
42
65
  await syncProviderField("thinking", cur.thinking)
43
- if (!enable) await syncProviderField("reasoningEffort", undefined)
44
- else await syncProviderField("reasoningEffort", cur.reasoningEffort)
66
+ if (enable) {
67
+ await syncProviderField("reasoningEffort", cur.reasoningEffort)
68
+ } else {
69
+ await syncProviderField("reasoningEffort", undefined)
70
+ }
45
71
  }
46
72
  }
47
73
  },
package/src/tui/index.mjs CHANGED
@@ -63,7 +63,7 @@ export async function startTUI(agent, opts = {}) {
63
63
  picker: null, // model picker { entries, lines, index, scroll, selectedLine }
64
64
  wizard: null, // first-launch config wizard { step, index, scroll, selectedLine, fields, error, lines }
65
65
  tasks: agent.tasks ?? [], // task list from task tool (progress shown in status bar); carried over on session restore, auto-collapsed when all done
66
- tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // cumulative token usage (shown in status bar)
66
+ tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0, reasoningTokens: 0 }, // cumulative token usage (shown in status bar)
67
67
  ctxCache: { len: -1, tokens: 0 }, // context utilization estimate cache (estimateTokens is O(n), only recompute when history grows)
68
68
  reasoning: "", // thinking stream buffer (dimmed display)
69
69
  completion: null, // Tab completion state { candidates, index }
@@ -74,6 +74,7 @@ export async function startTUI(agent, opts = {}) {
74
74
  processingStarted: 0, // current turn start time (status bar timer)
75
75
  status: "Ready",
76
76
  queue: [], // queued messages while processing: [{ text }], auto-dequeued when current turn finishes
77
+ interruptPrompt: null, // Ctrl+I interrupt message input: { text: "" } or null
77
78
  }
78
79
 
79
80
  // On session restore, if all tasks are completed, auto-collapse the todo panel (match runtime behavior)
@@ -91,7 +91,10 @@ export function createKeyHandler(ctx) {
91
91
  insertPastedText(state, text)
92
92
  render()
93
93
  }
94
- }).catch(() => { q._pasting = false })
94
+ }).catch((e) => {
95
+ q._pasting = false
96
+ console.error(`[tui] clipboard paste failed: ${e.message}`)
97
+ })
95
98
  } else if (str && !key.ctrl && !key.meta) {
96
99
  q.answer = (q.answer ?? "") + str
97
100
  render()
@@ -111,6 +114,38 @@ export function createKeyHandler(ctx) {
111
114
  setTimeout(() => process.exit(0), 100)
112
115
  }
113
116
 
117
+ // Ctrl+I: interrupt current generation and inject a message (time-travel inject)
118
+ if (key.ctrl && !key.alt && key.name === "i") {
119
+ if (state.processing && state.controller && !state.interruptPrompt) {
120
+ state.interruptPrompt = { text: "" }
121
+ render()
122
+ }
123
+ return
124
+ }
125
+
126
+ // Interrupt prompt mode: type message, Enter to inject, Esc to cancel
127
+ if (state.interruptPrompt) {
128
+ if (key.name === "escape") {
129
+ state.interruptPrompt = null
130
+ render()
131
+ } else if (key.name === "return") {
132
+ const msg = (state.interruptPrompt.text ?? "").trim()
133
+ state.interruptPrompt = null
134
+ if (msg) {
135
+ pushLine(` [inject] ${msg}`, C.warn)
136
+ state.controller.abort({ interrupt: true, message: msg })
137
+ render()
138
+ }
139
+ } else if (key.name === "backspace") {
140
+ state.interruptPrompt.text = state.interruptPrompt.text.slice(0, -1)
141
+ render()
142
+ } else if (str && !key.ctrl && !key.meta) {
143
+ state.interruptPrompt.text += str.replace(/[\r\n]+/g, "")
144
+ render()
145
+ }
146
+ return
147
+ }
148
+
114
149
  // generic list picker: ↑↓ move, Enter confirm, Esc cancel
115
150
  if (state.picker) {
116
151
  const items = state.picker?.entries.filter((e) => e.type === "item") ?? []
@@ -22,7 +22,9 @@ export function computeLayout(state, { cols, rows }) {
22
22
  const W = Math.max(20, cols - 1)
23
23
 
24
24
  // --- input box ---
25
- const inputLayout = layoutInput(state.input, state.cursor, W - 4)
25
+ const inputBuf = state.interruptPrompt ? [...state.interruptPrompt.text] : state.input
26
+ const inputCursor = state.interruptPrompt ? inputBuf.length : state.cursor
27
+ const inputLayout = layoutInput(inputBuf, inputCursor, W - 4)
26
28
  let inputOffset = 0
27
29
  if (inputLayout.lines.length > MAX_INPUT_LINES) {
28
30
  inputOffset = Math.min(inputLayout.cursorLine, inputLayout.lines.length - MAX_INPUT_LINES)
@@ -185,27 +185,27 @@ export function createPickers(ctx) {
185
185
  openPicker({
186
186
  title: "Add Provider",
187
187
  entries: presetEntries,
188
- onCancel: () => openModelPicker().catch(() => {}),
188
+ onCancel: () => openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)),
189
189
  onSelect: async (se) => {
190
190
  if (se.kind === "custom") {
191
191
  const name = await askQuestion("Enter provider name:")
192
- if (!name) { openModelPicker().catch(() => {}); return }
193
- if (agent.providers.some((p) => p.name === name)) { openModelPicker().catch(() => {}); return }
192
+ if (!name) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
193
+ if (agent.providers.some((p) => p.name === name)) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
194
194
  const baseURLRaw = await askQuestion("Enter baseURL (e.g. https://api.example.com/v1):")
195
- if (!baseURLRaw) { openModelPicker().catch(() => {}); return }
195
+ if (!baseURLRaw) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
196
196
  const baseURL = baseURLRaw.replace(/\/+$/, "")
197
- if (!/^https?:\/\//.test(baseURL)) { openModelPicker().catch(() => {}); return }
197
+ if (!/^https?:\/\//.test(baseURL)) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
198
198
  const model = await askQuestion("Enter model name:")
199
- if (!model) { openModelPicker().catch(() => {}); return }
199
+ if (!model) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
200
200
  agent.providers.push({ name, baseURL, model })
201
201
  await persistRaw((raw) => { raw.providers = agent.providers })
202
202
  const key = await askQuestion(`Enter API key for ${name} (leave empty to skip):`)
203
203
  if (key) { await setProviderKey(name, key) }
204
- openModelPicker().catch(() => {})
204
+ openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
205
205
  return
206
206
  }
207
207
  // preset
208
- if (agent.providers.some((p) => p.name === se.name)) { openModelPicker().catch(() => {}); return }
208
+ if (agent.providers.some((p) => p.name === se.name)) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
209
209
  const preset = PRESETS[se.name]
210
210
  const providerCfg = { name: se.name, baseURL: preset.baseURL, model: preset.model }
211
211
  if (preset.thinking) providerCfg.thinking = preset.thinking
@@ -217,7 +217,7 @@ export function createPickers(ctx) {
217
217
  await persistRaw((raw) => { raw.providers = agent.providers })
218
218
  const presetKey = await askQuestion(`Enter API key for ${se.name} (leave empty to skip):`)
219
219
  if (presetKey) await setProviderKey(se.name, presetKey)
220
- openModelPicker().catch(() => {})
220
+ openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
221
221
  },
222
222
  })
223
223
  }
@@ -225,7 +225,7 @@ export function createPickers(ctx) {
225
225
  /** Remove provider (cannot remove the currently active one) */
226
226
  async function removeProviderFlow() {
227
227
  const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
228
- if (candidates.length === 0) { openModelPicker().catch(() => {}); return }
228
+ if (candidates.length === 0) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
229
229
  const removeEntries = [
230
230
  { type: "header", text: "Select provider to remove (current one cannot be removed)" },
231
231
  ...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
@@ -233,12 +233,12 @@ export function createPickers(ctx) {
233
233
  openPicker({
234
234
  title: "Remove Provider",
235
235
  entries: removeEntries,
236
- onCancel: () => openModelPicker().catch(() => {}),
236
+ onCancel: () => openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)),
237
237
  onSelect: async (se) => {
238
238
  const at = agent.providers.findIndex((p) => p.name === se.name)
239
239
  agent.providers.splice(at, 1)
240
240
  await persistRaw((raw) => { raw.providers = agent.providers })
241
- openModelPicker().catch(() => {})
241
+ openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
242
242
  },
243
243
  })
244
244
  }
@@ -256,12 +256,12 @@ export function createPickers(ctx) {
256
256
  openPicker({
257
257
  title: "Configure API Key",
258
258
  entries: keyEntries,
259
- onCancel: () => openModelPicker().catch(() => {}),
259
+ onCancel: () => openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)),
260
260
  onSelect: async (se) => {
261
261
  const key = await askQuestion(`Enter API key for ${se.name}:`)
262
- if (!key) { openModelPicker().catch(() => {}); return }
262
+ if (!key) { openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error)); return }
263
263
  await setProviderKey(se.name, key)
264
- openModelPicker().catch(() => {})
264
+ openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
265
265
  },
266
266
  })
267
267
  }
@@ -40,8 +40,11 @@ export function renderFrame(state, agent, opts) {
40
40
  const thinking = agent.provider.thinking
41
41
  const effort = agent.provider.reasoningEffort
42
42
  const isMultimodal = specForModel(model).multimodal
43
+ const spec = specForModel(model)
44
+ const thinkOnValue = spec.thinkOnValue ?? "enabled"
43
45
  const thinkBadge = thinking?.type === "disabled" ? "│ think: off"
44
- : effort ? `│ think: ${effort}` : thinking?.type === "enabled" ? "│ think: on" : ""
46
+ : effort ? `│ think: ${effort}`
47
+ : thinking?.type === thinkOnValue ? "│ think: on" : ""
45
48
 
46
49
  const out = [ansi.home]
47
50
  let cursorRow = 0, cursorCol = 0
@@ -145,17 +148,19 @@ export function renderFrame(state, agent, opts) {
145
148
  // ---- queue preview ----
146
149
  if (panels.queue) {
147
150
  const preview = sliceByWidth(state.queue[0].text, W - 20)
148
- out.push(`${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D delete${ansi.reset}${ansi.clearLine}`)
151
+ out.push(`${C.dim}❯ Queue: ${state.queue.length} pending${state.queue.length > 1 ? ` (next: ${preview}…)` : ` (next: ${preview})`} — Ctrl+D delete │ Ctrl+I inject${ansi.reset}${ansi.clearLine}`)
149
152
  }
150
153
 
151
154
  // ---- input box ----
152
155
  const { borderColor, title } = inputBoxStyle(state)
153
156
  let topBorder
154
- if (title === " Input " || title === " Question ") {
157
+ if (title === " Input " || title === " Question " || title === " Inject Message ") {
155
158
  const parts = []
156
159
  if (title === " Input ") parts.push(" Ctrl+U clear ")
157
160
  if (title === " Question ") parts.push(" Enter submit ")
161
+ if (title === " Inject Message ") parts.push(" Enter send, Esc cancel ")
158
162
  parts.push(" Ctrl+V paste ")
163
+ parts.push(" Ctrl+I inject ")
159
164
  const hint = parts.join("")
160
165
  topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 4 - stringWidth(title) - stringWidth(hint)))}${hint}─╮`
161
166
  } else {
@@ -173,9 +178,10 @@ export function renderFrame(state, agent, opts) {
173
178
  const statusLine = buildStatusLine(state, agent, { cols, slashCommands })
174
179
  const autoBanner = agent.autoApprove ? `${C.warn} AUTO${ansi.reset}${ansi.dim}│` : ""
175
180
  const planBanner = agent.planMode ? `${C.tool} PLAN${ansi.reset}${ansi.dim}│` : ""
176
- const bannerPrefix = (agent.planMode ? " PLAN│ " : "") + (agent.autoApprove ? " AUTO│ " : "")
181
+ const advisorBanner = agent.config?.advisor?.enabled ? `${C.advisor} ADVISOR${ansi.reset}${ansi.dim}│` : ""
182
+ const bannerPrefix = (agent.planMode ? " PLAN│ " : "") + (agent.autoApprove ? " AUTO│ " : "") + (agent.config?.advisor?.enabled ? " ADVISOR│ " : "")
177
183
  const statusMax = cols - 1 - (bannerPrefix ? stringWidth(bannerPrefix) : 0)
178
- out.push(`${ansi.dim}${planBanner}${autoBanner}${sliceByWidth(statusLine, Math.max(10, statusMax))}${ansi.reset}${ansi.clearLine}`)
184
+ out.push(`${ansi.dim}${planBanner}${autoBanner}${advisorBanner}${sliceByWidth(statusLine, Math.max(10, statusMax))}${ansi.reset}${ansi.clearLine}`)
179
185
 
180
186
  const frame = out.join("\r\n")
181
187
 
@@ -239,7 +245,10 @@ function buildConvLines(state, cols) {
239
245
  function inputBoxStyle(state) {
240
246
  let borderColor = C.tool
241
247
  let title
242
- if (state.question) {
248
+ if (state.interruptPrompt) {
249
+ borderColor = C.warn
250
+ title = " Inject Message "
251
+ } else if (state.question) {
243
252
  borderColor = C.tool
244
253
  title = " Question "
245
254
  } else if (state.permission) {
@@ -308,7 +317,7 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
308
317
  const fmtK = (n) => (n >= 10000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`)
309
318
  const cacheTotal = tk.cacheHit + tk.cacheMiss
310
319
  const tokenHint = tk.prompt > 0
311
- ? ` │ ↑${fmtK(tk.prompt)} ↓${fmtK(tk.completion)}${cacheTotal > 0 ? ` hit${Math.round((tk.cacheHit / cacheTotal) * 100)}%` : ""}`
320
+ ? ` │ ↑${fmtK(tk.prompt)} ↓${fmtK(tk.completion)}${tk.reasoningTokens > 0 ? ` ✦${fmtK(tk.reasoningTokens)}` : ""}${cacheTotal > 0 ? ` hit${Math.round((tk.cacheHit / cacheTotal) * 100)}%` : ""}`
312
321
  : ""
313
322
  const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
314
323
  const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
@@ -321,7 +330,7 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
321
330
  : ` │ context ${ctxPct}%`
322
331
  : ""
323
332
  const queueHint = state.queue.length > 0 ? ` │ queue: ${state.queue.length}` : ""
324
- return ` ${statusText}${taskHint}${tokenHint}${ctxHint}${queueHint}${scrollHint} │ Enter: send${state.processing ? " (queue)" : ""} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+C: exit`
333
+ return ` ${statusText}${taskHint}${tokenHint}${ctxHint}${queueHint}${scrollHint} │ Enter: send${state.processing ? " (queue)" : ""} │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+I: inject │ Ctrl+C: exit`
325
334
  }
326
335
 
327
336
  /** Summarize tool args for subagent panel display (one line, short). Pure. */
@@ -21,6 +21,7 @@ import { handleGoalCommand } from "./cmd-goal.mjs"
21
21
  import { handleSkillsCommand } from "./cmd-skills.mjs"
22
22
  import { handleMcpCommand } from "./cmd-mcp.mjs"
23
23
  import { handleAutoCommand } from "./cmd-auto.mjs"
24
+ import { handleAdvisorCommand } from "./cmd-advisor.mjs"
24
25
  import { handleThinkCommand } from "./cmd-think.mjs"
25
26
  import { handleModelCommand } from "./cmd-model.mjs"
26
27
  import { handleConfigCommand } from "./cmd-config.mjs"
@@ -31,6 +32,7 @@ import { handleUpgradeCommand } from "./cmd-upgrade.mjs"
31
32
  export const SLASH_COMMANDS = [
32
33
  { name: "/plan", group: "Agent", desc: "toggle plan mode (design first, then implement)" },
33
34
  { name: "/auto", group: "Agent", desc: "toggle auto-approve" },
35
+ { name: "/advisor", group: "Agent", desc: "toggle advisor review & select model" },
34
36
  { name: "/model", group: "Agent", desc: "select model & manage providers" },
35
37
  { name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
36
38
  { name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
@@ -63,6 +65,7 @@ const HANDLERS = {
63
65
  "/skills": handleSkillsCommand,
64
66
  "/mcp": handleMcpCommand,
65
67
  "/auto": handleAutoCommand,
68
+ "/advisor": handleAdvisorCommand,
66
69
  "/think": handleThinkCommand,
67
70
  "/model": handleModelCommand,
68
71
  "/config": handleConfigCommand,
@@ -1,168 +0,0 @@
1
- /**
2
- * repomap-parse.mjs — repo dependency graph parser (zero dependencies, pure regex)
3
- * Gets known file list from code_chunks, parses each file's import/export relationships in real time,
4
- * builds forward dependency graph + reverse reference graph. Shared by repomap.mjs's buildSummary / buildOutline.
5
- */
6
- import { readFileSync, existsSync } from "node:fs"
7
- import { join } from "node:path"
8
-
9
- /**
10
- * Scan all files, build forward dependency graph + reverse reference graph.
11
- * Returns { deps, importers, fileCount } shared by buildOutline / buildSummary.
12
- */
13
- export function buildDepGraph(db, cwd) {
14
- const allFiles = db.prepare(`SELECT DISTINCT path FROM code_chunks ORDER BY path`).all().map((r) => r.path)
15
- if (allFiles.length === 0) return null
16
-
17
- const deps = new Map() // path → { imports: Set, exports: Set, size: number, dir: string }
18
- const importers = new Map() // importee → Set<importer>
19
-
20
- for (const rel of allFiles) {
21
- const abs = join(cwd, ...rel.split("/"))
22
- if (!existsSync(abs)) continue
23
- const text = readFileSync(abs, "utf8")
24
- const lines = text.split("\n")
25
- const ext = rel.slice(rel.lastIndexOf(".")).toLowerCase()
26
-
27
- let imports, exports
28
- if (ext === ".py") {
29
- const py = parsePyOutline(lines)
30
- imports = py.imports
31
- exports = py.symbols
32
- } else {
33
- imports = parseImports(lines, ext)
34
- exports = parseExports(lines, ext)
35
- }
36
-
37
- // Resolve import paths to relative paths (handle ./ ../)
38
- const resolved = []
39
- for (let imp of imports) {
40
- if (imp.startsWith("./")) imp = imp.slice(2)
41
- const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : ""
42
- const parts = imp.split("/")
43
- if (parts[0] === "..") {
44
- const up = dir.split("/").filter(Boolean)
45
- let i = 0
46
- while (parts[i] === ".." && up.length > 0) { up.pop(); i++ }
47
- resolved.push([...up, ...parts.slice(i)].join("/"))
48
- } else {
49
- resolved.push(dir ? `${dir}/${imp}` : imp)
50
- }
51
- }
52
-
53
- const dir = rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "."
54
- deps.set(rel, { imports: new Set(resolved), exports: new Set(exports), size: Math.floor(text.length / 1024), dir })
55
-
56
- for (const r of resolved) {
57
- if (!importers.has(r)) importers.set(r, new Set())
58
- importers.get(r).add(rel)
59
- }
60
- }
61
-
62
- return { deps, importers, fileCount: allFiles.length }
63
- }
64
-
65
- // ---------------------------------------------------------- internal implementation
66
-
67
- function normalizeExt(p) {
68
- return p.replace(/\.(m?js|jsx|tsx?)$/i, "")
69
- }
70
-
71
- /** Extract JS/TS file import paths (normalize by stripping .ts/.js/.mjs suffixes) */
72
- function parseImports(lines, ext) {
73
- const imports = []
74
- const text = lines.join("\n")
75
- // standard import
76
- const re = /import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+\s*,?\s*(?:{[^}]*})?)\s*from\s*['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g
77
- let m
78
- while ((m = re.exec(text))) {
79
- const raw = m[1] || m[2]
80
- if (!raw || raw.startsWith("node:") || !raw.startsWith(".")) continue
81
- imports.push(normalizeExt(raw))
82
- }
83
- // re-export: export { x } from './module'
84
- const reExportRe = /export\s*\{[^}]*\}\s*from\s*['"]([^'"]+)['"]/g
85
- while ((m = reExportRe.exec(text))) {
86
- const raw = m[1]
87
- if (!raw || raw.startsWith("node:") || !raw.startsWith(".")) continue
88
- imports.push(normalizeExt(raw))
89
- }
90
- return [...new Set(imports)]
91
- }
92
-
93
- /** Extract JS/TS file export symbols */
94
- function parseExports(lines, ext) {
95
- const exports = []
96
- const text = lines.join("\n")
97
- // export function/class/const/let/var name
98
- const namedRe = /export\s+(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+)|(?:const|let|var)\s+(\w+))/g
99
- let m
100
- while ((m = namedRe.exec(text))) {
101
- exports.push(m[1] || m[2] || m[3])
102
- }
103
- // export default function/class name / export default expression
104
- const defaultRe = /export\s+default\s+(?:(?:async\s+)?(?:function\s+(\w+)|class\s+(\w+))|(\w+))/g
105
- while ((m = defaultRe.exec(text))) {
106
- const name = m[1] || m[2] || m[3]
107
- if (name) exports.push(name)
108
- else if (!exports.some((e) => e === "default")) exports.push("default")
109
- }
110
- // export { a, b as c } — prefer the "as" alias as the exported name
111
- const braceRe = /export\s*\{([^}]+)\}/g
112
- while ((m = braceRe.exec(text))) {
113
- for (const name of m[1].split(",")) {
114
- const parts = name.trim().split(/\s+/)
115
- // "a as b" → b (exported name), "a" → a
116
- const exported = parts.length >= 3 ? parts[2] : parts[0]
117
- if (exported) exports.push(exported)
118
- }
119
- }
120
- // export const { a, b } = ... (destructured export)
121
- const destructRe = /export\s+(?:const|let|var)\s*\{([^}]+)\}\s*=/g
122
- while ((m = destructRe.exec(text))) {
123
- for (const name of m[1].split(",")) {
124
- const parts = name.trim().split(/\s*:\s*/)
125
- const n = parts[0].trim()
126
- if (n) exports.push(n)
127
- }
128
- }
129
- return [...new Set(exports)]
130
- }
131
-
132
- /** Extract Python imports and top-level def/class */
133
- function parsePyOutline(lines) {
134
- const imports = []
135
- const symbols = []
136
- for (const line of lines) {
137
- const fromRe = line.match(/^from\s+(\S+)\s+import\s+(.+)/)
138
- if (fromRe) {
139
- const rel = pyRelPath(fromRe[1])
140
- if (rel) imports.push(rel)
141
- continue
142
- }
143
- const impRe = line.match(/^import\s+(.+)/)
144
- if (impRe) {
145
- for (const mod of impRe[1].split(",")) {
146
- const rel = pyRelPath(mod.trim().split(/\s+/)[0])
147
- if (rel) imports.push(rel)
148
- }
149
- continue
150
- }
151
- const defRe = line.match(/^(?:async\s+)?(?:def|class)\s+(\w+)/)
152
- if (defRe) symbols.push(defRe[1])
153
- }
154
- return { imports: [...new Set(imports)], symbols: [...new Set(symbols)] }
155
- }
156
-
157
- /**
158
- * Python relative import → relative file path:
159
- * Leading n dots mean go up n-1 levels ("." = current package), module dots become path separators.
160
- * Non-relative imports (not starting with .) or bare package imports ("from . import x") return null.
161
- */
162
- function pyRelPath(mod) {
163
- if (!mod?.startsWith(".")) return null
164
- const dots = mod.match(/^\.+/)[0].length
165
- const rest = mod.slice(dots).replaceAll(".", "/")
166
- if (!rest) return null
167
- return normalizeExt("../".repeat(dots - 1) + rest)
168
- }