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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/advisor.mjs +360 -72
- package/src/agent/helpers.mjs +7 -3
- package/src/agent/setup.mjs +2 -2
- package/src/agent-tools/advisor.mjs +36 -0
- package/src/agent-tools/plan.mjs +53 -2
- package/src/agent-tools/subagent.mjs +7 -1
- package/src/agent-tools/timer.mjs +1 -1
- package/src/agent-tools/verify.mjs +1 -0
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +73 -21
- package/src/auto-think.mjs +23 -5
- package/src/cli/make-agent.mjs +7 -0
- package/src/config.mjs +47 -20
- package/src/prompts/advisor-round1.md +23 -0
- package/src/prompts/advisor-round2.md +26 -0
- package/src/prompts/advisor-round3.md +24 -0
- package/src/prompts/coder.md +6 -2
- package/src/prompts/discipline.md +23 -6
- package/src/prompts/explore.md +2 -0
- package/src/prompts/plan.md +2 -0
- package/src/prompts/system.md +13 -6
- package/src/provider/anthropic.mjs +190 -0
- package/src/provider/core.mjs +42 -130
- package/src/provider/google.mjs +199 -0
- package/src/provider/sse.mjs +112 -0
- package/src/proxy.mjs +236 -0
- package/src/tools/bash.md +8 -0
- package/src/tools/codemode.mjs +5 -16
- package/src/tools/edit.md +8 -0
- package/src/tools/fetch.md +2 -1
- package/src/tools/git.mjs +125 -156
- package/src/tools/index.mjs +9 -9
- package/src/tools/linter.mjs +46 -32
- package/src/tools/read.md +7 -0
- package/src/tools/shared.mjs +16 -0
- package/src/tools/system.mjs +14 -10
- package/src/tools/web.mjs +115 -89
- package/src/tools/websearch.md +5 -3
- package/src/tui/agent-turn.mjs +86 -75
- package/src/tui/cmd-advisor.mjs +138 -49
- package/src/tui/cmd-clear.mjs +11 -17
- package/src/tui/cmd-config.mjs +226 -142
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-fold.mjs +2 -3
- package/src/tui/cmd-goal.mjs +58 -27
- package/src/tui/cmd-help.mjs +3 -1
- package/src/tui/cmd-mcp.mjs +178 -142
- package/src/tui/cmd-model.mjs +15 -4
- package/src/tui/cmd-new.mjs +7 -13
- package/src/tui/cmd-restore.mjs +12 -16
- package/src/tui/cmd-session.mjs +28 -32
- package/src/tui/cmd-think.mjs +75 -50
- package/src/tui/cmd-undo.mjs +19 -23
- package/src/tui/cmd-upgrade.mjs +22 -26
- package/src/tui/index.mjs +61 -215
- package/src/tui/key-handler.mjs +56 -20
- package/src/tui/layout.mjs +13 -3
- package/src/tui/pickers.mjs +151 -182
- package/src/tui/render-conversation.mjs +92 -0
- package/src/tui/render-frame.mjs +56 -114
- package/src/tui/render-loop.mjs +181 -0
- package/src/tui/slash-commands.mjs +26 -16
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -1,68 +1,157 @@
|
|
|
1
|
-
/** /advisor command: toggle advisor on/off, select model.
|
|
2
|
-
* ctx: { agent,
|
|
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,
|
|
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 ||
|
|
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: ${
|
|
18
|
+
{ type: "item", text: `Model: ${curModel}`, action: "model", note: `Provider: ${curProvider}` },
|
|
19
|
+
{ type: "item", text: `Thinking: ${thinkInfo}`, action: "thinking" },
|
|
15
20
|
]
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
const entries = [
|
|
56
|
+
{ type: "item", text: "Use main model", action: "inherit", marker: !cfg.provider ? "●" : "" },
|
|
57
|
+
]
|
|
43
58
|
for (const p of providers) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
}
|
package/src/tui/cmd-clear.mjs
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
/** /clear command: clear screen (confirm to prevent accidental trigger).
|
|
2
|
-
* ctx: { state,
|
|
2
|
+
* ctx: { state, showPicker, render } */
|
|
3
3
|
export async function handleClearCommand(ctx) {
|
|
4
|
-
const { state,
|
|
4
|
+
const { state, showPicker, render } = ctx
|
|
5
5
|
if (state.lines.length > 0) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 = []
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
}
|
package/src/tui/cmd-extract.mjs
CHANGED
|
@@ -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 (
|
|
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
|
}
|