sanook-cli 0.5.2 → 0.5.7
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/CHANGELOG.md +112 -2
- package/README.md +15 -3
- package/README.th.md +8 -1
- package/dist/approval.js +7 -0
- package/dist/bin.js +637 -56
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +42 -3
- package/dist/brain-final.js +15 -9
- package/dist/brain-link.js +73 -0
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain.js +3 -0
- package/dist/brand.js +4 -0
- package/dist/cli-args.js +47 -9
- package/dist/cli-option-values.js +1 -1
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +98 -15
- package/dist/config.js +66 -34
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +20 -0
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/gateway/auth.js +14 -3
- package/dist/gateway/deliver.js +45 -3
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +30 -1
- package/dist/gateway/ledger.js +20 -1
- package/dist/gateway/session.js +34 -11
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +24 -4
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +65 -9
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +153 -9
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp.js +77 -5
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +51 -7
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +7 -5
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +20 -8
- package/dist/providers/keys.js +21 -0
- package/dist/providers/models.js +1 -1
- package/dist/providers/registry.js +11 -1
- package/dist/search/cli.js +9 -1
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +10 -10
- package/dist/session-brain.js +103 -0
- package/dist/session-distill.js +84 -0
- package/dist/session.js +1 -11
- package/dist/skill-install.js +24 -1
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +31 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/permission.js +82 -16
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/search.js +9 -2
- package/dist/tools/task.js +22 -2
- package/dist/tools/timeout.js +7 -5
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +874 -35
- package/dist/ui/banner.js +78 -4
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +30 -2
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +163 -50
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +32 -6
- package/dist/usage-cli.js +160 -0
- package/dist/usage-ledger.js +169 -0
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/package.json +4 -3
- package/scripts/postinstall.mjs +4 -4
- package/second-brain/Projects/_Index.md +17 -4
- package/second-brain/Projects/sanook-cli/_Index.md +7 -3
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +52 -11
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +2 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -23
- package/second-brain/Shared/Tech-Standards/_Index.md +2 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
package/dist/bin.js
CHANGED
|
@@ -18,7 +18,7 @@ import { hasContinueAnyRequest, hasContinueRequest, hasResumeRequest, hasServeCo
|
|
|
18
18
|
const useColor = !process.env.NO_COLOR && (Boolean(process.env.FORCE_COLOR) || process.stdout.isTTY === true);
|
|
19
19
|
const DIM = useColor ? '\x1b[2m' : '';
|
|
20
20
|
const RESET = useColor ? '\x1b[0m' : '';
|
|
21
|
-
async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'ask', quiet = false, fallbackModel) {
|
|
21
|
+
async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'ask', quiet = false, fallbackModel, planHandoffTask) {
|
|
22
22
|
const controller = new AbortController();
|
|
23
23
|
process.on('SIGINT', () => {
|
|
24
24
|
controller.abort();
|
|
@@ -59,6 +59,10 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
59
59
|
process.stdout.write(`\n${DIM}${cost.summary()}${RESET}\n`);
|
|
60
60
|
else if (quiet)
|
|
61
61
|
process.stdout.write('\n');
|
|
62
|
+
if (planMode && planHandoffTask && !json && !quiet) {
|
|
63
|
+
const { formatPlanExecuteHandoff } = await import('./plan-handoff.js');
|
|
64
|
+
process.stderr.write(`\n${DIM}${formatPlanExecuteHandoff(planHandoffTask)}${RESET}\n`);
|
|
65
|
+
}
|
|
62
66
|
// จำ session ไว้ทำงานต่อได้ (sanook --continue "...") — แก้ concern AI ลืมว่าทำถึงไหน
|
|
63
67
|
const now = new Date().toISOString();
|
|
64
68
|
await saveSession({ id: newSessionId(), created: now, updated: now, model, cwd: process.cwd(), messages });
|
|
@@ -68,6 +72,16 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
68
72
|
if (brain) {
|
|
69
73
|
await appendBrainWorklog(brain, { prompt, summary: cost.summary(), model, today: now.slice(0, 10) }).catch(() => { });
|
|
70
74
|
}
|
|
75
|
+
// opt-in (experimental, default OFF): auto-distill durable decisions/gotchas/preferences from this
|
|
76
|
+
// session into the compounding memory store so the self-retrieving brain surfaces them next time.
|
|
77
|
+
// Off by default per experiment H5 — extraction is high-precision (~0.88) but end-to-end recall is
|
|
78
|
+
// gated by retrieval quality (semantic helps). Enable with SANOOK_AUTO_DISTILL=1.
|
|
79
|
+
if (envFlag('SANOOK_AUTO_DISTILL')) {
|
|
80
|
+
const { distilledFactsFromMessages } = await import('./session-distill.js');
|
|
81
|
+
const { appendMemory } = await import('./memory.js');
|
|
82
|
+
for (const fact of distilledFactsFromMessages(messages))
|
|
83
|
+
await appendMemory(fact).catch(() => { });
|
|
84
|
+
}
|
|
71
85
|
}
|
|
72
86
|
catch (err) {
|
|
73
87
|
const msg = redactKey(err.message);
|
|
@@ -78,6 +92,28 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
78
92
|
process.exit(1);
|
|
79
93
|
}
|
|
80
94
|
}
|
|
95
|
+
/** sanook plan "<task>" — read-only plan mode; stderr prints execute handoff after success */
|
|
96
|
+
async function runPlan(args) {
|
|
97
|
+
const parsed = parseArgs(args);
|
|
98
|
+
if (parsed.budgetInvalid)
|
|
99
|
+
process.stderr.write(`${BRAND.cliName}: ⚠ --budget ไม่ถูกต้อง (ต้องเป็นจำนวนบวก) — รันต่อโดยไม่มี spend cap\n`);
|
|
100
|
+
const resumeSession = await requestedResumeSession(args, parsed.resume);
|
|
101
|
+
const budgetUsd = Number.isFinite(parsed.budget) ? parsed.budget : undefined;
|
|
102
|
+
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
103
|
+
const prompt = piped ? `${parsed.prompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : parsed.prompt;
|
|
104
|
+
if (!prompt) {
|
|
105
|
+
console.error(`ใช้: ${BRAND.cliName} plan "<task>" [--json] [-m model]`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const config = await loadConfig({ model: parsed.model ?? resumeSession?.model, budgetUsd });
|
|
109
|
+
const noKey = headlessKeyHint(config.model);
|
|
110
|
+
if (noKey) {
|
|
111
|
+
process.stderr.write(`${noKey}\n`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const history = resumeSession?.messages ?? (await requestedContinuationHistory(args));
|
|
115
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, true, 'ask', parsed.quiet, config.fallbackModel, prompt);
|
|
116
|
+
}
|
|
81
117
|
// อ่านจาก package.json (single source of truth) — กัน version constant drift
|
|
82
118
|
const PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
83
119
|
const VERSION = PACKAGE.version;
|
|
@@ -86,15 +122,20 @@ const HELP = `${BRAND.productName} — a terminal AI coding agent (BYOK)
|
|
|
86
122
|
|
|
87
123
|
usage:
|
|
88
124
|
${BRAND.cliName} "<task>" run one task (headless)
|
|
125
|
+
${BRAND.cliName} plan "<task>" plan-only (read-only) + execute handoff hint on stderr
|
|
89
126
|
${BRAND.cliName} -z "<task>" one-shot final output (script-friendly)
|
|
90
127
|
${BRAND.cliName} chat -q "<query>" direct one-shot query
|
|
91
128
|
${BRAND.cliName} interactive REPL
|
|
92
129
|
${BRAND.cliName} setup [section] setup wizard (model | gateway | tools | agent | brain)
|
|
130
|
+
${BRAND.cliName} dashboard [--port] Sanook Dashboard (local web admin UI)
|
|
93
131
|
${BRAND.cliName} model choose provider + model
|
|
94
132
|
${BRAND.cliName} --json "<task>" headless, JSONL output (for CI/scripts)
|
|
95
133
|
${BRAND.cliName} sessions list/resume-audit saved conversation sessions
|
|
96
134
|
${BRAND.cliName} insights local usage/session insights
|
|
135
|
+
${BRAND.cliName} usage [daily|...] token/cost ledger (ccusage-style)
|
|
97
136
|
${BRAND.cliName} dump [--show-keys] support snapshot (secrets redacted)
|
|
137
|
+
${BRAND.cliName} prompt-size [--json] inspect prompt/context budget without calling a model
|
|
138
|
+
${BRAND.cliName} runtimes [--json] inspect optional Python/Rust runtime surface
|
|
98
139
|
${BRAND.cliName} update update ${BRAND.cliName} to the latest npm release
|
|
99
140
|
${BRAND.cliName} doctor ตรวจการติดตั้ง + วิธีแก้ PATH (เมื่อพิมพ์ "${BRAND.cliName}" แล้วไม่เจอ)
|
|
100
141
|
|
|
@@ -119,6 +160,7 @@ gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
|
|
|
119
160
|
${BRAND.cliName} gateway start [--port 8787] เปิด gateway เป็น background process
|
|
120
161
|
${BRAND.cliName} gateway stop|restart|install จัดการ gateway service
|
|
121
162
|
${BRAND.cliName} gateway status ดู config/status gateway
|
|
163
|
+
${BRAND.cliName} gateway doctor ตรวจ token/webhook/allowlist ของ channels ที่ตั้งไว้
|
|
122
164
|
${BRAND.cliName} send --to telegram|discord|slack|mattermost|homeassistant|email|line|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams[:target] "msg" ส่งข้อความออก platform โดยไม่เรียก LLM
|
|
123
165
|
${BRAND.cliName} webhook subscribe <route> [--prompt "..."] [--to telegram|slack|mattermost|homeassistant|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams]
|
|
124
166
|
รับ event จาก GitHub/GitLab/Jira/Stripe แล้ว trigger agent/delivery
|
|
@@ -131,21 +173,36 @@ gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
|
|
|
131
173
|
|
|
132
174
|
skills (built-in + ติดตั้งเพิ่มได้):
|
|
133
175
|
${BRAND.cliName} skill list ดู skill ทั้งหมด
|
|
176
|
+
${BRAND.cliName} skill install <name|path> ติดตั้ง skill จาก bundled catalog / local path
|
|
134
177
|
${BRAND.cliName} skill add <user/repo|url|path> ติดตั้ง skill จาก GitHub / URL / local
|
|
135
178
|
${BRAND.cliName} skill remove <name> ลบ skill ที่ติดตั้ง
|
|
136
179
|
${BRAND.cliName} models [provider] ดู/verify model id (เทียบ provider จริงถ้ามี key)
|
|
137
180
|
|
|
181
|
+
project setup:
|
|
182
|
+
${BRAND.cliName} init [--trust] scaffold .sanook/commands + onboarding hints
|
|
183
|
+
|
|
138
184
|
second brain (Obsidian workspace สำหรับจัดเก็บงาน + ความจำ AI):
|
|
139
185
|
${BRAND.cliName} brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
|
|
140
186
|
${BRAND.cliName} brain doctor ตรวจ health ของ second-brain ที่ config.brainPath
|
|
141
|
-
${BRAND.cliName} brain context [--task "..."]
|
|
187
|
+
${BRAND.cliName} brain context [--task "..."] [--project <slug>] แสดง context ที่ Sanook จะ inject (+ project auto-detect จาก cwd)
|
|
188
|
+
${BRAND.cliName} brain projects list แสดง Projects/<slug>/ + repo_path mapping
|
|
142
189
|
${BRAND.cliName} brain eval รัน second-brain benchmark sanity checks
|
|
143
190
|
${BRAND.cliName} brain review curator review: inbox, packs, sessions, evals, note hygiene
|
|
191
|
+
${BRAND.cliName} brain pack list|show <name> ดู context packs ใน Shared/Context-Packs/
|
|
192
|
+
${BRAND.cliName} brain new project [--title "..."] [--repo /path] [--verify "..."] scaffold Projects/<slug>/ workspace
|
|
193
|
+
${BRAND.cliName} brain repair [--dry-run] แก้ one-line fixes หลัง doctor/review
|
|
194
|
+
${BRAND.cliName} brain consolidate [--apply] sleep-time consolidation (inbox, stale, retrieval; dry-run default)
|
|
195
|
+
${BRAND.cliName} brain metrics [--no-retrieval] vault counts, stale notes, index freshness, retrieval coverage
|
|
144
196
|
${BRAND.cliName} brain final --task "..." สร้าง final gate note ใน Sessions พร้อม evidence scaffold
|
|
145
197
|
|
|
146
198
|
search (BM25 + optional BYOK semantic เหนือ vault + memory + sessions + skills):
|
|
147
199
|
${BRAND.cliName} index (re)index vault+memory แบบ incremental (O(delta))
|
|
148
200
|
${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]
|
|
201
|
+
${BRAND.cliName} web status [--json] [--probe] ตรวจ true web/search readiness ผ่าน MCP (local search ไม่ใช่ internet)
|
|
202
|
+
${BRAND.cliName} web doctor [--json] probe web/search/fetch MCP candidates
|
|
203
|
+
${BRAND.cliName} web fetch <url> [--json] ดึงหน้าเว็บสาธารณะ + สรุปโครงสร้าง (fallback ladder ที่ถูกกติกา)
|
|
204
|
+
${BRAND.cliName} web search "<q>" [--limit N] ค้นเว็บผ่าน Tavily (ต้องมี TAVILY_API_KEY)
|
|
205
|
+
${BRAND.cliName} web setup tavily [--api-key K] ตั้งค่า Tavily (เขียน MCP + เก็บ key 0600)
|
|
149
206
|
${BRAND.cliName} mcp serve expose brain เป็น MCP server (stdio) ให้ Claude Desktop/Cursor
|
|
150
207
|
|
|
151
208
|
config & mcp:
|
|
@@ -153,10 +210,14 @@ config & mcp:
|
|
|
153
210
|
${BRAND.cliName} auth [list|status|add|remove] จัดการ API keys ของ providers (BYOK, redacted)
|
|
154
211
|
${BRAND.cliName} sessions [list|latest|show|rm] จัดการ saved sessions
|
|
155
212
|
${BRAND.cliName} insights [--days N] [--all] ดู usage/session insights ในเครื่อง
|
|
213
|
+
${BRAND.cliName} memory [stats|log "<q>"] ดู memory: สถิติ + วิวัฒนาการของ belief (เคยเชื่ออะไร ถูก supersede ตอนไหน)
|
|
156
214
|
${BRAND.cliName} dump [--show-keys] diagnostic/support dump แบบไม่โชว์ raw secret
|
|
215
|
+
${BRAND.cliName} prompt-size [--json] ดู system prompt / skills / brain / tools token budget แบบ offline
|
|
216
|
+
${BRAND.cliName} runtimes [--json] ดู Python/Rust optional runtime + บทบาทใน Sanook
|
|
217
|
+
${BRAND.cliName} web status [--json] ดู web-search/fetch readiness และ grounding policy
|
|
157
218
|
${BRAND.cliName} tools ดู tool surface ที่ agent ใช้ได้
|
|
158
219
|
${BRAND.cliName} config [get|set <k> <v>] ดู/แก้ ${appHomePath('config.json')} (model/budgetUsd/permissionMode/cacheTtl/compaction/contextCompression/thinking/embeddingModel)
|
|
159
|
-
${BRAND.cliName} mcp [search|info|install|test|doctor|preset|list|add|remove] จัดการ MCP servers
|
|
220
|
+
${BRAND.cliName} mcp [search|info|install|test|doctor|enable|disable|preset|list|add|remove] จัดการ MCP servers
|
|
160
221
|
${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
|
|
161
222
|
|
|
162
223
|
flags:
|
|
@@ -183,6 +244,10 @@ async function runServe(args) {
|
|
|
183
244
|
console.error(`port ไม่ถูกต้อง: ${parsed.portError}`);
|
|
184
245
|
process.exit(1);
|
|
185
246
|
}
|
|
247
|
+
if (parsed.modelError) {
|
|
248
|
+
console.error(`model ไม่ถูกต้อง: ${parsed.modelError}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
186
251
|
const config = await loadConfig({ model: parsed.model });
|
|
187
252
|
const { startGateway } = await import('./gateway/serve.js');
|
|
188
253
|
process.stdout.write(`${DIM}${BRAND.productName} gateway — model: ${config.model}${RESET}\n`);
|
|
@@ -215,6 +280,35 @@ async function startModelSetup() {
|
|
|
215
280
|
},
|
|
216
281
|
});
|
|
217
282
|
}
|
|
283
|
+
async function runDashboard(args = []) {
|
|
284
|
+
let port = 9119;
|
|
285
|
+
for (let i = 0; i < args.length; i++) {
|
|
286
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
287
|
+
port = Number(args[++i]);
|
|
288
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
289
|
+
console.error(`${BRAND.cliName}: --port ต้องเป็นจำนวนบวก`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (args[i] === '-h' || args[i] === '--help') {
|
|
294
|
+
console.log(`ใช้: ${BRAND.cliName} dashboard [--port 9119]`);
|
|
295
|
+
console.log('เปิด Sanook Dashboard (local web UI — Hermes-style admin panel)');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const { startDashboardServer } = await import('./dashboard/server.js');
|
|
300
|
+
const stop = await startDashboardServer({
|
|
301
|
+
port,
|
|
302
|
+
onLog: (m) => console.log(m),
|
|
303
|
+
});
|
|
304
|
+
const shutdown = () => {
|
|
305
|
+
stop();
|
|
306
|
+
process.stdout.write('\n[dashboard] stopped\n');
|
|
307
|
+
process.exit(0);
|
|
308
|
+
};
|
|
309
|
+
process.on('SIGINT', shutdown);
|
|
310
|
+
process.on('SIGTERM', shutdown);
|
|
311
|
+
}
|
|
218
312
|
async function runTools(_args = []) {
|
|
219
313
|
const { tools } = await import('./tools/index.js');
|
|
220
314
|
const names = Object.keys(tools).sort();
|
|
@@ -222,6 +316,207 @@ async function runTools(_args = []) {
|
|
|
222
316
|
console.log(names.map((n) => ` ${n}`).join('\n'));
|
|
223
317
|
console.log(`\nจัดการ MCP เพิ่มเติม: ${BRAND.cliName} mcp add <name> <command> [args...]`);
|
|
224
318
|
}
|
|
319
|
+
async function runPromptSize(args = []) {
|
|
320
|
+
const allowed = new Set(['--json']);
|
|
321
|
+
const unknown = args.find((arg) => !allowed.has(arg));
|
|
322
|
+
if (unknown) {
|
|
323
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
324
|
+
console.error(`ใช้: ${BRAND.cliName} prompt-size [--json]`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const { buildPromptSizeBreakdown, renderPromptSizeBreakdown } = await import('./prompt-size.js');
|
|
328
|
+
const report = await buildPromptSizeBreakdown();
|
|
329
|
+
if (args.includes('--json'))
|
|
330
|
+
console.log(JSON.stringify(report, null, 2));
|
|
331
|
+
else
|
|
332
|
+
process.stdout.write(renderPromptSizeBreakdown(report));
|
|
333
|
+
}
|
|
334
|
+
async function runRuntimes(args = []) {
|
|
335
|
+
const allowed = new Set(['--json']);
|
|
336
|
+
const unknown = args.find((arg) => !allowed.has(arg));
|
|
337
|
+
if (unknown) {
|
|
338
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
339
|
+
console.error(`ใช้: ${BRAND.cliName} runtimes [--json]`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
const { inspectPolyglotRuntimes, renderPolyglotReport } = await import('./polyglot.js');
|
|
343
|
+
const report = await inspectPolyglotRuntimes();
|
|
344
|
+
if (args.includes('--json'))
|
|
345
|
+
console.log(JSON.stringify(report, null, 2));
|
|
346
|
+
else
|
|
347
|
+
process.stdout.write(renderPolyglotReport(report));
|
|
348
|
+
}
|
|
349
|
+
async function runWeb(args = []) {
|
|
350
|
+
const action = args[0] && !args[0].startsWith('--') ? args[0] : 'status';
|
|
351
|
+
const rest = action === args[0] ? args.slice(1) : args;
|
|
352
|
+
if (action === 'fetch')
|
|
353
|
+
return runWebFetch(rest);
|
|
354
|
+
if (action === 'search')
|
|
355
|
+
return runWebSearch(rest);
|
|
356
|
+
if (action === 'setup')
|
|
357
|
+
return runWebSetup(rest);
|
|
358
|
+
const flags = rest;
|
|
359
|
+
const allowed = new Set(['--json', '--probe']);
|
|
360
|
+
const unknown = flags.find((arg) => !allowed.has(arg));
|
|
361
|
+
if (!['status', 'doctor'].includes(action) || unknown) {
|
|
362
|
+
if (unknown)
|
|
363
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
364
|
+
console.error(`ใช้: ${BRAND.cliName} web status [--json] [--probe]`);
|
|
365
|
+
console.error(` ${BRAND.cliName} web doctor [--json]`);
|
|
366
|
+
console.error(` ${BRAND.cliName} web fetch <url> [--json] [--no-reader] [--no-archive] [--no-robots] [--allow-private]`);
|
|
367
|
+
console.error(` ${BRAND.cliName} web search "<query>" [--json] [--limit N]`);
|
|
368
|
+
console.error(` ${BRAND.cliName} web setup tavily [--api-key <key>]`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
const { inspectWebSurface, renderWebSurfaceReport } = await import('./web-surface.js');
|
|
372
|
+
const report = await inspectWebSurface({ cwd: process.cwd(), probe: action === 'doctor' || flags.includes('--probe') });
|
|
373
|
+
if (flags.includes('--json'))
|
|
374
|
+
console.log(JSON.stringify(report, null, 2));
|
|
375
|
+
else
|
|
376
|
+
process.stdout.write(renderWebSurfaceReport(report));
|
|
377
|
+
if (action === 'doctor' && report.webCandidates.some((candidate) => candidate.probe && !candidate.probe.ok))
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
/** TAVILY_API_KEY from env first, then any MCP server env in ~/.sanook/mcp.json. */
|
|
381
|
+
async function resolveTavilyKey() {
|
|
382
|
+
const fromEnv = process.env.TAVILY_API_KEY?.trim();
|
|
383
|
+
if (fromEnv)
|
|
384
|
+
return fromEnv;
|
|
385
|
+
try {
|
|
386
|
+
const { loadMcpConfig } = await import('./mcp.js');
|
|
387
|
+
const cfg = await loadMcpConfig();
|
|
388
|
+
for (const server of Object.values(cfg)) {
|
|
389
|
+
const key = server.env?.TAVILY_API_KEY?.trim();
|
|
390
|
+
if (key)
|
|
391
|
+
return key;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
/* no mcp config */
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
async function runWebFetch(args) {
|
|
400
|
+
const url = args.find((a) => !a.startsWith('--'));
|
|
401
|
+
if (!url) {
|
|
402
|
+
console.error(`ใช้: ${BRAND.cliName} web fetch <url> [--json] [--no-reader] [--no-archive] [--no-robots] [--allow-private]`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const { fetchWeb, renderWebFetchResult } = await import('./web-fetch.js');
|
|
406
|
+
const result = await fetchWeb(url, {
|
|
407
|
+
allowReader: !args.includes('--no-reader'),
|
|
408
|
+
allowArchive: !args.includes('--no-archive'),
|
|
409
|
+
respectRobots: !args.includes('--no-robots'),
|
|
410
|
+
allowPrivateHosts: args.includes('--allow-private'),
|
|
411
|
+
tavilyApiKey: await resolveTavilyKey(),
|
|
412
|
+
});
|
|
413
|
+
if (args.includes('--json'))
|
|
414
|
+
console.log(JSON.stringify(result, null, 2));
|
|
415
|
+
else
|
|
416
|
+
process.stdout.write(`${renderWebFetchResult(result)}\n`);
|
|
417
|
+
if (!result.ok)
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
async function runWebSearch(args) {
|
|
421
|
+
let limit = 5;
|
|
422
|
+
const terms = [];
|
|
423
|
+
for (let i = 0; i < args.length; i++) {
|
|
424
|
+
const a = args[i];
|
|
425
|
+
if (a === '--json')
|
|
426
|
+
continue;
|
|
427
|
+
if (a === '--limit') {
|
|
428
|
+
limit = Number(args[++i]) || limit;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (a.startsWith('--limit=')) {
|
|
432
|
+
limit = Number(a.slice('--limit='.length)) || limit;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (a.startsWith('--'))
|
|
436
|
+
continue;
|
|
437
|
+
terms.push(a);
|
|
438
|
+
}
|
|
439
|
+
const query = terms.join(' ').trim();
|
|
440
|
+
if (!query) {
|
|
441
|
+
console.error(`ใช้: ${BRAND.cliName} web search "<query>" [--json] [--limit N]`);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
const apiKey = await resolveTavilyKey();
|
|
445
|
+
if (!apiKey) {
|
|
446
|
+
console.error(`web search ต้องมี Tavily API key — ตั้งค่า: ${BRAND.cliName} web setup tavily`);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
const { tavilySearch } = await import('./web-fetch.js');
|
|
450
|
+
try {
|
|
451
|
+
const hits = await tavilySearch(query, { apiKey, maxResults: Math.min(Math.max(limit, 1), 20) });
|
|
452
|
+
if (args.includes('--json')) {
|
|
453
|
+
console.log(JSON.stringify(hits, null, 2));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!hits.length) {
|
|
457
|
+
console.log('(no results)');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
for (const h of hits)
|
|
461
|
+
console.log(`- ${h.title}\n ${h.url}\n ${h.content.slice(0, 200)}`);
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
console.error(`web search ล้มเหลว: ${e.message}`);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function runWebSetup(args) {
|
|
469
|
+
const provider = args.find((a) => !a.startsWith('--'));
|
|
470
|
+
if (provider !== 'tavily') {
|
|
471
|
+
console.error(`ใช้: ${BRAND.cliName} web setup tavily [--api-key <key>]`);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
let apiKey = (argValue(args, '--api-key') ?? process.env.TAVILY_API_KEY ?? '').trim();
|
|
475
|
+
if (!apiKey)
|
|
476
|
+
apiKey = (await askText('Tavily API key (tvly-...): ')).trim();
|
|
477
|
+
if (!apiKey) {
|
|
478
|
+
console.error('ต้องระบุ Tavily API key');
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
const mcpPath = appHomePath('mcp.json');
|
|
482
|
+
let cfg;
|
|
483
|
+
try {
|
|
484
|
+
const parsed = JSON.parse(await readFile(mcpPath, 'utf8'));
|
|
485
|
+
cfg = { mcpServers: parsed.mcpServers ?? {} };
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
cfg = { mcpServers: {} };
|
|
489
|
+
}
|
|
490
|
+
cfg.mcpServers.tavily = { command: 'npx', args: ['-y', 'tavily-mcp'], env: { TAVILY_API_KEY: apiKey } };
|
|
491
|
+
await mkdir(dirname(mcpPath), { recursive: true });
|
|
492
|
+
await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
|
|
493
|
+
await chmod(mcpPath, 0o600).catch(() => { });
|
|
494
|
+
console.log(`ตั้งค่า Tavily แล้ว → ${mcpPath} (chmod 600; key เก็บแบบ env ไม่ echo ออกจอ)`);
|
|
495
|
+
console.log(` • agent runtime ใช้ผ่าน MCP "tavily" (search/extract)`);
|
|
496
|
+
console.log(` • ${BRAND.cliName} web fetch <url> และ ${BRAND.cliName} web search "<q>" จะหยิบ key นี้อัตโนมัติ`);
|
|
497
|
+
console.log(`ทดสอบ: ${BRAND.cliName} mcp test tavily`);
|
|
498
|
+
}
|
|
499
|
+
/** sanook memory [log "<query>" | stats] — read-only view over the bi-temporal memory store */
|
|
500
|
+
async function runMemory(args = []) {
|
|
501
|
+
const action = args[0] && !args[0].startsWith('--') ? args[0] : undefined;
|
|
502
|
+
const rest = action ? args.slice(1) : args;
|
|
503
|
+
const json = args.includes('--json');
|
|
504
|
+
const { loadStore } = await import('./memory-store.js');
|
|
505
|
+
const { memoryLog, renderMemoryLog, memoryStats, renderMemoryStats } = await import('./memory-log.js');
|
|
506
|
+
const store = await loadStore();
|
|
507
|
+
if (action === 'log') {
|
|
508
|
+
const query = rest.filter((a) => !a.startsWith('--')).join(' ').trim();
|
|
509
|
+
const entries = memoryLog(store, query);
|
|
510
|
+
if (json)
|
|
511
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
512
|
+
else
|
|
513
|
+
process.stdout.write(`${renderMemoryLog(entries, query)}\n`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
// default + `stats`: overview of what's remembered (active / superseded / archived)
|
|
517
|
+
const stats = memoryStats(store);
|
|
518
|
+
console.log(json ? JSON.stringify(stats, null, 2) : renderMemoryStats(stats));
|
|
519
|
+
}
|
|
225
520
|
async function runAgentSetupSummary() {
|
|
226
521
|
const cfg = await loadConfig({});
|
|
227
522
|
console.log(`${BRAND.productName} agent settings`);
|
|
@@ -239,6 +534,13 @@ async function runAgentSetupSummary() {
|
|
|
239
534
|
console.log(` ${BRAND.cliName} config set budgetUsd 0.25`);
|
|
240
535
|
console.log(` ${BRAND.cliName} config set fallbackModel haiku`);
|
|
241
536
|
}
|
|
537
|
+
async function runGatewayDoctor() {
|
|
538
|
+
const { checkGateway, formatGatewayDoctorReport } = await import('./gateway/doctor.js');
|
|
539
|
+
const report = await checkGateway();
|
|
540
|
+
console.log(formatGatewayDoctorReport(report));
|
|
541
|
+
if (!report.ok)
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
242
544
|
async function runGatewayStatus() {
|
|
243
545
|
const { readGatewayConfig, redactGatewayConfig, resolveBlueBubblesConfig, resolveDiscordConfig, resolveEmailConfig, resolveGoogleChatConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveTeamsConfig, resolveWhatsAppConfig, resolveWebhookConfig, gatewayConfigPath, } = await import('./gateway/config.js');
|
|
244
546
|
const cfg = await readGatewayConfig();
|
|
@@ -415,6 +717,31 @@ async function runGatewayStatus() {
|
|
|
415
717
|
console.log(` public url: ${webhooks.publicUrl ?? '(not set)'}`);
|
|
416
718
|
console.log(` rate limit: ${webhooks.rateLimitPerMinute}/minute`);
|
|
417
719
|
}
|
|
720
|
+
const { checkGateway, formatGatewayDoctorStatus, listPendingCronJobs, listRecentDeliveryFailures, summarizeChannelHealth, } = await import('./gateway/doctor.js');
|
|
721
|
+
const pendingCron = await listPendingCronJobs();
|
|
722
|
+
console.log(`\ncron (pending): ${pendingCron.length}`);
|
|
723
|
+
if (pendingCron.length) {
|
|
724
|
+
for (const task of pendingCron.slice(0, 10)) {
|
|
725
|
+
const when = new Date(task.runAt).toISOString();
|
|
726
|
+
const deliver = task.deliver ? ` → ${task.deliver}` : '';
|
|
727
|
+
console.log(` ${task.id} ${when} ${task.schedule ?? 'once'} ${task.spec.slice(0, 60)}${deliver}`);
|
|
728
|
+
}
|
|
729
|
+
if (pendingCron.length > 10)
|
|
730
|
+
console.log(` … และอีก ${pendingCron.length - 10} งาน`);
|
|
731
|
+
}
|
|
732
|
+
const deliveryFailures = await listRecentDeliveryFailures(5);
|
|
733
|
+
console.log(`\ndelivery failures (recent): ${deliveryFailures.length ? deliveryFailures.length : 'none'}`);
|
|
734
|
+
for (const failure of deliveryFailures) {
|
|
735
|
+
const when = failure.lastRun ? new Date(failure.lastRun).toISOString() : '(unknown)';
|
|
736
|
+
console.log(` ${failure.taskId} ${when} ${failure.deliver} ${failure.error.slice(0, 120)}`);
|
|
737
|
+
}
|
|
738
|
+
const healthReport = await checkGateway({ config: cfg, skipNetwork: true });
|
|
739
|
+
const health = summarizeChannelHealth(healthReport.checks).filter((item) => item.status !== 'skip');
|
|
740
|
+
console.log(`\nchannel health (config): ${health.length ? '' : 'no configured channels'}`);
|
|
741
|
+
for (const item of health) {
|
|
742
|
+
console.log(` ${item.channel.padEnd(12)} ${formatGatewayDoctorStatus(item.status)}`);
|
|
743
|
+
}
|
|
744
|
+
console.log(` (live token/webhook probes: ${BRAND.cliName} gateway doctor)`);
|
|
418
745
|
console.log(`\nredacted config:\n${JSON.stringify(redactGatewayConfig(cfg), null, 2)}`);
|
|
419
746
|
}
|
|
420
747
|
async function runGatewaySetup(args) {
|
|
@@ -1604,6 +1931,8 @@ async function runGateway(args) {
|
|
|
1604
1931
|
const [action, ...rest] = args;
|
|
1605
1932
|
if (!action || action === 'status' || action === 'list')
|
|
1606
1933
|
return runGatewayStatus();
|
|
1934
|
+
if (action === 'doctor')
|
|
1935
|
+
return runGatewayDoctor();
|
|
1607
1936
|
if (action === 'setup')
|
|
1608
1937
|
return runGatewaySetup(rest);
|
|
1609
1938
|
if (action === 'run') {
|
|
@@ -1651,7 +1980,7 @@ async function runGateway(args) {
|
|
|
1651
1980
|
console.log(removed.length ? `ลบ service files:\n${removed.map((p) => ` ${p}`).join('\n')}` : 'ไม่พบ service file ที่ต้องลบ');
|
|
1652
1981
|
return;
|
|
1653
1982
|
}
|
|
1654
|
-
console.error(`ไม่รู้จัก: gateway ${action} — ใช้ setup / run / start / stop / restart / install / status`);
|
|
1983
|
+
console.error(`ไม่รู้จัก: gateway ${action} — ใช้ setup / run / start / stop / restart / install / doctor / status`);
|
|
1655
1984
|
process.exit(1);
|
|
1656
1985
|
}
|
|
1657
1986
|
async function runStatus() {
|
|
@@ -1989,6 +2318,17 @@ async function runSessions(args) {
|
|
|
1989
2318
|
console.error(`ไม่รู้จัก: sessions ${action}\n${sessionUsage()}`);
|
|
1990
2319
|
process.exit(1);
|
|
1991
2320
|
}
|
|
2321
|
+
async function runUsage(args) {
|
|
2322
|
+
const { parseUsageArgs, renderUsageReport, usageHelpText } = await import('./usage-cli.js');
|
|
2323
|
+
const parsed = parseUsageArgs(args);
|
|
2324
|
+
if (!parsed) {
|
|
2325
|
+
console.log(usageHelpText());
|
|
2326
|
+
if (args.length && !args.includes('-h') && !args.includes('--help'))
|
|
2327
|
+
process.exit(2);
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
console.log(await renderUsageReport(parsed));
|
|
2331
|
+
}
|
|
1992
2332
|
async function runInsights(args) {
|
|
1993
2333
|
const { parseInsightsArgs } = await import('./insights-args.js');
|
|
1994
2334
|
const parsed = parseInsightsArgs(args);
|
|
@@ -2308,7 +2648,7 @@ providers: ${providerIds()}`);
|
|
|
2308
2648
|
process.exit(1);
|
|
2309
2649
|
}
|
|
2310
2650
|
const history = resumeSession?.messages ?? (await requestedContinuationHistory(args));
|
|
2311
|
-
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, parsed.planMode, parsed.yes || yolo ? 'auto' : safeMode ? 'ask' : config.permissionMode, parsed.quiet, config.fallbackModel);
|
|
2651
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, parsed.planMode, parsed.yes || yolo ? 'auto' : safeMode ? 'ask' : config.permissionMode, parsed.quiet, config.fallbackModel, parsed.planMode ? prompt : undefined);
|
|
2312
2652
|
}
|
|
2313
2653
|
async function runPureOneShot(args) {
|
|
2314
2654
|
const rest = args;
|
|
@@ -2328,7 +2668,7 @@ async function runPureOneShot(args) {
|
|
|
2328
2668
|
process.exit(1);
|
|
2329
2669
|
}
|
|
2330
2670
|
const history = resumeSession?.messages ?? (await requestedContinuationHistory(rest));
|
|
2331
|
-
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, parsed.planMode, parsed.yes ? 'auto' : config.permissionMode, true, config.fallbackModel);
|
|
2671
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, parsed.planMode, parsed.yes ? 'auto' : config.permissionMode, true, config.fallbackModel, parsed.planMode ? prompt : undefined);
|
|
2332
2672
|
}
|
|
2333
2673
|
async function runSend(args) {
|
|
2334
2674
|
const json = args.includes('--json');
|
|
@@ -2686,19 +3026,31 @@ async function runCron(args) {
|
|
|
2686
3026
|
console.error(`ไม่รู้จัก: cron ${action} — ใช้ add / list / rm`);
|
|
2687
3027
|
process.exit(1);
|
|
2688
3028
|
}
|
|
2689
|
-
/** sanook
|
|
3029
|
+
/** sanook init [--trust] — scaffold project .sanook/commands + onboarding hints */
|
|
3030
|
+
async function runInit(args) {
|
|
3031
|
+
const trust = args.includes('--trust');
|
|
3032
|
+
const unknown = args.filter((a) => a !== '--trust');
|
|
3033
|
+
if (unknown.length) {
|
|
3034
|
+
console.error(`ใช้: ${BRAND.cliName} init [--trust]`);
|
|
3035
|
+
process.exit(1);
|
|
3036
|
+
}
|
|
3037
|
+
const { initProject, formatInitResult } = await import('./project-init.js');
|
|
3038
|
+
const result = await initProject({ trust });
|
|
3039
|
+
console.log(formatInitResult(result));
|
|
3040
|
+
}
|
|
3041
|
+
/** sanook skill list | install <name|path> | add <source> | remove <name> */
|
|
2690
3042
|
async function runSkill(args) {
|
|
2691
3043
|
const [action, ...rest] = args;
|
|
2692
|
-
if (action === 'add') {
|
|
3044
|
+
if (action === 'add' || action === 'install') {
|
|
2693
3045
|
const source = rest[0];
|
|
2694
3046
|
if (!source) {
|
|
2695
|
-
console.error(
|
|
3047
|
+
console.error(`ใช้: ${BRAND.cliName} skill install <bundled-name|path> หรือ ${BRAND.cliName} skill add <github "user/repo" | URL ของ SKILL.md | local path>`);
|
|
2696
3048
|
process.exit(1);
|
|
2697
3049
|
}
|
|
2698
3050
|
console.error(`${DIM}⚠ skill = instruction ที่ AI จะทำตาม — ติดตั้งจาก source ที่เชื่อถือเท่านั้น${RESET}`);
|
|
2699
|
-
const {
|
|
3051
|
+
const { installNamedSkill } = await import('./skill-install.js');
|
|
2700
3052
|
try {
|
|
2701
|
-
const installed = await
|
|
3053
|
+
const installed = await installNamedSkill(source, (m) => process.stderr.write(`${DIM}${m}${RESET}\n`));
|
|
2702
3054
|
console.log(`ติดตั้ง ${installed.length} skill: ${installed.map((s) => s.name).join(', ')}`);
|
|
2703
3055
|
}
|
|
2704
3056
|
catch (e) {
|
|
@@ -2793,13 +3145,15 @@ async function runBrainContext(args) {
|
|
|
2793
3145
|
const parsed = parseBrainContextArgs(args);
|
|
2794
3146
|
if (!parsed.ok) {
|
|
2795
3147
|
console.error(parsed.message);
|
|
2796
|
-
console.error(`ใช้: ${BRAND.cliName} brain context [--task "..."] [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,session,skill] [--no-content]`);
|
|
3148
|
+
console.error(`ใช้: ${BRAND.cliName} brain context [--task "..."] [--project <slug>] [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,session,skill] [--no-content]`);
|
|
2797
3149
|
process.exit(1);
|
|
2798
3150
|
}
|
|
2799
3151
|
const cfg = await loadConfig({});
|
|
2800
3152
|
const report = await inspectBrainContext({
|
|
2801
3153
|
brainPath: cfg.brainPath,
|
|
2802
3154
|
task: parsed.value.task,
|
|
3155
|
+
cwd: process.cwd(),
|
|
3156
|
+
projectSlug: parsed.value.project,
|
|
2803
3157
|
mode: parsed.value.mode,
|
|
2804
3158
|
limit: parsed.value.limit,
|
|
2805
3159
|
sources: parsed.value.sources,
|
|
@@ -2842,6 +3196,130 @@ async function runBrainReview(args) {
|
|
|
2842
3196
|
if (!report.ok)
|
|
2843
3197
|
process.exit(1);
|
|
2844
3198
|
}
|
|
3199
|
+
/** sanook brain consolidate [--apply] [--archive] [--memory] — sleep-time consolidation runner */
|
|
3200
|
+
async function runBrainConsolidate(args) {
|
|
3201
|
+
const { parseBrainConsolidateArgs, runBrainConsolidate: consolidate, formatBrainConsolidateReport } = await import('./brain-consolidate.js');
|
|
3202
|
+
const parsed = parseBrainConsolidateArgs(args);
|
|
3203
|
+
if (!parsed.ok) {
|
|
3204
|
+
console.error(parsed.message);
|
|
3205
|
+
console.error(`ใช้: ${BRAND.cliName} brain consolidate [--apply] [--apply --archive] [--memory] [--no-retrieval]`);
|
|
3206
|
+
process.exit(1);
|
|
3207
|
+
}
|
|
3208
|
+
const cfg = await loadConfig({});
|
|
3209
|
+
const report = await consolidate({
|
|
3210
|
+
brainPath: cfg.brainPath,
|
|
3211
|
+
apply: parsed.value.apply,
|
|
3212
|
+
archive: parsed.value.archive,
|
|
3213
|
+
memory: parsed.value.memory,
|
|
3214
|
+
runRetrieval: parsed.value.runRetrieval,
|
|
3215
|
+
});
|
|
3216
|
+
console.log(formatBrainConsolidateReport(report));
|
|
3217
|
+
if (!report.ok)
|
|
3218
|
+
process.exit(1);
|
|
3219
|
+
}
|
|
3220
|
+
/** sanook brain metrics [--no-retrieval] — vault counts, stale notes, index + retrieval coverage */
|
|
3221
|
+
async function runBrainMetrics(args) {
|
|
3222
|
+
const allowed = new Set(['--no-retrieval']);
|
|
3223
|
+
const unknown = args.filter((arg) => !allowed.has(arg));
|
|
3224
|
+
if (unknown.length) {
|
|
3225
|
+
console.error(`ไม่รู้จัก option: ${unknown.join(' ')}`);
|
|
3226
|
+
console.error(`ใช้: ${BRAND.cliName} brain metrics [--no-retrieval]`);
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
}
|
|
3229
|
+
const cfg = await loadConfig({});
|
|
3230
|
+
const { collectBrainMetrics, formatBrainMetricsReport } = await import('./brain-metrics.js');
|
|
3231
|
+
const report = await collectBrainMetrics({ brainPath: cfg.brainPath, runRetrievalEval: !args.includes('--no-retrieval') });
|
|
3232
|
+
console.log(formatBrainMetricsReport(report));
|
|
3233
|
+
if (!report.ok)
|
|
3234
|
+
process.exit(1);
|
|
3235
|
+
}
|
|
3236
|
+
/** sanook brain projects list — show vault project workspaces and repo_path mappings */
|
|
3237
|
+
async function runBrainProjects(args) {
|
|
3238
|
+
if (args[0] && args[0] !== 'list') {
|
|
3239
|
+
console.error(`ใช้: ${BRAND.cliName} brain projects list`);
|
|
3240
|
+
process.exit(1);
|
|
3241
|
+
}
|
|
3242
|
+
const cfg = await loadConfig({});
|
|
3243
|
+
if (!cfg.brainPath) {
|
|
3244
|
+
console.error('ยังไม่ได้ตั้ง brainPath — รัน `sanook brain init [path]` ก่อน');
|
|
3245
|
+
process.exit(1);
|
|
3246
|
+
}
|
|
3247
|
+
const { listVaultProjects, formatVaultProjectLine, resolveVaultProject } = await import('./project-registry.js');
|
|
3248
|
+
const projects = await listVaultProjects(cfg.brainPath);
|
|
3249
|
+
const active = await resolveVaultProject({ brainPath: cfg.brainPath, cwd: process.cwd() });
|
|
3250
|
+
console.log(`${BRAND.productName} brain projects`);
|
|
3251
|
+
console.log(`vault: ${cfg.brainPath}`);
|
|
3252
|
+
console.log(`cwd: ${process.cwd()}${active ? ` → active: ${active.slug}` : ''}`);
|
|
3253
|
+
if (!projects.length) {
|
|
3254
|
+
console.log('(no project workspaces — run `sanook brain new project --title "..." --repo /path`)');
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
console.log('\nslug repo_path');
|
|
3258
|
+
for (const project of projects) {
|
|
3259
|
+
const marker = active?.slug === project.slug ? ' *' : '';
|
|
3260
|
+
console.log(`${formatVaultProjectLine(project)}${marker}`);
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
/** sanook brain pack list|show <name> — inspect Shared/Context-Packs/ bundles */
|
|
3264
|
+
async function runBrainPack(args) {
|
|
3265
|
+
const { parseBrainPackArgs, listContextPacks, showContextPack, formatBrainPackListReport, formatBrainPackShowReport } = await import('./brain-pack.js');
|
|
3266
|
+
const parsed = parseBrainPackArgs(args);
|
|
3267
|
+
if (!parsed.ok) {
|
|
3268
|
+
console.error(parsed.message);
|
|
3269
|
+
console.error(`ใช้: ${BRAND.cliName} brain pack list | ${BRAND.cliName} brain pack show <name>`);
|
|
3270
|
+
process.exit(1);
|
|
3271
|
+
}
|
|
3272
|
+
const cfg = await loadConfig({});
|
|
3273
|
+
if (!cfg.brainPath) {
|
|
3274
|
+
console.error('ยังไม่ได้ตั้ง brainPath — รัน `sanook brain init [path]` ก่อน');
|
|
3275
|
+
process.exit(1);
|
|
3276
|
+
}
|
|
3277
|
+
if (parsed.action === 'list') {
|
|
3278
|
+
const report = await listContextPacks(cfg.brainPath);
|
|
3279
|
+
console.log(formatBrainPackListReport(report));
|
|
3280
|
+
if (!report.ok)
|
|
3281
|
+
process.exit(1);
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
const report = await showContextPack(cfg.brainPath, parsed.name);
|
|
3285
|
+
console.log(formatBrainPackShowReport(report));
|
|
3286
|
+
if (!report.ok)
|
|
3287
|
+
process.exit(1);
|
|
3288
|
+
}
|
|
3289
|
+
/** sanook brain new <type> [--title "..."] — template-backed note creation in the correct vault folder */
|
|
3290
|
+
async function runBrainNew(args) {
|
|
3291
|
+
const { parseBrainNewArgs, createBrainNote, formatBrainNewReport } = await import('./brain-new.js');
|
|
3292
|
+
const parsed = parseBrainNewArgs(args);
|
|
3293
|
+
if (!parsed.ok) {
|
|
3294
|
+
console.error(parsed.message);
|
|
3295
|
+
console.error(`ใช้: ${BRAND.cliName} brain new <session|bug|handoff|project|golden-case|checklist> [--title "..."] [--repo /path] [--verify "..."] [--output path] [--force]`);
|
|
3296
|
+
process.exit(1);
|
|
3297
|
+
}
|
|
3298
|
+
const cfg = await loadConfig({});
|
|
3299
|
+
const report = await createBrainNote({
|
|
3300
|
+
brainPath: cfg.brainPath,
|
|
3301
|
+
today: new Date().toISOString().slice(0, 10),
|
|
3302
|
+
...parsed.value,
|
|
3303
|
+
});
|
|
3304
|
+
console.log(formatBrainNewReport(report));
|
|
3305
|
+
if (!report.ok)
|
|
3306
|
+
process.exit(1);
|
|
3307
|
+
}
|
|
3308
|
+
/** sanook brain repair [--dry-run] — apply safe one-line fixes after doctor/review findings */
|
|
3309
|
+
async function runBrainRepair(args) {
|
|
3310
|
+
const { parseBrainRepairArgs, repairBrain, formatBrainRepairReport } = await import('./brain-repair.js');
|
|
3311
|
+
const parsed = parseBrainRepairArgs(args);
|
|
3312
|
+
if (!parsed.ok) {
|
|
3313
|
+
console.error(parsed.message);
|
|
3314
|
+
console.error(`ใช้: ${BRAND.cliName} brain repair [--dry-run]`);
|
|
3315
|
+
process.exit(1);
|
|
3316
|
+
}
|
|
3317
|
+
const cfg = await loadConfig({});
|
|
3318
|
+
const report = await repairBrain({ brainPath: cfg.brainPath, dryRun: parsed.dryRun });
|
|
3319
|
+
console.log(formatBrainRepairReport(report));
|
|
3320
|
+
if (!report.ok)
|
|
3321
|
+
process.exit(1);
|
|
3322
|
+
}
|
|
2845
3323
|
/** sanook brain final [--task "..."] [--from-diff] [--lite] — create an evidence-backed closeout note */
|
|
2846
3324
|
async function runBrainFinal(args) {
|
|
2847
3325
|
const { parseBrainFinalArgs, createBrainFinal, formatBrainFinalReport } = await import('./brain-final.js');
|
|
@@ -2871,16 +3349,39 @@ async function runBrain(args) {
|
|
|
2871
3349
|
return runBrainEval(args.slice(1));
|
|
2872
3350
|
if (args[0] === 'review')
|
|
2873
3351
|
return runBrainReview(args.slice(1));
|
|
3352
|
+
if (args[0] === 'projects')
|
|
3353
|
+
return runBrainProjects(args.slice(1));
|
|
3354
|
+
if (args[0] === 'pack')
|
|
3355
|
+
return runBrainPack(args.slice(1));
|
|
3356
|
+
if (args[0] === 'new')
|
|
3357
|
+
return runBrainNew(args.slice(1));
|
|
3358
|
+
if (args[0] === 'repair')
|
|
3359
|
+
return runBrainRepair(args.slice(1));
|
|
3360
|
+
if (args[0] === 'consolidate')
|
|
3361
|
+
return runBrainConsolidate(args.slice(1));
|
|
3362
|
+
if (args[0] === 'metrics')
|
|
3363
|
+
return runBrainMetrics(args.slice(1));
|
|
2874
3364
|
if (args[0] === 'final')
|
|
2875
3365
|
return runBrainFinal(args.slice(1));
|
|
2876
3366
|
if (args[0] !== 'init') {
|
|
2877
3367
|
console.log(`ใช้:
|
|
2878
3368
|
sanook brain init [path] สร้างโครงสร้าง second-brain (Obsidian vault)
|
|
2879
3369
|
sanook brain doctor ตรวจ health ของ second-brain ที่ config.brainPath
|
|
2880
|
-
sanook brain context แสดง context ที่ Sanook จะ inject
|
|
3370
|
+
sanook brain context แสดง context ที่ Sanook จะ inject (project auto-detect จาก cwd)
|
|
2881
3371
|
sanook brain context --task "..." ดู retrieval hits ต่อ task
|
|
3372
|
+
sanook brain context --project <slug> บังคับ project workspace
|
|
3373
|
+
sanook brain projects list แสดง Projects/<slug>/ + repo_path
|
|
2882
3374
|
sanook brain eval รัน second-brain benchmark sanity checks
|
|
2883
3375
|
sanook brain review curator review: inbox, packs, sessions, evals, note hygiene
|
|
3376
|
+
sanook brain pack list แสดง context packs ใน Shared/Context-Packs/
|
|
3377
|
+
sanook brain pack show <name> แสดง load order / done criteria ของ pack
|
|
3378
|
+
sanook brain new project --title "..." --repo /path [--verify "..."]
|
|
3379
|
+
sanook brain new <type> [--title "..."] สร้างโน้ตจาก template
|
|
3380
|
+
sanook brain repair [--dry-run] แก้ one-line fixes หลัง doctor/review
|
|
3381
|
+
sanook brain consolidate [--apply] [--apply --archive] [--memory]
|
|
3382
|
+
sleep-time consolidation (dry-run default)
|
|
3383
|
+
sanook brain metrics [--no-retrieval]
|
|
3384
|
+
vault counts, stale notes, index freshness, retrieval coverage
|
|
2884
3385
|
sanook brain final --task "..." [--from-diff] [--lite]
|
|
2885
3386
|
สร้าง final gate note ใน Sessions
|
|
2886
3387
|
|
|
@@ -2898,7 +3399,9 @@ async function runBrain(args) {
|
|
|
2898
3399
|
return;
|
|
2899
3400
|
}
|
|
2900
3401
|
const { scaffoldBrain, BRAIN_DEFAULTS, expandHome, wireBrainMcp } = await import('./brain.js');
|
|
2901
|
-
|
|
3402
|
+
// resolve to absolute before persisting — getBrainPath() is later read from an arbitrary cwd,
|
|
3403
|
+
// so a relative path (e.g. "./vault") would resolve differently per run
|
|
3404
|
+
const target = resolve(expandHome(pathArg ?? join(homedir(), 'Documents', BRAIN_DEFAULTS.vaultName)));
|
|
2902
3405
|
const today = new Date().toISOString().slice(0, 10);
|
|
2903
3406
|
try {
|
|
2904
3407
|
const res = await scaffoldBrain(target, { ...BRAIN_DEFAULTS, today });
|
|
@@ -2985,11 +3488,16 @@ async function runConfig(args) {
|
|
|
2985
3488
|
console.error('contextCompression ต้องเป็น off, selective หรือ headroom');
|
|
2986
3489
|
process.exit(1);
|
|
2987
3490
|
}
|
|
3491
|
+
else if (key === 'brainPath') {
|
|
3492
|
+
// store absolute — getBrainPath() is read from arbitrary cwd, so a relative path drifts
|
|
3493
|
+
const { expandHome } = await import('./brain.js');
|
|
3494
|
+
value = resolve(expandHome(raw.trim()));
|
|
3495
|
+
}
|
|
2988
3496
|
else if (key === 'thinking') {
|
|
2989
3497
|
// เก็บเป็น number (budget) หรือ boolean ให้ตรง ConfigSchema (ไม่เก็บ string)
|
|
2990
3498
|
value = parseThinkingConfigValue(raw);
|
|
2991
3499
|
if (value === undefined) {
|
|
2992
|
-
console.error('thinking ต้องเป็น on/off หรือ budget tokens (integer บวก เช่น 4000)');
|
|
3500
|
+
console.error('thinking ต้องเป็น on/off, true/false, yes/no หรือ budget tokens (integer บวก เช่น 4000)');
|
|
2993
3501
|
process.exit(1);
|
|
2994
3502
|
}
|
|
2995
3503
|
}
|
|
@@ -3065,7 +3573,7 @@ async function runMcpServe() {
|
|
|
3065
3573
|
const { runMcpServer } = await import('./mcp-server.js');
|
|
3066
3574
|
await runMcpServer();
|
|
3067
3575
|
}
|
|
3068
|
-
/** sanook mcp [search|info|install|test|doctor|list|add|remove] — จัดการ ~/.sanook/mcp.json */
|
|
3576
|
+
/** sanook mcp [search|info|install|test|doctor|enable|disable|list|add|remove] — จัดการ ~/.sanook/mcp.json */
|
|
3069
3577
|
async function runMcp(args) {
|
|
3070
3578
|
const readConfig = async (path) => {
|
|
3071
3579
|
try {
|
|
@@ -3132,34 +3640,22 @@ async function runMcp(args) {
|
|
|
3132
3640
|
return;
|
|
3133
3641
|
}
|
|
3134
3642
|
if (action === 'install') {
|
|
3135
|
-
const { buildMcpInstallPlan, getMcpRegistryServer, parseKeyValueList, formatRegistryInfo, } = await import('./mcp-registry.js');
|
|
3136
|
-
const
|
|
3137
|
-
if (!
|
|
3138
|
-
console.error(
|
|
3643
|
+
const { buildMcpInstallPlan, getMcpRegistryServer, parseKeyValueList, formatRegistryInfo, parseMcpRegistryInstallArgs, } = await import('./mcp-registry.js');
|
|
3644
|
+
const parsedInstall = parseMcpRegistryInstallArgs(rest);
|
|
3645
|
+
if (!parsedInstall.ok) {
|
|
3646
|
+
console.error(parsedInstall.message);
|
|
3139
3647
|
process.exit(1);
|
|
3140
3648
|
}
|
|
3141
|
-
const
|
|
3142
|
-
const out = [];
|
|
3143
|
-
for (let i = 0; i < rest.length; i++) {
|
|
3144
|
-
if (rest[i] === flag && rest[i + 1])
|
|
3145
|
-
out.push(rest[++i]);
|
|
3146
|
-
else if (rest[i].startsWith(`${flag}=`))
|
|
3147
|
-
out.push(rest[i].slice(flag.length + 1));
|
|
3148
|
-
}
|
|
3149
|
-
return out;
|
|
3150
|
-
};
|
|
3151
|
-
const valueOf = (flag) => optionValues(flag)[0];
|
|
3152
|
-
const alias = valueOf('--name');
|
|
3649
|
+
const { name, alias, transport, version, env, headers, project } = parsedInstall.value;
|
|
3153
3650
|
if (alias && !isValidMcpServerName(alias)) {
|
|
3154
3651
|
console.error('ชื่อ MCP server ต้องเป็น a-z/A-Z/0-9/_/- ความยาวไม่เกิน 64 และห้ามใช้ชื่อพิเศษ');
|
|
3155
3652
|
process.exit(1);
|
|
3156
3653
|
}
|
|
3157
|
-
const transport = valueOf('--transport');
|
|
3158
3654
|
if (transport && !['auto', 'remote', 'stdio'].includes(transport)) {
|
|
3159
3655
|
console.error('--transport ต้องเป็น auto, remote, หรือ stdio');
|
|
3160
3656
|
process.exit(1);
|
|
3161
3657
|
}
|
|
3162
|
-
const server = await getMcpRegistryServer(name, { version
|
|
3658
|
+
const server = await getMcpRegistryServer(name, { version });
|
|
3163
3659
|
if (!server) {
|
|
3164
3660
|
console.error(`ไม่เจอ MCP registry server: ${name}`);
|
|
3165
3661
|
process.exit(1);
|
|
@@ -3167,8 +3663,8 @@ async function runMcp(args) {
|
|
|
3167
3663
|
const plan = buildMcpInstallPlan(server, {
|
|
3168
3664
|
alias,
|
|
3169
3665
|
transport,
|
|
3170
|
-
env: parseKeyValueList(
|
|
3171
|
-
headers: parseKeyValueList(
|
|
3666
|
+
env: parseKeyValueList(env),
|
|
3667
|
+
headers: parseKeyValueList(headers),
|
|
3172
3668
|
});
|
|
3173
3669
|
if (!plan.ok) {
|
|
3174
3670
|
console.log(formatRegistryInfo(server));
|
|
@@ -3182,20 +3678,25 @@ async function runMcp(args) {
|
|
|
3182
3678
|
process.exit(1);
|
|
3183
3679
|
}
|
|
3184
3680
|
let targetPath = mcpPath;
|
|
3185
|
-
if (
|
|
3186
|
-
|
|
3681
|
+
if (project) {
|
|
3682
|
+
// Use trust status (not projectConfigPathIfTrusted, which requires a PRE-EXISTING file) so the
|
|
3683
|
+
// FIRST project-scoped install into a trusted project can create .sanook/mcp.json.
|
|
3684
|
+
const { projectTrustStatus, projectRoot } = await import('./trust.js');
|
|
3685
|
+
const { appProjectPath } = await import('./brand.js');
|
|
3187
3686
|
const root = await projectRoot(process.cwd());
|
|
3188
|
-
const
|
|
3189
|
-
if (!
|
|
3687
|
+
const trust = await projectTrustStatus(root);
|
|
3688
|
+
if (!trust.trusted) {
|
|
3190
3689
|
console.error(`project MCP ต้อง trust ก่อน: ${BRAND.cliName} trust add`);
|
|
3191
3690
|
process.exit(1);
|
|
3192
3691
|
}
|
|
3193
|
-
targetPath =
|
|
3692
|
+
targetPath = appProjectPath(root, 'mcp.json'); // may not exist yet — writeConfig creates it
|
|
3194
3693
|
}
|
|
3195
3694
|
cfg = await readConfig(targetPath);
|
|
3196
3695
|
cfg.mcpServers[plan.alias] = plan.config;
|
|
3197
3696
|
await writeConfig(targetPath, cfg);
|
|
3198
3697
|
console.log(`ติดตั้ง MCP "${plan.alias}" จาก ${server.name} (${plan.source}) → ${targetPath}`);
|
|
3698
|
+
const { inferRegistryServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3699
|
+
console.log(`risk: ${formatMcpRiskLabel(inferRegistryServerRisk(server))}`);
|
|
3199
3700
|
if (plan.requirements.length)
|
|
3200
3701
|
console.log(`requirements: ${plan.requirements.join(', ')}`);
|
|
3201
3702
|
for (const warning of plan.warnings)
|
|
@@ -3204,10 +3705,16 @@ async function runMcp(args) {
|
|
|
3204
3705
|
return;
|
|
3205
3706
|
}
|
|
3206
3707
|
if (action === 'test' || action === 'doctor') {
|
|
3207
|
-
const { loadMcpConfig, probeMcpServer } = await import('./mcp.js');
|
|
3708
|
+
const { loadMcpConfig, probeMcpServer, isMcpServerEnabled } = await import('./mcp.js');
|
|
3709
|
+
const { inferConfiguredServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3208
3710
|
const logs = [];
|
|
3209
3711
|
const merged = await loadMcpConfig((m) => logs.push(m));
|
|
3210
|
-
const
|
|
3712
|
+
const explicitName = action === 'test' && rest[0] ? rest[0] : undefined;
|
|
3713
|
+
const names = explicitName
|
|
3714
|
+
? [explicitName]
|
|
3715
|
+
: action === 'test'
|
|
3716
|
+
? Object.keys(merged).filter((n) => isMcpServerEnabled(merged[n]))
|
|
3717
|
+
: Object.keys(merged);
|
|
3211
3718
|
if (!names.length) {
|
|
3212
3719
|
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp search github`);
|
|
3213
3720
|
return;
|
|
@@ -3223,9 +3730,14 @@ async function runMcp(args) {
|
|
|
3223
3730
|
console.log(`[FAIL] ${n} — ไม่เจอใน config`);
|
|
3224
3731
|
continue;
|
|
3225
3732
|
}
|
|
3733
|
+
if (!explicitName && action === 'doctor' && !isMcpServerEnabled(server)) {
|
|
3734
|
+
console.log(`[SKIP] ${n} (disabled)`);
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3226
3737
|
const probe = await probeMcpServer(server);
|
|
3738
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, server, probe.tools));
|
|
3227
3739
|
if (probe.ok) {
|
|
3228
|
-
console.log(`[PASS] ${n} (${probe.transport}) — ${probe.tools.length} tool(s)`);
|
|
3740
|
+
console.log(`[PASS] ${n} (${probe.transport}) — ${probe.tools.length} tool(s) · risk: ${risk}`);
|
|
3229
3741
|
for (const tool of probe.tools.slice(0, action === 'doctor' ? 8 : 30))
|
|
3230
3742
|
console.log(` - ${tool.name}${tool.description ? ` — ${tool.description}` : ''}`);
|
|
3231
3743
|
if (action === 'doctor' && probe.tools.length > 8)
|
|
@@ -3233,13 +3745,39 @@ async function runMcp(args) {
|
|
|
3233
3745
|
}
|
|
3234
3746
|
else {
|
|
3235
3747
|
failed = true;
|
|
3236
|
-
console.log(`[FAIL] ${n} (${probe.transport}) — ${probe.error}`);
|
|
3748
|
+
console.log(`[FAIL] ${n} (${probe.transport}) — ${probe.error} · risk: ${risk}`);
|
|
3749
|
+
if (probe.authHints?.length)
|
|
3750
|
+
for (const hint of probe.authHints)
|
|
3751
|
+
console.log(` hint: ${hint}`);
|
|
3237
3752
|
}
|
|
3238
3753
|
}
|
|
3239
3754
|
if (failed)
|
|
3240
3755
|
process.exit(1);
|
|
3241
3756
|
return;
|
|
3242
3757
|
}
|
|
3758
|
+
if (action === 'enable' || action === 'disable') {
|
|
3759
|
+
const { findMcpServerConfigPath } = await import('./mcp.js');
|
|
3760
|
+
const [name] = rest;
|
|
3761
|
+
if (!name) {
|
|
3762
|
+
console.error(`ใช้: ${BRAND.cliName} mcp ${action} <name>`);
|
|
3763
|
+
process.exit(1);
|
|
3764
|
+
}
|
|
3765
|
+
const targetPath = await findMcpServerConfigPath(name);
|
|
3766
|
+
if (!targetPath) {
|
|
3767
|
+
console.error(`ไม่เจอ MCP server "${name}" ใน global หรือ trusted project config`);
|
|
3768
|
+
process.exit(1);
|
|
3769
|
+
}
|
|
3770
|
+
const targetCfg = await readConfig(targetPath);
|
|
3771
|
+
if (!targetCfg.mcpServers[name]) {
|
|
3772
|
+
console.error(`ไม่เจอ MCP server "${name}" ใน ${targetPath}`);
|
|
3773
|
+
process.exit(1);
|
|
3774
|
+
}
|
|
3775
|
+
targetCfg.mcpServers[name] = { ...targetCfg.mcpServers[name], enabled: action === 'enable' };
|
|
3776
|
+
await writeConfig(targetPath, targetCfg);
|
|
3777
|
+
console.log(`${action === 'enable' ? 'เปิด' : 'ปิด'} MCP server "${name}" แล้ว → ${targetPath}`);
|
|
3778
|
+
console.log(`ทดสอบ: ${BRAND.cliName} mcp test ${name}`);
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3243
3781
|
if (action === 'add') {
|
|
3244
3782
|
const [name, command, ...cmdArgs] = rest;
|
|
3245
3783
|
if (!name || !command) {
|
|
@@ -3269,10 +3807,11 @@ async function runMcp(args) {
|
|
|
3269
3807
|
return;
|
|
3270
3808
|
}
|
|
3271
3809
|
if (action !== 'list') {
|
|
3272
|
-
console.log(`ใช้: ${BRAND.cliName} mcp [search|info|install|test|doctor|preset|list|add|remove|serve]`);
|
|
3810
|
+
console.log(`ใช้: ${BRAND.cliName} mcp [search|info|install|test|doctor|enable|disable|preset|list|add|remove|serve]`);
|
|
3273
3811
|
return;
|
|
3274
3812
|
}
|
|
3275
|
-
const { loadMcpConfig } = await import('./mcp.js');
|
|
3813
|
+
const { loadMcpConfig, isMcpServerEnabled } = await import('./mcp.js');
|
|
3814
|
+
const { inferConfiguredServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3276
3815
|
const logs = [];
|
|
3277
3816
|
const merged = await loadMcpConfig((m) => logs.push(m));
|
|
3278
3817
|
const names = Object.keys(merged);
|
|
@@ -3286,13 +3825,23 @@ async function runMcp(args) {
|
|
|
3286
3825
|
console.log(`${names.length} MCP servers:`);
|
|
3287
3826
|
for (const n of names) {
|
|
3288
3827
|
const s = merged[n];
|
|
3289
|
-
|
|
3828
|
+
const enabled = isMcpServerEnabled(s);
|
|
3829
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, s));
|
|
3830
|
+
console.log(` ${n}${enabled ? '' : ' (disabled)'} — risk: ${risk} — ${s.url ? `${s.url} (http)` : `${s.command} ${(s.args ?? []).join(' ')}`}`);
|
|
3290
3831
|
}
|
|
3291
3832
|
if (rest.includes('--tools')) {
|
|
3292
3833
|
const { probeMcpServer } = await import('./mcp.js');
|
|
3293
3834
|
for (const n of names) {
|
|
3835
|
+
if (!isMcpServerEnabled(merged[n])) {
|
|
3836
|
+
console.log(`\n[SKIP] ${n} tools (disabled)`);
|
|
3837
|
+
continue;
|
|
3838
|
+
}
|
|
3294
3839
|
const probe = await probeMcpServer(merged[n]);
|
|
3295
|
-
|
|
3840
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, merged[n], probe.tools));
|
|
3841
|
+
console.log(`\n${probe.ok ? '[PASS]' : '[FAIL]'} ${n} tools${probe.ok ? ` (${probe.tools.length})` : ` — ${probe.error}`} · risk: ${risk}`);
|
|
3842
|
+
if (probe.authHints?.length)
|
|
3843
|
+
for (const hint of probe.authHints)
|
|
3844
|
+
console.log(` hint: ${hint}`);
|
|
3296
3845
|
for (const tool of probe.tools.slice(0, 30))
|
|
3297
3846
|
console.log(` - ${tool.name}`);
|
|
3298
3847
|
}
|
|
@@ -3505,11 +4054,15 @@ async function main() {
|
|
|
3505
4054
|
process.exit(1);
|
|
3506
4055
|
}
|
|
3507
4056
|
const argv = process.argv.slice(2);
|
|
3508
|
-
|
|
4057
|
+
// --version/--help win in ANY position before `--` (so `sanook --help foo`, `-m x -h` work),
|
|
4058
|
+
// not only as the sole argument. (Quoted prompts like `sanook "explain --help"` are one token → unaffected.)
|
|
4059
|
+
const optArgsEnd = argv.indexOf('--');
|
|
4060
|
+
const topOpts = optArgsEnd === -1 ? argv : argv.slice(0, optArgsEnd);
|
|
4061
|
+
if (topOpts.includes('-v') || topOpts.includes('--version')) {
|
|
3509
4062
|
console.log(VERSION);
|
|
3510
4063
|
return;
|
|
3511
4064
|
}
|
|
3512
|
-
if (
|
|
4065
|
+
if (topOpts.includes('-h') || topOpts.includes('--help')) {
|
|
3513
4066
|
console.log(HELP);
|
|
3514
4067
|
return;
|
|
3515
4068
|
}
|
|
@@ -3529,6 +4082,8 @@ async function main() {
|
|
|
3529
4082
|
return runPureOneShot(argv.slice(1));
|
|
3530
4083
|
if (argv[0] === 'chat')
|
|
3531
4084
|
return runChat(argv.slice(1));
|
|
4085
|
+
if (argv[0] === 'plan')
|
|
4086
|
+
return runPlan(argv.slice(1));
|
|
3532
4087
|
if (argv[0] === 'setup')
|
|
3533
4088
|
return runSetup(argv.slice(1));
|
|
3534
4089
|
if (argv[0] === 'model' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
@@ -3541,10 +4096,23 @@ async function main() {
|
|
|
3541
4096
|
return runAuth(argv.slice(1));
|
|
3542
4097
|
if (argv[0] === 'sessions' || argv[0] === 'session')
|
|
3543
4098
|
return runSessions(argv.slice(1));
|
|
4099
|
+
if (argv[0] === 'usage')
|
|
4100
|
+
return runUsage(argv.slice(1));
|
|
3544
4101
|
if (argv[0] === 'insights')
|
|
3545
4102
|
return runInsights(argv.slice(1));
|
|
4103
|
+
if (argv[0] === 'memory' && ['log', 'stats', undefined].includes(argv[1]))
|
|
4104
|
+
return runMemory(argv.slice(1));
|
|
3546
4105
|
if (argv[0] === 'dump')
|
|
3547
4106
|
return runDump(argv.slice(1));
|
|
4107
|
+
if (argv[0] === 'prompt-size' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4108
|
+
return runPromptSize(argv.slice(1));
|
|
4109
|
+
if (argv[0] === 'runtimes' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4110
|
+
return runRuntimes(argv.slice(1));
|
|
4111
|
+
if (argv[0] === 'dashboard' && (argv.length === 1 || !argv[1].startsWith('-') || argv[1] === '--port')) {
|
|
4112
|
+
return runDashboard(argv.slice(1));
|
|
4113
|
+
}
|
|
4114
|
+
if (argv[0] === 'web' && ['status', 'doctor', 'fetch', 'search', 'setup', undefined].includes(argv[1]))
|
|
4115
|
+
return runWeb(argv.slice(1));
|
|
3548
4116
|
if (argv[0] === 'tools' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
3549
4117
|
return runTools(argv.slice(1));
|
|
3550
4118
|
if (argv[0] === 'send')
|
|
@@ -3557,12 +4125,15 @@ async function main() {
|
|
|
3557
4125
|
if (argv[0] === 'cron' && ['add', 'list', 'rm', 'remove', undefined].includes(argv[1])) {
|
|
3558
4126
|
return runCron(argv.slice(1));
|
|
3559
4127
|
}
|
|
3560
|
-
if (argv[0] === 'skill' && ['list', 'add', 'remove', 'rm', undefined].includes(argv[1])) {
|
|
4128
|
+
if (argv[0] === 'skill' && ['list', 'add', 'install', 'remove', 'rm', undefined].includes(argv[1])) {
|
|
3561
4129
|
return runSkill(argv.slice(1));
|
|
3562
4130
|
}
|
|
4131
|
+
if (argv[0] === 'init' && (argv.length === 1 || argv[1].startsWith('-')))
|
|
4132
|
+
return runInit(argv.slice(1));
|
|
3563
4133
|
if (argv[0] === 'models')
|
|
3564
4134
|
return runModels(argv.slice(1));
|
|
3565
|
-
if (argv[0] === 'brain' &&
|
|
4135
|
+
if (argv[0] === 'brain' &&
|
|
4136
|
+
['init', 'doctor', 'context', 'eval', 'review', 'projects', 'pack', 'new', 'repair', 'consolidate', 'metrics', 'final', undefined].includes(argv[1]))
|
|
3566
4137
|
return runBrain(argv.slice(1));
|
|
3567
4138
|
if (argv[0] === 'config' && ['get', 'set', 'list', undefined].includes(argv[1]))
|
|
3568
4139
|
return runConfig(argv.slice(1));
|
|
@@ -3572,11 +4143,21 @@ async function main() {
|
|
|
3572
4143
|
return runSearch(argv.slice(1));
|
|
3573
4144
|
if (argv[0] === 'mcp' && argv[1] === 'serve')
|
|
3574
4145
|
return runMcpServe();
|
|
3575
|
-
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', 'search', 'info', 'install', 'test', 'doctor', 'preset', undefined].includes(argv[1]))
|
|
4146
|
+
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', 'search', 'info', 'install', 'test', 'doctor', 'enable', 'disable', 'preset', undefined].includes(argv[1]))
|
|
3576
4147
|
return runMcp(argv.slice(1));
|
|
3577
4148
|
if (argv[0] === 'trust' && ['status', 'add', 'remove', 'rm', undefined].includes(argv[1]))
|
|
3578
4149
|
return runTrust(argv.slice(1));
|
|
3579
|
-
|
|
4150
|
+
// A management command word whose subcommand didn't match any route above → don't silently
|
|
4151
|
+
// fall through and run it as an LLM task (costly + confusing). These words intentionally
|
|
4152
|
+
// require a valid subcommand; an NL prompt starting with one can still be quoted as a single arg.
|
|
4153
|
+
const MANAGEMENT_WORDS = new Set(['config', 'mcp', 'brain', 'web', 'trust', 'cron', 'skill', 'init', 'dashboard']);
|
|
4154
|
+
if (argv[0] && MANAGEMENT_WORDS.has(argv[0]) && argv[1] && !argv[1].startsWith('-')) {
|
|
4155
|
+
console.error(`${BRAND.cliName}: ไม่รู้จัก subcommand "${argv[0]} ${argv[1]}" — ดูวิธีใช้: ${BRAND.cliName} --help`);
|
|
4156
|
+
process.exit(1);
|
|
4157
|
+
}
|
|
4158
|
+
const { model, budget, budgetInvalid, json, quiet, prompt: argPrompt, planMode, yes, resume } = parseArgs(argv);
|
|
4159
|
+
if (budgetInvalid)
|
|
4160
|
+
process.stderr.write(`${BRAND.cliName}: ⚠ --budget ไม่ถูกต้อง (ต้องเป็นจำนวนบวก) — รันต่อโดยไม่มี spend cap\n`);
|
|
3580
4161
|
const resumeSession = await requestedResumeSession(argv, resume);
|
|
3581
4162
|
const budgetUsd = Number.isFinite(budget) ? budget : undefined;
|
|
3582
4163
|
// stdin piping: `git diff | sanook "review this"` → ผนวก stdin เข้า prompt (headless/CI)
|
|
@@ -3592,7 +4173,7 @@ async function main() {
|
|
|
3592
4173
|
}
|
|
3593
4174
|
// --continue / -c → โหลด session ล่าสุดมาต่อ (จำว่าทำถึงไหน)
|
|
3594
4175
|
const history = resumeSession?.messages ?? (await requestedContinuationHistory(argv));
|
|
3595
|
-
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode, quiet, config.fallbackModel);
|
|
4176
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode, quiet, config.fallbackModel, planMode ? prompt : undefined);
|
|
3596
4177
|
return;
|
|
3597
4178
|
}
|
|
3598
4179
|
await maybePromptForInteractiveUpdate();
|