thincoder 0.10.0 → 0.11.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 (64) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/advisor.mjs +360 -72
  4. package/src/agent/helpers.mjs +7 -3
  5. package/src/agent/setup.mjs +2 -2
  6. package/src/agent-tools/advisor.mjs +36 -0
  7. package/src/agent-tools/plan.mjs +53 -2
  8. package/src/agent-tools/subagent.mjs +7 -1
  9. package/src/agent-tools/timer.mjs +1 -1
  10. package/src/agent-tools/verify.mjs +1 -0
  11. package/src/agent-tools.mjs +1 -0
  12. package/src/agent.mjs +73 -21
  13. package/src/auto-think.mjs +23 -5
  14. package/src/cli/make-agent.mjs +7 -0
  15. package/src/config.mjs +47 -20
  16. package/src/prompts/advisor-round1.md +23 -0
  17. package/src/prompts/advisor-round2.md +26 -0
  18. package/src/prompts/advisor-round3.md +24 -0
  19. package/src/prompts/coder.md +6 -2
  20. package/src/prompts/discipline.md +23 -6
  21. package/src/prompts/explore.md +2 -0
  22. package/src/prompts/plan.md +2 -0
  23. package/src/prompts/system.md +13 -6
  24. package/src/provider/anthropic.mjs +190 -0
  25. package/src/provider/core.mjs +42 -130
  26. package/src/provider/google.mjs +199 -0
  27. package/src/provider/sse.mjs +112 -0
  28. package/src/proxy.mjs +236 -0
  29. package/src/tools/bash.md +8 -0
  30. package/src/tools/codemode.mjs +5 -16
  31. package/src/tools/edit.md +8 -0
  32. package/src/tools/fetch.md +2 -1
  33. package/src/tools/git.mjs +125 -156
  34. package/src/tools/index.mjs +9 -9
  35. package/src/tools/linter.mjs +46 -32
  36. package/src/tools/read.md +7 -0
  37. package/src/tools/shared.mjs +16 -0
  38. package/src/tools/system.mjs +14 -10
  39. package/src/tools/web.mjs +115 -89
  40. package/src/tools/websearch.md +5 -3
  41. package/src/tui/agent-turn.mjs +86 -75
  42. package/src/tui/cmd-advisor.mjs +138 -49
  43. package/src/tui/cmd-clear.mjs +11 -17
  44. package/src/tui/cmd-config.mjs +226 -142
  45. package/src/tui/cmd-extract.mjs +1 -1
  46. package/src/tui/cmd-fold.mjs +2 -3
  47. package/src/tui/cmd-goal.mjs +58 -27
  48. package/src/tui/cmd-help.mjs +3 -1
  49. package/src/tui/cmd-mcp.mjs +178 -142
  50. package/src/tui/cmd-model.mjs +15 -4
  51. package/src/tui/cmd-new.mjs +7 -13
  52. package/src/tui/cmd-restore.mjs +12 -16
  53. package/src/tui/cmd-session.mjs +28 -32
  54. package/src/tui/cmd-think.mjs +75 -50
  55. package/src/tui/cmd-undo.mjs +19 -23
  56. package/src/tui/cmd-upgrade.mjs +22 -26
  57. package/src/tui/index.mjs +61 -215
  58. package/src/tui/key-handler.mjs +56 -20
  59. package/src/tui/layout.mjs +13 -3
  60. package/src/tui/pickers.mjs +151 -182
  61. package/src/tui/render-conversation.mjs +92 -0
  62. package/src/tui/render-frame.mjs +56 -114
  63. package/src/tui/render-loop.mjs +181 -0
  64. package/src/tui/slash-commands.mjs +26 -16
@@ -1,68 +1,157 @@
1
- /** /advisor command: toggle advisor on/off, select model.
2
- * ctx: { agent, openPicker, pushLine } */
1
+ /** /advisor command: toggle advisor on/off, select model, configure thinking.
2
+ * ctx: { agent, showPicker, pushLine, persistRaw } */
3
3
  import { C } from "./ansi.mjs"
4
4
 
5
5
  export async function handleAdvisorCommand(ctx) {
6
- const { agent, openPicker, pushLine } = ctx
6
+ const { agent, showPicker, pushLine } = ctx
7
7
  const cfg = agent.config.advisor ??= {}
8
8
  const enabled = cfg.enabled === true
9
- const curProvider = cfg.provider || agent.activeProvider
9
+ const curProvider = cfg.provider || "(main)"
10
10
  const curModel = cfg.model || agent.provider.model
11
+ const thinkInfo = cfg.thinking === null ? "off"
12
+ : cfg.thinking?.type === "disabled" ? "off"
13
+ : cfg.reasoningEffort ? `on (${cfg.reasoningEffort})`
14
+ : cfg.thinking ? `on (${cfg.thinking.type})` : "(main)"
11
15
 
12
16
  const entries = [
13
17
  { type: "item", text: `Advisor: ${enabled ? "ON" : "OFF"}`, action: "toggle" },
14
- { type: "item", text: `Model: ${curProvider}/${curModel}`, action: "model" },
18
+ { type: "item", text: `Model: ${curModel}`, action: "model", note: `Provider: ${curProvider}` },
19
+ { type: "item", text: `Thinking: ${thinkInfo}`, action: "thinking" },
15
20
  ]
16
21
 
17
- openPicker({
18
- title: "Advisor",
19
- entries,
20
- onSelect: async (e) => {
21
- if (e.action === "toggle") {
22
- cfg.enabled = !cfg.enabled
23
- agent._pendingReminders = agent._pendingReminders ?? []
24
- if (cfg.enabled) {
25
- agent._pendingReminders.push("[系统提醒: Advisor 审查已开启。每轮操作后,你的输出将被审查,观察结果可能作为系统提醒注入。请批判性参考——这是观察,不是命令。]")
26
- } else {
27
- agent._pendingReminders.push("[系统提醒: Advisor 审查已关闭。后续轮次不再自动审查。]")
28
- }
29
- } else if (e.action === "model") {
30
- await openAdvisorModelPicker(ctx).catch(err => pushLine(`[error] ${err.message}`, C.error))
31
- }
32
- },
33
- })
22
+ const e = await showPicker("Advisor", entries)
23
+ if (!e) return
24
+
25
+ const persist = async () => {
26
+ if (ctx.persistRaw) {
27
+ await ctx.persistRaw((raw) => {
28
+ raw.agent ??= {}
29
+ raw.agent.advisor = cfg
30
+ })
31
+ }
32
+ }
33
+
34
+ if (e.action === "toggle") {
35
+ cfg.enabled = !cfg.enabled
36
+ agent._pendingReminders = agent._pendingReminders ?? []
37
+ if (cfg.enabled) {
38
+ agent._pendingReminders.push("[System reminder: Advisor review is now ON. You can call the `advisor` tool to get an independent code review before finalising your work. The advisor is an independent read-only sub-agent that explores the codebase, runs git diff, reads files, and traces callers via grep/lsp.]")
39
+ } else {
40
+ agent._pendingReminders.push("[System reminder: Advisor review is now OFF. The `advisor` tool will not produce results.]")
41
+ }
42
+ await persist().catch(err => pushLine(`[error] Advisor toggle: ${err.message}`, C.error))
43
+ } else if (e.action === "model") {
44
+ await openAdvisorModelPicker(ctx, persist).catch(err => pushLine(`[error] ${err.message}`, C.error))
45
+ } else if (e.action === "thinking") {
46
+ await openAdvisorThinkingPicker(ctx, persist).catch(err => pushLine(`[error] ${err.message}`, C.error))
47
+ }
34
48
  }
35
49
 
36
- async function openAdvisorModelPicker(ctx) {
37
- const { agent, openPicker, pushLine } = ctx
50
+ async function openAdvisorModelPicker(ctx, persist) {
51
+ const { agent, showPicker, pushLine } = ctx
38
52
  const providers = agent.providers || []
53
+ const cfg = agent.config.advisor ??= {}
39
54
 
40
- // Build flat list: each provider's name + a "use current model" entry
41
- const entries = []
42
- let idx = 0
55
+ const entries = [
56
+ { type: "item", text: "Use main model", action: "inherit", marker: !cfg.provider ? "●" : "" },
57
+ ]
43
58
  for (const p of providers) {
44
- const mark = p.name === agent.activeProvider ? "* " : " "
45
- entries.push({ type: "item", text: `${mark}${p.name} ${p.baseURL}`, action: "set_provider", provider: p.name, model: p.model })
46
- idx++
59
+ entries.push({ type: "header", text: p.name, note: `${p.baseURL}${agent.activeProvider === p.name ? " ← active" : ""} loading…` })
60
+ const mark = cfg.provider === p.name && cfg.model === p.model ? "" : " "
61
+ entries.push({ type: "item", text: `${mark}${p.model}`, action: "switch", provider: p.name, model: p.model })
47
62
  }
48
63
 
49
- openPicker({
50
- title: "Advisor Model",
51
- entries,
52
- onSelect: async (e) => {
53
- if (e.action === "set_provider") {
54
- const cfg = agent.config.advisor ??= {}
55
- if (e.provider === agent.activeProvider && e.model === agent.provider.model) {
56
- // Same as main — clear override (use main pool)
57
- delete cfg.provider
58
- delete cfg.model
59
- pushLine(`Advisor: 使用主模型 (${agent.activeProvider}/${agent.provider.model})`, C.dim)
60
- } else {
61
- cfg.provider = e.provider
62
- cfg.model = e.model
63
- pushLine(`Advisor: ${e.provider}/${e.model}`, C.dim)
64
- }
65
- }
66
- },
67
- })
64
+ // Fetch models first, then show picker
65
+ await fetchAdvisorModels(entries, providers, agent)
66
+
67
+ const e = await showPicker("Advisor Model", entries)
68
+ if (!e) return
69
+
70
+ if (e.action === "inherit") {
71
+ delete cfg.provider
72
+ delete cfg.model
73
+ pushLine("Advisor: using main model", C.dim)
74
+ } else if (e.action === "switch") {
75
+ cfg.provider = e.provider
76
+ cfg.model = e.model
77
+ pushLine(`Advisor: ${e.provider}/${e.model}`, C.dim)
78
+ }
79
+ await persist()
80
+ }
81
+
82
+ async function fetchAdvisorModels(entries, providers, agent) {
83
+ const { listModels } = await import("../provider/index.mjs")
84
+ await Promise.all(providers.map(async (p) => {
85
+ try {
86
+ const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[p.name]
87
+ let apiKey = p.apiKey
88
+ if (!apiKey && envKey && process.env[envKey]) apiKey = process.env[envKey]
89
+ if (!apiKey) apiKey = process.env.THINCODER_API_KEY
90
+ const models = await listModels({ baseURL: p.baseURL, apiKey: apiKey ?? "" }, { signal: AbortSignal.timeout(10000) })
91
+ const at = entries.findLastIndex((e) => e.type === "header" && e.text === p.name)
92
+ if (at < 0) return
93
+ entries.splice(at + 2, 0, ...models
94
+ .filter((m) => m !== p.model)
95
+ .map((m) => ({ type: "item", text: ` ${m}`, action: "switch", provider: p.name, model: m })))
96
+ const header = entries[at]
97
+ header.note = `${p.baseURL}${p.apiKey ? "" : " (no key)"}${agent.activeProvider === p.name ? " ← active" : ""}`
98
+ } catch (err) {
99
+ const header = entries.find((e) => e.type === "header" && e.text === p.name)
100
+ if (header) header.note = `${p.baseURL}${p.apiKey ? "" : " (no key)"}${agent.activeProvider === p.name ? " ← active" : ""} (fetch failed: ${err.message.slice(0, 40)})`
101
+ }
102
+ }))
103
+ }
104
+
105
+ async function openAdvisorThinkingPicker(ctx, persist) {
106
+ const { agent, showPicker, pushLine } = ctx
107
+ const { specForModel } = await import("../config.mjs")
108
+ const cfg = agent.config.advisor ??= {}
109
+
110
+ const providerForDefaults = cfg.provider
111
+ ? agent.providers?.find(p => p.name === cfg.provider) || agent.provider
112
+ : agent.provider
113
+ const effectiveModel = cfg.model || providerForDefaults.model
114
+ const spec = specForModel(effectiveModel)
115
+ const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
116
+ const isCustomThink = thinkOnValue !== "enabled"
117
+ const isEffortOnly = spec.thinkApi === "effort"
118
+ const effortLevels = spec.reasoningEffortEnum ?? ["high", "max"]
119
+
120
+ const curEffort = cfg.reasoningEffort ?? providerForDefaults.reasoningEffort
121
+ const curThinking = cfg.thinking ?? providerForDefaults.thinking
122
+ const thinkingEnabled = curThinking?.type === thinkOnValue
123
+ || (curThinking?.type === undefined && !isCustomThink)
124
+
125
+ const entries = [
126
+ { type: "item", text: "Use main model settings", action: "inherit" },
127
+ ]
128
+ if (!isEffortOnly) {
129
+ entries.push({ type: "header", text: "Thinking mode" })
130
+ entries.push({ type: "item", text: `Enabled ${thinkingEnabled ? "← current" : ""}`, action: "think_on" })
131
+ entries.push({ type: "item", text: `Disabled ${curThinking?.type === "disabled" || curThinking === null ? "← current" : ""}`, action: "think_off" })
132
+ }
133
+ entries.push({ type: "header", text: "Reasoning effort" })
134
+ for (const level of effortLevels) {
135
+ entries.push({ type: "item", text: `${level} ${curEffort === level ? "← current" : ""}`, action: `effort_${level}` })
136
+ }
137
+
138
+ const e = await showPicker("Advisor Thinking", entries)
139
+ if (!e) return
140
+
141
+ if (e.action === "inherit") {
142
+ delete cfg.thinking
143
+ delete cfg.reasoningEffort
144
+ pushLine("Advisor: using main model thinking settings", C.dim)
145
+ } else if (e.action === "think_on") {
146
+ cfg.thinking = { type: thinkOnValue }
147
+ if (isEffortOnly) delete cfg.thinking
148
+ pushLine(`Advisor: thinking ON (${thinkOnValue})`, C.dim)
149
+ } else if (e.action === "think_off") {
150
+ cfg.thinking = isCustomThink ? null : { type: "disabled" }
151
+ pushLine("Advisor: thinking OFF", C.dim)
152
+ } else if (e.action.startsWith("effort_")) {
153
+ cfg.reasoningEffort = e.action.slice(7)
154
+ pushLine(`Advisor: reasoning effort = ${cfg.reasoningEffort}`, C.dim)
155
+ }
156
+ await persist()
68
157
  }
@@ -1,23 +1,17 @@
1
1
  /** /clear command: clear screen (confirm to prevent accidental trigger).
2
- * ctx: { state, openPicker, render } */
2
+ * ctx: { state, showPicker, render } */
3
3
  export async function handleClearCommand(ctx) {
4
- const { state, openPicker, render } = ctx
4
+ const { state, showPicker, render } = ctx
5
5
  if (state.lines.length > 0) {
6
- openPicker({
7
- title: "Clear screen?",
8
- entries: [
9
- { type: "item", text: "Yes, clear all conversation output", action: "yes" },
10
- { type: "item", text: "Cancel", action: "no" },
11
- ],
12
- defaultIndex: 1,
13
- onSelect: (e) => {
14
- if (e.action === "yes") {
15
- state.lines = []
16
- state.streaming = ""
17
- render()
18
- }
19
- },
20
- })
6
+ const e = await showPicker("Clear screen?", [
7
+ { type: "item", text: "Yes, clear all conversation output", action: "yes" },
8
+ { type: "item", text: "Cancel", action: "no" },
9
+ ], { defaultIndex: 1 })
10
+ if (e?.action === "yes") {
11
+ state.lines = []
12
+ state.streaming = ""
13
+ render()
14
+ }
21
15
  return
22
16
  }
23
17
  state.lines = []
@@ -1,157 +1,241 @@
1
1
  import { existsSync, readFileSync } from "node:fs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
 
4
- /** /config command: view and set agent/embedding config.
5
- * Extracted from slash-commands.mjs.
6
- * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey, ansi, C } */
7
- export async function handleConfigCommand(ctx) {
8
- const { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey } = ctx
4
+ /** /config command: view and set agent/embedding/proxy config. */
5
+ export async function handleConfigCommand(ctx, args = []) {
6
+ const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey } = ctx
9
7
  const { configPath } = await import("../config.mjs")
10
8
  const ac = agent.config?.agent ?? {}
11
9
  const ec = agent.config?.embedding ?? {}
12
10
 
13
- function cfgSummary() {
14
- const tn = `${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`
15
- const vg = ac.verifyGuard === true ? "on" : "off"
16
- return `agent.maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn} | verifyGuard=${vg} | embedding=${agent.memory?.embedder ? "on" : "off"}`
11
+ // agent.config.proxy 已被 loadConfig 归一化为 { uri, web, model } | undefined
12
+ function proxySummary() {
13
+ const pc = agent.config?.proxy
14
+ if (!pc) return "not configured"
15
+ return `${pc.uri} web:${pc.web ? "on" : "off"} model:${pc.model ? "on" : "off"}`
17
16
  }
18
17
 
19
- const mainEntries = [
20
- { type: "header", text: `Config: ${cfgSummary()}` },
21
- { type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 100}`, action: "agent.maxTurns" },
22
- { type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
23
- { type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
24
- { type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
25
- { type: "item", text: "Set embedding API key", action: "embedkey" },
26
- { type: "item", text: `embedding.model = ${ec.model ?? "BAAI/bge-m3"}`, action: "embedding.model" },
27
- { type: "item", text: "View full config", action: "view" },
28
- ]
29
-
30
- openPicker({
31
- title: "Config",
32
- entries: mainEntries,
33
- onSelect: async (e) => {
34
- if (e.action === "view") {
35
- const cp = configPath
36
- pushLabel(`❯ Config`, ansi.bold + C.tool)
37
- pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
38
- pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
39
- pushLine(`agent.maxTurns: ${ac.maxTurns ?? 100}`, C.dim)
40
- pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
41
- pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
42
- pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
43
- pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
44
- pushLine(`Config file: ${cp}`, C.dim)
45
- return
46
- }
47
- if (e.action === "embedkey") {
48
- const embKey = await askQuestion("Enter embedding API key (default: SiliconFlow bge-m3):")
49
- if (!embKey) return
50
- agent.config.embedding ??= {}
51
- agent.config.embedding.apiKey = embKey
52
- await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: embKey } })
53
- if (agent.memory) {
54
- const { createEmbedder } = await import("../embedding.mjs")
55
- agent.memory.embedder = createEmbedder(agent.config.embedding)
18
+ async function setEmbedKey() {
19
+ const embKey = await askQuestion("Enter embedding API key (default: SiliconFlow bge-m3):")
20
+ if (!embKey) return false
21
+ agent.config.embedding ??= {}
22
+ agent.config.embedding.apiKey = embKey
23
+ await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: embKey } })
24
+ if (agent.memory) {
25
+ const { createEmbedder } = await import("../embedding.mjs")
26
+ agent.memory.embedder = createEmbedder(agent.config.embedding)
27
+ }
28
+ pushLabel("❯ Config", ansi.bold + C.tool)
29
+ pushLine("Embedding key saved, vector search enabled", C.tool)
30
+ return true
31
+ }
32
+
33
+ /** 保存后的公共重载:loadConfig injectProxy → 恢复 provider 选择。
34
+ * 运行时 /model 切过 provider(未落盘)时保持它,不回滚到磁盘值。 */
35
+ async function reloadConfig() {
36
+ const { loadConfig } = await import("../config.mjs")
37
+ const { injectProxy } = await import("../proxy.mjs")
38
+ const cfg = loadConfig()
39
+ injectProxy(cfg.providersList, cfg)
40
+ const runtimeName = agent.activeProvider
41
+ agent.providers = cfg.providersList
42
+ agent.config = cfg
43
+ agent.config.agent ??= {}
44
+ const keep = cfg.providersList.find((p) => p.name === runtimeName)
45
+ if (runtimeName && runtimeName !== cfg.activeProvider && keep) {
46
+ // 运行时选择在新配置里仍存在 保持(provider 为注入 proxyUri 后的新对象)
47
+ agent.activeProvider = runtimeName
48
+ agent.provider = { ...keep }
49
+ } else {
50
+ agent.activeProvider = cfg.activeProvider
51
+ agent.provider = cfg.provider
52
+ agent.provider.proxyUri = cfg.providersList.find((p) => p.name === cfg.activeProvider)?.proxyUri
53
+ }
54
+ }
55
+
56
+ /** 保存 config(mutate 改 raw)→ reloadConfig(provider 代理无需重启即生效) */
57
+ async function saveProxy(mutate) {
58
+ const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
59
+ mutate(raw)
60
+ const { saveConfig } = await import("../config.mjs")
61
+ saveConfig(raw)
62
+ await reloadConfig()
63
+ }
64
+
65
+ // ── Proxy sub-menu loop:每轮重建 entries 显示最新状态,defaultIndex 记住上次位置 ──
66
+ async function proxyMenu() {
67
+ let proxyIdx = 0
68
+ for (;;) {
69
+ const pc = agent.config?.proxy // 已归一化 { uri, web, model } | undefined
70
+ const entries = [
71
+ { type: "header", text: `Proxy: ${pc?.uri || "(not set)"}` },
72
+ { type: "item", text: "Set proxy URI…", action: "seturi" },
73
+ { type: "item", text: `Web tools (fetch/websearch): ${!pc || pc.web ? "ON" : "OFF"}`, action: "toggleweb" },
74
+ { type: "item", text: `Model requests (providers with proxy:true): ${pc?.model ? "ON" : "OFF"}`, action: "togglemodel" },
75
+ { type: "item", text: "Test connection", action: "test" },
76
+ { type: "item", text: "Clear proxy", action: "clear" },
77
+ ]
78
+ const c = await showPicker("Proxy", entries, { defaultIndex: proxyIdx })
79
+ if (!c) return // Esc 返回主菜单
80
+ proxyIdx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(c))
81
+
82
+ try {
83
+ if (c.action === "seturi") {
84
+ const newUri = await askQuestion("Proxy URI (e.g. http://127.0.0.1:7890):")
85
+ if (!newUri) continue // 空输入不改动
86
+ // web 默认 true、保留原 model 值(对象形态);旧 string 形态升级为规范对象
87
+ await saveProxy((raw) => {
88
+ raw.proxy = raw.proxy && typeof raw.proxy === "object" && !Array.isArray(raw.proxy)
89
+ ? { ...raw.proxy, uri: newUri }
90
+ : { uri: newUri, web: true, model: false }
91
+ })
92
+ pushLabel("❯ Config", ansi.bold + C.tool)
93
+ pushLine(`proxy.uri = ${newUri}`, C.tool)
94
+ } else if (c.action === "toggleweb" || c.action === "togglemodel") {
95
+ if (!pc) { pushLine("Proxy URI not set — use Set proxy URI… first", C.error); continue }
96
+ const key = c.action === "toggleweb" ? "web" : "model"
97
+ await saveProxy((raw) => { raw.proxy = { ...pc, [key]: !pc[key] } })
98
+ pushLabel("❯ Config", ansi.bold + C.tool)
99
+ pushLine(`proxy.${key} = ${!pc[key] ? "on" : "off"}`, C.tool)
100
+ } else if (c.action === "test") {
101
+ const { proxyFetch, resolveWebProxy } = await import("../proxy.mjs")
102
+ const { UA } = await import("../tools/web.mjs")
103
+ const uri = resolveWebProxy({ agent })
104
+ pushLabel("❯ Config", ansi.bold + C.tool)
105
+ pushLine(`Testing ${uri ? `via proxy ${uri}` : "direct (no proxy)"}...`, C.dim)
106
+ try {
107
+ const res = await Promise.race([
108
+ proxyFetch("https://www.gstatic.com/generate_204", { headers: { "User-Agent": UA } }, uri),
109
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 5s")), 5000)),
110
+ ])
111
+ if (res.ok) pushLine(`✓ OK (HTTP ${res.status})`, C.tool)
112
+ else pushLine(`✗ HTTP ${res.status}`, C.error)
113
+ } catch (error) {
114
+ pushLine(`✗ ${error.message}`, C.error)
115
+ }
116
+ } else if (c.action === "clear") {
117
+ await saveProxy((raw) => { delete raw.proxy })
118
+ pushLabel("❯ Config", ansi.bold + C.tool)
119
+ pushLine("Proxy cleared", C.tool)
56
120
  }
57
- pushLabel(`❯ Config`, ansi.bold + C.tool)
58
- pushLine(`Embedding key saved, vector search enabled`, C.tool)
59
- return
60
- }
61
- // Boolean toggle: agent.verifyGuard
62
- if (e.action === "agent.verifyGuard") {
63
- const newVal = ac.verifyGuard !== true // toggle: undefined/false → true, true → false
64
- try {
65
- const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
121
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
122
+ }
123
+ }
124
+
125
+ // Direct args: /config embedkey
126
+ const sub = args[0]?.toLowerCase()
127
+ if (sub === "embedkey") {
128
+ await setEmbedKey()
129
+ return
130
+ }
131
+ if (sub) { pushLine("Usage: /config [embedkey]", C.error); return }
132
+
133
+ // ── Main config loop ──
134
+ let running = true
135
+ let mainIdx = 0 // 记住上次选中位置,改完一项回主菜单时恢复
136
+ while (running) {
137
+ const mainEntries = [
138
+ { type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | embedding=${agent.memory?.embedder ? "on" : "off"}` },
139
+ { type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 100}`, action: "agent.maxTurns" },
140
+ { type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
141
+ { type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
142
+ { type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
143
+ { type: "item", text: "Set embedding API key", action: "embedkey" },
144
+ { type: "item", text: `embedding.model = ${ec.model ?? "BAAI/bge-m3"}`, action: "embedding.model" },
145
+ { type: "item", text: `proxy = ${proxySummary()}`, action: "proxy" },
146
+ { type: "item", text: "View full config", action: "view" },
147
+ ]
148
+
149
+ const choice = await showPicker("Config", mainEntries, { defaultIndex: mainIdx })
150
+ if (!choice) { running = false; continue } // Esc
151
+ mainIdx = Math.max(0, mainEntries.filter((e) => e.type === "item").indexOf(choice))
152
+
153
+ if (choice.action === "view") {
154
+ pushLabel("❯ Config", ansi.bold + C.tool)
155
+ pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
156
+ pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
157
+ pushLine(`agent.maxTurns: ${ac.maxTurns ?? 100}`, C.dim)
158
+ pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
159
+ pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
160
+ pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
161
+ pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
162
+ pushLine(`proxy: ${proxySummary()}`, C.dim)
163
+ pushLine(`Config file: ${configPath}`, C.dim)
164
+ running = false
165
+ continue
166
+ }
167
+
168
+ if (choice.action === "proxy") {
169
+ await proxyMenu()
170
+ continue
171
+ }
172
+
173
+ if (choice.action === "embedkey") {
174
+ if (await setEmbedKey()) running = false
175
+ continue
176
+ }
177
+
178
+ if (choice.action === "agent.verifyGuard") {
179
+ const newVal = ac.verifyGuard !== true
180
+ try {
181
+ await saveProxy((raw) => {
66
182
  raw.agent ??= {}
67
183
  raw.agent.verifyGuard = newVal
68
- const { saveConfig, loadConfig } = await import("../config.mjs")
69
- saveConfig(raw)
70
- const cfg = loadConfig()
71
- agent.provider = cfg.provider
72
- agent.providers = cfg.providersList
73
- agent.activeProvider = cfg.activeProvider
74
- agent.config = cfg
75
- agent.config.agent ??= {}
76
- pushLabel(`❯ Config`, ansi.bold + C.tool)
77
- pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
78
- } catch (error) {
79
- pushLine(`Save failed: ${error.message}`, C.error)
80
- }
81
- return
82
- }
83
- // embedding.model picker
84
- if (e.action === "embedding.model") {
85
- const models = [
86
- { label: "BAAI/bge-m3 (multilingual, 1024d)", value: "BAAI/bge-m3" },
87
- { label: "BAAI/bge-large-zh-v1.5 (Chinese, 1024d)", value: "BAAI/bge-large-zh-v1.5" },
88
- { label: "BAAI/bge-large-en-v1.5 (English, 1024d)", value: "BAAI/bge-large-en-v1.5" },
89
- { label: "text-embedding-3-small (OpenAI, 1536d)", value: "text-embedding-3-small" },
90
- { label: "text-embedding-3-large (OpenAI, 3072d)", value: "text-embedding-3-large" },
91
- ]
92
- const currentVal = ec.model ?? "BAAI/bge-m3"
93
- openPicker({
94
- title: `Embedding model (current: ${currentVal})`,
95
- entries: [
96
- { type: "header", text: `Current: ${currentVal}` },
97
- ...models.map((m) => ({ type: "item", text: m.label, action: m.value })),
98
- ],
99
- onSelect: async (sel) => {
100
- try {
101
- const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
102
- raw.embedding ??= {}
103
- raw.embedding.model = sel.action
104
- const { saveConfig, loadConfig } = await import("../config.mjs")
105
- saveConfig(raw)
106
- const cfg = loadConfig()
107
- agent.provider = cfg.provider
108
- agent.providers = cfg.providersList
109
- agent.activeProvider = cfg.activeProvider
110
- agent.config = cfg
111
- agent.config.agent ??= {}
112
- pushLabel(`❯ Config`, ansi.bold + C.tool)
113
- pushLine(`embedding.model = ${sel.action}`, C.tool)
114
- } catch (error) {
115
- pushLine(`Save failed: ${error.message}`, C.error)
116
- }
117
- },
118
184
  })
119
- return
120
- }
121
- // Numeric config items: ask for value, parse as number
122
- const isNumeric = e.action.startsWith("agent.")
123
- const label = e.action
124
- const current = e.action === "agent.maxTurns" ? (ac.maxTurns ?? 100)
125
- : e.action === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
126
- : e.action === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
127
- : ""
128
- const prompt = `${label} (current: ${current}):`
129
- const val = await askQuestion(prompt)
130
- if (!val) return
185
+ pushLabel("❯ Config", ansi.bold + C.tool)
186
+ pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
187
+ running = false
188
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
189
+ continue
190
+ }
191
+
192
+ if (choice.action === "embedding.model") {
193
+ const models = [
194
+ { label: "BAAI/bge-m3 (multilingual, 1024d)", value: "BAAI/bge-m3" },
195
+ { label: "BAAI/bge-large-zh-v1.5 (Chinese, 1024d)", value: "BAAI/bge-large-zh-v1.5" },
196
+ { label: "BAAI/bge-large-en-v1.5 (English, 1024d)", value: "BAAI/bge-large-en-v1.5" },
197
+ { label: "text-embedding-3-small (OpenAI, 1536d)", value: "text-embedding-3-small" },
198
+ { label: "text-embedding-3-large (OpenAI, 3072d)", value: "text-embedding-3-large" },
199
+ ]
200
+ const currentVal = ec.model ?? "BAAI/bge-m3"
201
+ const modelChoice = await showPicker("Embedding Model", [
202
+ { type: "header", text: `Current: ${currentVal}` },
203
+ ...models.map(m => ({ type: "item", text: m.label, action: m.value })),
204
+ ])
205
+ if (!modelChoice) continue
131
206
  try {
132
- const { configPath, loadConfig, saveConfig } = await import("../config.mjs")
133
- const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
134
- if (isNumeric) {
135
- const keys = label.split(".")
136
- let obj = raw
137
- for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
138
- const num = Number(val)
139
- if (isNaN(num)) { pushLine("Value must be a number", C.error); return }
140
- obj[keys[keys.length - 1]] = num
141
- }
142
- saveConfig(raw)
143
- const cfg = loadConfig()
144
- agent.provider = cfg.provider
145
- agent.providers = cfg.providersList
146
- agent.activeProvider = cfg.activeProvider
147
- agent.config = cfg
148
- agent.config.agent ??= {}
149
- pushLabel(`❯ Config`, ansi.bold + C.tool)
150
- pushLine(`${label} = ${val}`, C.tool)
151
- pushLine("(restart to apply to existing agent state)", C.dim)
152
- } catch (error) {
153
- pushLine(`Save failed: ${error.message}`, C.error)
154
- }
155
- },
156
- })
207
+ await saveProxy((raw) => {
208
+ raw.embedding ??= {}
209
+ raw.embedding.model = modelChoice.action
210
+ })
211
+ pushLabel("❯ Config", ansi.bold + C.tool)
212
+ pushLine(`embedding.model = ${modelChoice.action}`, C.tool)
213
+ running = false
214
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
215
+ continue
216
+ }
217
+
218
+ // Numeric config items
219
+ const label = choice.action
220
+ const current = label === "agent.maxTurns" ? (ac.maxTurns ?? 100)
221
+ : label === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
222
+ : label === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
223
+ : ""
224
+ const val = await askQuestion(`${label} (current: ${current}):`)
225
+ if (!val) continue
226
+ try {
227
+ const num = Number(val)
228
+ if (isNaN(num)) { pushLine("Value must be a number", C.error); continue }
229
+ await saveProxy((raw) => {
230
+ const keys = label.split(".")
231
+ let obj = raw
232
+ for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
233
+ obj[keys[keys.length - 1]] = num
234
+ })
235
+ pushLabel("❯ Config", ansi.bold + C.tool)
236
+ pushLine(`${label} = ${val}`, C.tool)
237
+ pushLine("(restart to apply)", C.dim)
238
+ running = false
239
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
240
+ }
157
241
  }
@@ -7,7 +7,7 @@ export async function handleExtractCommand(ctx) {
7
7
  pushLine("[extract] Analyzing session...", C.dim)
8
8
  const count = await runDistill()
9
9
  const msg = count > 0
10
- ? `Knowledge extracted: ${count} candidate(s) saved to memory (use /skills to list, agent will recall via memory_search)`
10
+ ? `Knowledge extracted: ${count} candidate(s) saved to memory (agent will recall them via memory_search)`
11
11
  : "No new knowledge found in this session."
12
12
  pushLine(msg, count > 0 ? C.tool : C.dim)
13
13
  }