sanook-cli 0.5.2 → 0.5.5
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 +91 -2
- package/README.md +15 -3
- package/README.th.md +8 -1
- package/dist/approval.js +7 -0
- package/dist/bin.js +623 -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-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/cli-args.js +47 -9
- package/dist/cli-option-values.js +1 -1
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +94 -14
- package/dist/config.js +31 -5
- package/dist/context-pack.js +145 -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 +30 -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 +34 -5
- 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/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-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 +835 -29
- 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 +20 -1
- 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/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/package.json +2 -2
- 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,19 @@ 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
|
|
97
135
|
${BRAND.cliName} dump [--show-keys] support snapshot (secrets redacted)
|
|
136
|
+
${BRAND.cliName} prompt-size [--json] inspect prompt/context budget without calling a model
|
|
137
|
+
${BRAND.cliName} runtimes [--json] inspect optional Python/Rust runtime surface
|
|
98
138
|
${BRAND.cliName} update update ${BRAND.cliName} to the latest npm release
|
|
99
139
|
${BRAND.cliName} doctor ตรวจการติดตั้ง + วิธีแก้ PATH (เมื่อพิมพ์ "${BRAND.cliName}" แล้วไม่เจอ)
|
|
100
140
|
|
|
@@ -119,6 +159,7 @@ gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
|
|
|
119
159
|
${BRAND.cliName} gateway start [--port 8787] เปิด gateway เป็น background process
|
|
120
160
|
${BRAND.cliName} gateway stop|restart|install จัดการ gateway service
|
|
121
161
|
${BRAND.cliName} gateway status ดู config/status gateway
|
|
162
|
+
${BRAND.cliName} gateway doctor ตรวจ token/webhook/allowlist ของ channels ที่ตั้งไว้
|
|
122
163
|
${BRAND.cliName} send --to telegram|discord|slack|mattermost|homeassistant|email|line|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams[:target] "msg" ส่งข้อความออก platform โดยไม่เรียก LLM
|
|
123
164
|
${BRAND.cliName} webhook subscribe <route> [--prompt "..."] [--to telegram|slack|mattermost|homeassistant|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams]
|
|
124
165
|
รับ event จาก GitHub/GitLab/Jira/Stripe แล้ว trigger agent/delivery
|
|
@@ -131,21 +172,36 @@ gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
|
|
|
131
172
|
|
|
132
173
|
skills (built-in + ติดตั้งเพิ่มได้):
|
|
133
174
|
${BRAND.cliName} skill list ดู skill ทั้งหมด
|
|
175
|
+
${BRAND.cliName} skill install <name|path> ติดตั้ง skill จาก bundled catalog / local path
|
|
134
176
|
${BRAND.cliName} skill add <user/repo|url|path> ติดตั้ง skill จาก GitHub / URL / local
|
|
135
177
|
${BRAND.cliName} skill remove <name> ลบ skill ที่ติดตั้ง
|
|
136
178
|
${BRAND.cliName} models [provider] ดู/verify model id (เทียบ provider จริงถ้ามี key)
|
|
137
179
|
|
|
180
|
+
project setup:
|
|
181
|
+
${BRAND.cliName} init [--trust] scaffold .sanook/commands + onboarding hints
|
|
182
|
+
|
|
138
183
|
second brain (Obsidian workspace สำหรับจัดเก็บงาน + ความจำ AI):
|
|
139
184
|
${BRAND.cliName} brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
|
|
140
185
|
${BRAND.cliName} brain doctor ตรวจ health ของ second-brain ที่ config.brainPath
|
|
141
|
-
${BRAND.cliName} brain context [--task "..."]
|
|
186
|
+
${BRAND.cliName} brain context [--task "..."] [--project <slug>] แสดง context ที่ Sanook จะ inject (+ project auto-detect จาก cwd)
|
|
187
|
+
${BRAND.cliName} brain projects list แสดง Projects/<slug>/ + repo_path mapping
|
|
142
188
|
${BRAND.cliName} brain eval รัน second-brain benchmark sanity checks
|
|
143
189
|
${BRAND.cliName} brain review curator review: inbox, packs, sessions, evals, note hygiene
|
|
190
|
+
${BRAND.cliName} brain pack list|show <name> ดู context packs ใน Shared/Context-Packs/
|
|
191
|
+
${BRAND.cliName} brain new project [--title "..."] [--repo /path] [--verify "..."] scaffold Projects/<slug>/ workspace
|
|
192
|
+
${BRAND.cliName} brain repair [--dry-run] แก้ one-line fixes หลัง doctor/review
|
|
193
|
+
${BRAND.cliName} brain consolidate [--apply] sleep-time consolidation (inbox, stale, retrieval; dry-run default)
|
|
194
|
+
${BRAND.cliName} brain metrics [--no-retrieval] vault counts, stale notes, index freshness, retrieval coverage
|
|
144
195
|
${BRAND.cliName} brain final --task "..." สร้าง final gate note ใน Sessions พร้อม evidence scaffold
|
|
145
196
|
|
|
146
197
|
search (BM25 + optional BYOK semantic เหนือ vault + memory + sessions + skills):
|
|
147
198
|
${BRAND.cliName} index (re)index vault+memory แบบ incremental (O(delta))
|
|
148
199
|
${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]
|
|
200
|
+
${BRAND.cliName} web status [--json] [--probe] ตรวจ true web/search readiness ผ่าน MCP (local search ไม่ใช่ internet)
|
|
201
|
+
${BRAND.cliName} web doctor [--json] probe web/search/fetch MCP candidates
|
|
202
|
+
${BRAND.cliName} web fetch <url> [--json] ดึงหน้าเว็บสาธารณะ + สรุปโครงสร้าง (fallback ladder ที่ถูกกติกา)
|
|
203
|
+
${BRAND.cliName} web search "<q>" [--limit N] ค้นเว็บผ่าน Tavily (ต้องมี TAVILY_API_KEY)
|
|
204
|
+
${BRAND.cliName} web setup tavily [--api-key K] ตั้งค่า Tavily (เขียน MCP + เก็บ key 0600)
|
|
149
205
|
${BRAND.cliName} mcp serve expose brain เป็น MCP server (stdio) ให้ Claude Desktop/Cursor
|
|
150
206
|
|
|
151
207
|
config & mcp:
|
|
@@ -153,10 +209,14 @@ config & mcp:
|
|
|
153
209
|
${BRAND.cliName} auth [list|status|add|remove] จัดการ API keys ของ providers (BYOK, redacted)
|
|
154
210
|
${BRAND.cliName} sessions [list|latest|show|rm] จัดการ saved sessions
|
|
155
211
|
${BRAND.cliName} insights [--days N] [--all] ดู usage/session insights ในเครื่อง
|
|
212
|
+
${BRAND.cliName} memory [stats|log "<q>"] ดู memory: สถิติ + วิวัฒนาการของ belief (เคยเชื่ออะไร ถูก supersede ตอนไหน)
|
|
156
213
|
${BRAND.cliName} dump [--show-keys] diagnostic/support dump แบบไม่โชว์ raw secret
|
|
214
|
+
${BRAND.cliName} prompt-size [--json] ดู system prompt / skills / brain / tools token budget แบบ offline
|
|
215
|
+
${BRAND.cliName} runtimes [--json] ดู Python/Rust optional runtime + บทบาทใน Sanook
|
|
216
|
+
${BRAND.cliName} web status [--json] ดู web-search/fetch readiness และ grounding policy
|
|
157
217
|
${BRAND.cliName} tools ดู tool surface ที่ agent ใช้ได้
|
|
158
218
|
${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
|
|
219
|
+
${BRAND.cliName} mcp [search|info|install|test|doctor|enable|disable|preset|list|add|remove] จัดการ MCP servers
|
|
160
220
|
${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
|
|
161
221
|
|
|
162
222
|
flags:
|
|
@@ -183,6 +243,10 @@ async function runServe(args) {
|
|
|
183
243
|
console.error(`port ไม่ถูกต้อง: ${parsed.portError}`);
|
|
184
244
|
process.exit(1);
|
|
185
245
|
}
|
|
246
|
+
if (parsed.modelError) {
|
|
247
|
+
console.error(`model ไม่ถูกต้อง: ${parsed.modelError}`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
186
250
|
const config = await loadConfig({ model: parsed.model });
|
|
187
251
|
const { startGateway } = await import('./gateway/serve.js');
|
|
188
252
|
process.stdout.write(`${DIM}${BRAND.productName} gateway — model: ${config.model}${RESET}\n`);
|
|
@@ -215,6 +279,35 @@ async function startModelSetup() {
|
|
|
215
279
|
},
|
|
216
280
|
});
|
|
217
281
|
}
|
|
282
|
+
async function runDashboard(args = []) {
|
|
283
|
+
let port = 9119;
|
|
284
|
+
for (let i = 0; i < args.length; i++) {
|
|
285
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
286
|
+
port = Number(args[++i]);
|
|
287
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
288
|
+
console.error(`${BRAND.cliName}: --port ต้องเป็นจำนวนบวก`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (args[i] === '-h' || args[i] === '--help') {
|
|
293
|
+
console.log(`ใช้: ${BRAND.cliName} dashboard [--port 9119]`);
|
|
294
|
+
console.log('เปิด Sanook Dashboard (local web UI — Hermes-style admin panel)');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const { startDashboardServer } = await import('./dashboard/server.js');
|
|
299
|
+
const stop = await startDashboardServer({
|
|
300
|
+
port,
|
|
301
|
+
onLog: (m) => console.log(m),
|
|
302
|
+
});
|
|
303
|
+
const shutdown = () => {
|
|
304
|
+
stop();
|
|
305
|
+
process.stdout.write('\n[dashboard] stopped\n');
|
|
306
|
+
process.exit(0);
|
|
307
|
+
};
|
|
308
|
+
process.on('SIGINT', shutdown);
|
|
309
|
+
process.on('SIGTERM', shutdown);
|
|
310
|
+
}
|
|
218
311
|
async function runTools(_args = []) {
|
|
219
312
|
const { tools } = await import('./tools/index.js');
|
|
220
313
|
const names = Object.keys(tools).sort();
|
|
@@ -222,6 +315,207 @@ async function runTools(_args = []) {
|
|
|
222
315
|
console.log(names.map((n) => ` ${n}`).join('\n'));
|
|
223
316
|
console.log(`\nจัดการ MCP เพิ่มเติม: ${BRAND.cliName} mcp add <name> <command> [args...]`);
|
|
224
317
|
}
|
|
318
|
+
async function runPromptSize(args = []) {
|
|
319
|
+
const allowed = new Set(['--json']);
|
|
320
|
+
const unknown = args.find((arg) => !allowed.has(arg));
|
|
321
|
+
if (unknown) {
|
|
322
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
323
|
+
console.error(`ใช้: ${BRAND.cliName} prompt-size [--json]`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
const { buildPromptSizeBreakdown, renderPromptSizeBreakdown } = await import('./prompt-size.js');
|
|
327
|
+
const report = await buildPromptSizeBreakdown();
|
|
328
|
+
if (args.includes('--json'))
|
|
329
|
+
console.log(JSON.stringify(report, null, 2));
|
|
330
|
+
else
|
|
331
|
+
process.stdout.write(renderPromptSizeBreakdown(report));
|
|
332
|
+
}
|
|
333
|
+
async function runRuntimes(args = []) {
|
|
334
|
+
const allowed = new Set(['--json']);
|
|
335
|
+
const unknown = args.find((arg) => !allowed.has(arg));
|
|
336
|
+
if (unknown) {
|
|
337
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
338
|
+
console.error(`ใช้: ${BRAND.cliName} runtimes [--json]`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
const { inspectPolyglotRuntimes, renderPolyglotReport } = await import('./polyglot.js');
|
|
342
|
+
const report = await inspectPolyglotRuntimes();
|
|
343
|
+
if (args.includes('--json'))
|
|
344
|
+
console.log(JSON.stringify(report, null, 2));
|
|
345
|
+
else
|
|
346
|
+
process.stdout.write(renderPolyglotReport(report));
|
|
347
|
+
}
|
|
348
|
+
async function runWeb(args = []) {
|
|
349
|
+
const action = args[0] && !args[0].startsWith('--') ? args[0] : 'status';
|
|
350
|
+
const rest = action === args[0] ? args.slice(1) : args;
|
|
351
|
+
if (action === 'fetch')
|
|
352
|
+
return runWebFetch(rest);
|
|
353
|
+
if (action === 'search')
|
|
354
|
+
return runWebSearch(rest);
|
|
355
|
+
if (action === 'setup')
|
|
356
|
+
return runWebSetup(rest);
|
|
357
|
+
const flags = rest;
|
|
358
|
+
const allowed = new Set(['--json', '--probe']);
|
|
359
|
+
const unknown = flags.find((arg) => !allowed.has(arg));
|
|
360
|
+
if (!['status', 'doctor'].includes(action) || unknown) {
|
|
361
|
+
if (unknown)
|
|
362
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
363
|
+
console.error(`ใช้: ${BRAND.cliName} web status [--json] [--probe]`);
|
|
364
|
+
console.error(` ${BRAND.cliName} web doctor [--json]`);
|
|
365
|
+
console.error(` ${BRAND.cliName} web fetch <url> [--json] [--no-reader] [--no-archive] [--no-robots] [--allow-private]`);
|
|
366
|
+
console.error(` ${BRAND.cliName} web search "<query>" [--json] [--limit N]`);
|
|
367
|
+
console.error(` ${BRAND.cliName} web setup tavily [--api-key <key>]`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
const { inspectWebSurface, renderWebSurfaceReport } = await import('./web-surface.js');
|
|
371
|
+
const report = await inspectWebSurface({ cwd: process.cwd(), probe: action === 'doctor' || flags.includes('--probe') });
|
|
372
|
+
if (flags.includes('--json'))
|
|
373
|
+
console.log(JSON.stringify(report, null, 2));
|
|
374
|
+
else
|
|
375
|
+
process.stdout.write(renderWebSurfaceReport(report));
|
|
376
|
+
if (action === 'doctor' && report.webCandidates.some((candidate) => candidate.probe && !candidate.probe.ok))
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
/** TAVILY_API_KEY from env first, then any MCP server env in ~/.sanook/mcp.json. */
|
|
380
|
+
async function resolveTavilyKey() {
|
|
381
|
+
const fromEnv = process.env.TAVILY_API_KEY?.trim();
|
|
382
|
+
if (fromEnv)
|
|
383
|
+
return fromEnv;
|
|
384
|
+
try {
|
|
385
|
+
const { loadMcpConfig } = await import('./mcp.js');
|
|
386
|
+
const cfg = await loadMcpConfig();
|
|
387
|
+
for (const server of Object.values(cfg)) {
|
|
388
|
+
const key = server.env?.TAVILY_API_KEY?.trim();
|
|
389
|
+
if (key)
|
|
390
|
+
return key;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
/* no mcp config */
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
async function runWebFetch(args) {
|
|
399
|
+
const url = args.find((a) => !a.startsWith('--'));
|
|
400
|
+
if (!url) {
|
|
401
|
+
console.error(`ใช้: ${BRAND.cliName} web fetch <url> [--json] [--no-reader] [--no-archive] [--no-robots] [--allow-private]`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
const { fetchWeb, renderWebFetchResult } = await import('./web-fetch.js');
|
|
405
|
+
const result = await fetchWeb(url, {
|
|
406
|
+
allowReader: !args.includes('--no-reader'),
|
|
407
|
+
allowArchive: !args.includes('--no-archive'),
|
|
408
|
+
respectRobots: !args.includes('--no-robots'),
|
|
409
|
+
allowPrivateHosts: args.includes('--allow-private'),
|
|
410
|
+
tavilyApiKey: await resolveTavilyKey(),
|
|
411
|
+
});
|
|
412
|
+
if (args.includes('--json'))
|
|
413
|
+
console.log(JSON.stringify(result, null, 2));
|
|
414
|
+
else
|
|
415
|
+
process.stdout.write(`${renderWebFetchResult(result)}\n`);
|
|
416
|
+
if (!result.ok)
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
async function runWebSearch(args) {
|
|
420
|
+
let limit = 5;
|
|
421
|
+
const terms = [];
|
|
422
|
+
for (let i = 0; i < args.length; i++) {
|
|
423
|
+
const a = args[i];
|
|
424
|
+
if (a === '--json')
|
|
425
|
+
continue;
|
|
426
|
+
if (a === '--limit') {
|
|
427
|
+
limit = Number(args[++i]) || limit;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (a.startsWith('--limit=')) {
|
|
431
|
+
limit = Number(a.slice('--limit='.length)) || limit;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (a.startsWith('--'))
|
|
435
|
+
continue;
|
|
436
|
+
terms.push(a);
|
|
437
|
+
}
|
|
438
|
+
const query = terms.join(' ').trim();
|
|
439
|
+
if (!query) {
|
|
440
|
+
console.error(`ใช้: ${BRAND.cliName} web search "<query>" [--json] [--limit N]`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const apiKey = await resolveTavilyKey();
|
|
444
|
+
if (!apiKey) {
|
|
445
|
+
console.error(`web search ต้องมี Tavily API key — ตั้งค่า: ${BRAND.cliName} web setup tavily`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
const { tavilySearch } = await import('./web-fetch.js');
|
|
449
|
+
try {
|
|
450
|
+
const hits = await tavilySearch(query, { apiKey, maxResults: Math.min(Math.max(limit, 1), 20) });
|
|
451
|
+
if (args.includes('--json')) {
|
|
452
|
+
console.log(JSON.stringify(hits, null, 2));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!hits.length) {
|
|
456
|
+
console.log('(no results)');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
for (const h of hits)
|
|
460
|
+
console.log(`- ${h.title}\n ${h.url}\n ${h.content.slice(0, 200)}`);
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
console.error(`web search ล้มเหลว: ${e.message}`);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function runWebSetup(args) {
|
|
468
|
+
const provider = args.find((a) => !a.startsWith('--'));
|
|
469
|
+
if (provider !== 'tavily') {
|
|
470
|
+
console.error(`ใช้: ${BRAND.cliName} web setup tavily [--api-key <key>]`);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
let apiKey = (argValue(args, '--api-key') ?? process.env.TAVILY_API_KEY ?? '').trim();
|
|
474
|
+
if (!apiKey)
|
|
475
|
+
apiKey = (await askText('Tavily API key (tvly-...): ')).trim();
|
|
476
|
+
if (!apiKey) {
|
|
477
|
+
console.error('ต้องระบุ Tavily API key');
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
const mcpPath = appHomePath('mcp.json');
|
|
481
|
+
let cfg;
|
|
482
|
+
try {
|
|
483
|
+
const parsed = JSON.parse(await readFile(mcpPath, 'utf8'));
|
|
484
|
+
cfg = { mcpServers: parsed.mcpServers ?? {} };
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
cfg = { mcpServers: {} };
|
|
488
|
+
}
|
|
489
|
+
cfg.mcpServers.tavily = { command: 'npx', args: ['-y', 'tavily-mcp'], env: { TAVILY_API_KEY: apiKey } };
|
|
490
|
+
await mkdir(dirname(mcpPath), { recursive: true });
|
|
491
|
+
await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
|
|
492
|
+
await chmod(mcpPath, 0o600).catch(() => { });
|
|
493
|
+
console.log(`ตั้งค่า Tavily แล้ว → ${mcpPath} (chmod 600; key เก็บแบบ env ไม่ echo ออกจอ)`);
|
|
494
|
+
console.log(` • agent runtime ใช้ผ่าน MCP "tavily" (search/extract)`);
|
|
495
|
+
console.log(` • ${BRAND.cliName} web fetch <url> และ ${BRAND.cliName} web search "<q>" จะหยิบ key นี้อัตโนมัติ`);
|
|
496
|
+
console.log(`ทดสอบ: ${BRAND.cliName} mcp test tavily`);
|
|
497
|
+
}
|
|
498
|
+
/** sanook memory [log "<query>" | stats] — read-only view over the bi-temporal memory store */
|
|
499
|
+
async function runMemory(args = []) {
|
|
500
|
+
const action = args[0] && !args[0].startsWith('--') ? args[0] : undefined;
|
|
501
|
+
const rest = action ? args.slice(1) : args;
|
|
502
|
+
const json = args.includes('--json');
|
|
503
|
+
const { loadStore } = await import('./memory-store.js');
|
|
504
|
+
const { memoryLog, renderMemoryLog, memoryStats, renderMemoryStats } = await import('./memory-log.js');
|
|
505
|
+
const store = await loadStore();
|
|
506
|
+
if (action === 'log') {
|
|
507
|
+
const query = rest.filter((a) => !a.startsWith('--')).join(' ').trim();
|
|
508
|
+
const entries = memoryLog(store, query);
|
|
509
|
+
if (json)
|
|
510
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
511
|
+
else
|
|
512
|
+
process.stdout.write(`${renderMemoryLog(entries, query)}\n`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
// default + `stats`: overview of what's remembered (active / superseded / archived)
|
|
516
|
+
const stats = memoryStats(store);
|
|
517
|
+
console.log(json ? JSON.stringify(stats, null, 2) : renderMemoryStats(stats));
|
|
518
|
+
}
|
|
225
519
|
async function runAgentSetupSummary() {
|
|
226
520
|
const cfg = await loadConfig({});
|
|
227
521
|
console.log(`${BRAND.productName} agent settings`);
|
|
@@ -239,6 +533,13 @@ async function runAgentSetupSummary() {
|
|
|
239
533
|
console.log(` ${BRAND.cliName} config set budgetUsd 0.25`);
|
|
240
534
|
console.log(` ${BRAND.cliName} config set fallbackModel haiku`);
|
|
241
535
|
}
|
|
536
|
+
async function runGatewayDoctor() {
|
|
537
|
+
const { checkGateway, formatGatewayDoctorReport } = await import('./gateway/doctor.js');
|
|
538
|
+
const report = await checkGateway();
|
|
539
|
+
console.log(formatGatewayDoctorReport(report));
|
|
540
|
+
if (!report.ok)
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
242
543
|
async function runGatewayStatus() {
|
|
243
544
|
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
545
|
const cfg = await readGatewayConfig();
|
|
@@ -415,6 +716,31 @@ async function runGatewayStatus() {
|
|
|
415
716
|
console.log(` public url: ${webhooks.publicUrl ?? '(not set)'}`);
|
|
416
717
|
console.log(` rate limit: ${webhooks.rateLimitPerMinute}/minute`);
|
|
417
718
|
}
|
|
719
|
+
const { checkGateway, formatGatewayDoctorStatus, listPendingCronJobs, listRecentDeliveryFailures, summarizeChannelHealth, } = await import('./gateway/doctor.js');
|
|
720
|
+
const pendingCron = await listPendingCronJobs();
|
|
721
|
+
console.log(`\ncron (pending): ${pendingCron.length}`);
|
|
722
|
+
if (pendingCron.length) {
|
|
723
|
+
for (const task of pendingCron.slice(0, 10)) {
|
|
724
|
+
const when = new Date(task.runAt).toISOString();
|
|
725
|
+
const deliver = task.deliver ? ` → ${task.deliver}` : '';
|
|
726
|
+
console.log(` ${task.id} ${when} ${task.schedule ?? 'once'} ${task.spec.slice(0, 60)}${deliver}`);
|
|
727
|
+
}
|
|
728
|
+
if (pendingCron.length > 10)
|
|
729
|
+
console.log(` … และอีก ${pendingCron.length - 10} งาน`);
|
|
730
|
+
}
|
|
731
|
+
const deliveryFailures = await listRecentDeliveryFailures(5);
|
|
732
|
+
console.log(`\ndelivery failures (recent): ${deliveryFailures.length ? deliveryFailures.length : 'none'}`);
|
|
733
|
+
for (const failure of deliveryFailures) {
|
|
734
|
+
const when = failure.lastRun ? new Date(failure.lastRun).toISOString() : '(unknown)';
|
|
735
|
+
console.log(` ${failure.taskId} ${when} ${failure.deliver} ${failure.error.slice(0, 120)}`);
|
|
736
|
+
}
|
|
737
|
+
const healthReport = await checkGateway({ config: cfg, skipNetwork: true });
|
|
738
|
+
const health = summarizeChannelHealth(healthReport.checks).filter((item) => item.status !== 'skip');
|
|
739
|
+
console.log(`\nchannel health (config): ${health.length ? '' : 'no configured channels'}`);
|
|
740
|
+
for (const item of health) {
|
|
741
|
+
console.log(` ${item.channel.padEnd(12)} ${formatGatewayDoctorStatus(item.status)}`);
|
|
742
|
+
}
|
|
743
|
+
console.log(` (live token/webhook probes: ${BRAND.cliName} gateway doctor)`);
|
|
418
744
|
console.log(`\nredacted config:\n${JSON.stringify(redactGatewayConfig(cfg), null, 2)}`);
|
|
419
745
|
}
|
|
420
746
|
async function runGatewaySetup(args) {
|
|
@@ -1604,6 +1930,8 @@ async function runGateway(args) {
|
|
|
1604
1930
|
const [action, ...rest] = args;
|
|
1605
1931
|
if (!action || action === 'status' || action === 'list')
|
|
1606
1932
|
return runGatewayStatus();
|
|
1933
|
+
if (action === 'doctor')
|
|
1934
|
+
return runGatewayDoctor();
|
|
1607
1935
|
if (action === 'setup')
|
|
1608
1936
|
return runGatewaySetup(rest);
|
|
1609
1937
|
if (action === 'run') {
|
|
@@ -1651,7 +1979,7 @@ async function runGateway(args) {
|
|
|
1651
1979
|
console.log(removed.length ? `ลบ service files:\n${removed.map((p) => ` ${p}`).join('\n')}` : 'ไม่พบ service file ที่ต้องลบ');
|
|
1652
1980
|
return;
|
|
1653
1981
|
}
|
|
1654
|
-
console.error(`ไม่รู้จัก: gateway ${action} — ใช้ setup / run / start / stop / restart / install / status`);
|
|
1982
|
+
console.error(`ไม่รู้จัก: gateway ${action} — ใช้ setup / run / start / stop / restart / install / doctor / status`);
|
|
1655
1983
|
process.exit(1);
|
|
1656
1984
|
}
|
|
1657
1985
|
async function runStatus() {
|
|
@@ -2308,7 +2636,7 @@ providers: ${providerIds()}`);
|
|
|
2308
2636
|
process.exit(1);
|
|
2309
2637
|
}
|
|
2310
2638
|
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);
|
|
2639
|
+
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
2640
|
}
|
|
2313
2641
|
async function runPureOneShot(args) {
|
|
2314
2642
|
const rest = args;
|
|
@@ -2328,7 +2656,7 @@ async function runPureOneShot(args) {
|
|
|
2328
2656
|
process.exit(1);
|
|
2329
2657
|
}
|
|
2330
2658
|
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);
|
|
2659
|
+
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
2660
|
}
|
|
2333
2661
|
async function runSend(args) {
|
|
2334
2662
|
const json = args.includes('--json');
|
|
@@ -2686,19 +3014,31 @@ async function runCron(args) {
|
|
|
2686
3014
|
console.error(`ไม่รู้จัก: cron ${action} — ใช้ add / list / rm`);
|
|
2687
3015
|
process.exit(1);
|
|
2688
3016
|
}
|
|
2689
|
-
/** sanook
|
|
3017
|
+
/** sanook init [--trust] — scaffold project .sanook/commands + onboarding hints */
|
|
3018
|
+
async function runInit(args) {
|
|
3019
|
+
const trust = args.includes('--trust');
|
|
3020
|
+
const unknown = args.filter((a) => a !== '--trust');
|
|
3021
|
+
if (unknown.length) {
|
|
3022
|
+
console.error(`ใช้: ${BRAND.cliName} init [--trust]`);
|
|
3023
|
+
process.exit(1);
|
|
3024
|
+
}
|
|
3025
|
+
const { initProject, formatInitResult } = await import('./project-init.js');
|
|
3026
|
+
const result = await initProject({ trust });
|
|
3027
|
+
console.log(formatInitResult(result));
|
|
3028
|
+
}
|
|
3029
|
+
/** sanook skill list | install <name|path> | add <source> | remove <name> */
|
|
2690
3030
|
async function runSkill(args) {
|
|
2691
3031
|
const [action, ...rest] = args;
|
|
2692
|
-
if (action === 'add') {
|
|
3032
|
+
if (action === 'add' || action === 'install') {
|
|
2693
3033
|
const source = rest[0];
|
|
2694
3034
|
if (!source) {
|
|
2695
|
-
console.error(
|
|
3035
|
+
console.error(`ใช้: ${BRAND.cliName} skill install <bundled-name|path> หรือ ${BRAND.cliName} skill add <github "user/repo" | URL ของ SKILL.md | local path>`);
|
|
2696
3036
|
process.exit(1);
|
|
2697
3037
|
}
|
|
2698
3038
|
console.error(`${DIM}⚠ skill = instruction ที่ AI จะทำตาม — ติดตั้งจาก source ที่เชื่อถือเท่านั้น${RESET}`);
|
|
2699
|
-
const {
|
|
3039
|
+
const { installNamedSkill } = await import('./skill-install.js');
|
|
2700
3040
|
try {
|
|
2701
|
-
const installed = await
|
|
3041
|
+
const installed = await installNamedSkill(source, (m) => process.stderr.write(`${DIM}${m}${RESET}\n`));
|
|
2702
3042
|
console.log(`ติดตั้ง ${installed.length} skill: ${installed.map((s) => s.name).join(', ')}`);
|
|
2703
3043
|
}
|
|
2704
3044
|
catch (e) {
|
|
@@ -2793,13 +3133,15 @@ async function runBrainContext(args) {
|
|
|
2793
3133
|
const parsed = parseBrainContextArgs(args);
|
|
2794
3134
|
if (!parsed.ok) {
|
|
2795
3135
|
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]`);
|
|
3136
|
+
console.error(`ใช้: ${BRAND.cliName} brain context [--task "..."] [--project <slug>] [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,session,skill] [--no-content]`);
|
|
2797
3137
|
process.exit(1);
|
|
2798
3138
|
}
|
|
2799
3139
|
const cfg = await loadConfig({});
|
|
2800
3140
|
const report = await inspectBrainContext({
|
|
2801
3141
|
brainPath: cfg.brainPath,
|
|
2802
3142
|
task: parsed.value.task,
|
|
3143
|
+
cwd: process.cwd(),
|
|
3144
|
+
projectSlug: parsed.value.project,
|
|
2803
3145
|
mode: parsed.value.mode,
|
|
2804
3146
|
limit: parsed.value.limit,
|
|
2805
3147
|
sources: parsed.value.sources,
|
|
@@ -2842,6 +3184,130 @@ async function runBrainReview(args) {
|
|
|
2842
3184
|
if (!report.ok)
|
|
2843
3185
|
process.exit(1);
|
|
2844
3186
|
}
|
|
3187
|
+
/** sanook brain consolidate [--apply] [--archive] [--memory] — sleep-time consolidation runner */
|
|
3188
|
+
async function runBrainConsolidate(args) {
|
|
3189
|
+
const { parseBrainConsolidateArgs, runBrainConsolidate: consolidate, formatBrainConsolidateReport } = await import('./brain-consolidate.js');
|
|
3190
|
+
const parsed = parseBrainConsolidateArgs(args);
|
|
3191
|
+
if (!parsed.ok) {
|
|
3192
|
+
console.error(parsed.message);
|
|
3193
|
+
console.error(`ใช้: ${BRAND.cliName} brain consolidate [--apply] [--apply --archive] [--memory] [--no-retrieval]`);
|
|
3194
|
+
process.exit(1);
|
|
3195
|
+
}
|
|
3196
|
+
const cfg = await loadConfig({});
|
|
3197
|
+
const report = await consolidate({
|
|
3198
|
+
brainPath: cfg.brainPath,
|
|
3199
|
+
apply: parsed.value.apply,
|
|
3200
|
+
archive: parsed.value.archive,
|
|
3201
|
+
memory: parsed.value.memory,
|
|
3202
|
+
runRetrieval: parsed.value.runRetrieval,
|
|
3203
|
+
});
|
|
3204
|
+
console.log(formatBrainConsolidateReport(report));
|
|
3205
|
+
if (!report.ok)
|
|
3206
|
+
process.exit(1);
|
|
3207
|
+
}
|
|
3208
|
+
/** sanook brain metrics [--no-retrieval] — vault counts, stale notes, index + retrieval coverage */
|
|
3209
|
+
async function runBrainMetrics(args) {
|
|
3210
|
+
const allowed = new Set(['--no-retrieval']);
|
|
3211
|
+
const unknown = args.filter((arg) => !allowed.has(arg));
|
|
3212
|
+
if (unknown.length) {
|
|
3213
|
+
console.error(`ไม่รู้จัก option: ${unknown.join(' ')}`);
|
|
3214
|
+
console.error(`ใช้: ${BRAND.cliName} brain metrics [--no-retrieval]`);
|
|
3215
|
+
process.exit(1);
|
|
3216
|
+
}
|
|
3217
|
+
const cfg = await loadConfig({});
|
|
3218
|
+
const { collectBrainMetrics, formatBrainMetricsReport } = await import('./brain-metrics.js');
|
|
3219
|
+
const report = await collectBrainMetrics({ brainPath: cfg.brainPath, runRetrievalEval: !args.includes('--no-retrieval') });
|
|
3220
|
+
console.log(formatBrainMetricsReport(report));
|
|
3221
|
+
if (!report.ok)
|
|
3222
|
+
process.exit(1);
|
|
3223
|
+
}
|
|
3224
|
+
/** sanook brain projects list — show vault project workspaces and repo_path mappings */
|
|
3225
|
+
async function runBrainProjects(args) {
|
|
3226
|
+
if (args[0] && args[0] !== 'list') {
|
|
3227
|
+
console.error(`ใช้: ${BRAND.cliName} brain projects list`);
|
|
3228
|
+
process.exit(1);
|
|
3229
|
+
}
|
|
3230
|
+
const cfg = await loadConfig({});
|
|
3231
|
+
if (!cfg.brainPath) {
|
|
3232
|
+
console.error('ยังไม่ได้ตั้ง brainPath — รัน `sanook brain init [path]` ก่อน');
|
|
3233
|
+
process.exit(1);
|
|
3234
|
+
}
|
|
3235
|
+
const { listVaultProjects, formatVaultProjectLine, resolveVaultProject } = await import('./project-registry.js');
|
|
3236
|
+
const projects = await listVaultProjects(cfg.brainPath);
|
|
3237
|
+
const active = await resolveVaultProject({ brainPath: cfg.brainPath, cwd: process.cwd() });
|
|
3238
|
+
console.log(`${BRAND.productName} brain projects`);
|
|
3239
|
+
console.log(`vault: ${cfg.brainPath}`);
|
|
3240
|
+
console.log(`cwd: ${process.cwd()}${active ? ` → active: ${active.slug}` : ''}`);
|
|
3241
|
+
if (!projects.length) {
|
|
3242
|
+
console.log('(no project workspaces — run `sanook brain new project --title "..." --repo /path`)');
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
console.log('\nslug repo_path');
|
|
3246
|
+
for (const project of projects) {
|
|
3247
|
+
const marker = active?.slug === project.slug ? ' *' : '';
|
|
3248
|
+
console.log(`${formatVaultProjectLine(project)}${marker}`);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
/** sanook brain pack list|show <name> — inspect Shared/Context-Packs/ bundles */
|
|
3252
|
+
async function runBrainPack(args) {
|
|
3253
|
+
const { parseBrainPackArgs, listContextPacks, showContextPack, formatBrainPackListReport, formatBrainPackShowReport } = await import('./brain-pack.js');
|
|
3254
|
+
const parsed = parseBrainPackArgs(args);
|
|
3255
|
+
if (!parsed.ok) {
|
|
3256
|
+
console.error(parsed.message);
|
|
3257
|
+
console.error(`ใช้: ${BRAND.cliName} brain pack list | ${BRAND.cliName} brain pack show <name>`);
|
|
3258
|
+
process.exit(1);
|
|
3259
|
+
}
|
|
3260
|
+
const cfg = await loadConfig({});
|
|
3261
|
+
if (!cfg.brainPath) {
|
|
3262
|
+
console.error('ยังไม่ได้ตั้ง brainPath — รัน `sanook brain init [path]` ก่อน');
|
|
3263
|
+
process.exit(1);
|
|
3264
|
+
}
|
|
3265
|
+
if (parsed.action === 'list') {
|
|
3266
|
+
const report = await listContextPacks(cfg.brainPath);
|
|
3267
|
+
console.log(formatBrainPackListReport(report));
|
|
3268
|
+
if (!report.ok)
|
|
3269
|
+
process.exit(1);
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
const report = await showContextPack(cfg.brainPath, parsed.name);
|
|
3273
|
+
console.log(formatBrainPackShowReport(report));
|
|
3274
|
+
if (!report.ok)
|
|
3275
|
+
process.exit(1);
|
|
3276
|
+
}
|
|
3277
|
+
/** sanook brain new <type> [--title "..."] — template-backed note creation in the correct vault folder */
|
|
3278
|
+
async function runBrainNew(args) {
|
|
3279
|
+
const { parseBrainNewArgs, createBrainNote, formatBrainNewReport } = await import('./brain-new.js');
|
|
3280
|
+
const parsed = parseBrainNewArgs(args);
|
|
3281
|
+
if (!parsed.ok) {
|
|
3282
|
+
console.error(parsed.message);
|
|
3283
|
+
console.error(`ใช้: ${BRAND.cliName} brain new <session|bug|handoff|project|golden-case|checklist> [--title "..."] [--repo /path] [--verify "..."] [--output path] [--force]`);
|
|
3284
|
+
process.exit(1);
|
|
3285
|
+
}
|
|
3286
|
+
const cfg = await loadConfig({});
|
|
3287
|
+
const report = await createBrainNote({
|
|
3288
|
+
brainPath: cfg.brainPath,
|
|
3289
|
+
today: new Date().toISOString().slice(0, 10),
|
|
3290
|
+
...parsed.value,
|
|
3291
|
+
});
|
|
3292
|
+
console.log(formatBrainNewReport(report));
|
|
3293
|
+
if (!report.ok)
|
|
3294
|
+
process.exit(1);
|
|
3295
|
+
}
|
|
3296
|
+
/** sanook brain repair [--dry-run] — apply safe one-line fixes after doctor/review findings */
|
|
3297
|
+
async function runBrainRepair(args) {
|
|
3298
|
+
const { parseBrainRepairArgs, repairBrain, formatBrainRepairReport } = await import('./brain-repair.js');
|
|
3299
|
+
const parsed = parseBrainRepairArgs(args);
|
|
3300
|
+
if (!parsed.ok) {
|
|
3301
|
+
console.error(parsed.message);
|
|
3302
|
+
console.error(`ใช้: ${BRAND.cliName} brain repair [--dry-run]`);
|
|
3303
|
+
process.exit(1);
|
|
3304
|
+
}
|
|
3305
|
+
const cfg = await loadConfig({});
|
|
3306
|
+
const report = await repairBrain({ brainPath: cfg.brainPath, dryRun: parsed.dryRun });
|
|
3307
|
+
console.log(formatBrainRepairReport(report));
|
|
3308
|
+
if (!report.ok)
|
|
3309
|
+
process.exit(1);
|
|
3310
|
+
}
|
|
2845
3311
|
/** sanook brain final [--task "..."] [--from-diff] [--lite] — create an evidence-backed closeout note */
|
|
2846
3312
|
async function runBrainFinal(args) {
|
|
2847
3313
|
const { parseBrainFinalArgs, createBrainFinal, formatBrainFinalReport } = await import('./brain-final.js');
|
|
@@ -2871,16 +3337,39 @@ async function runBrain(args) {
|
|
|
2871
3337
|
return runBrainEval(args.slice(1));
|
|
2872
3338
|
if (args[0] === 'review')
|
|
2873
3339
|
return runBrainReview(args.slice(1));
|
|
3340
|
+
if (args[0] === 'projects')
|
|
3341
|
+
return runBrainProjects(args.slice(1));
|
|
3342
|
+
if (args[0] === 'pack')
|
|
3343
|
+
return runBrainPack(args.slice(1));
|
|
3344
|
+
if (args[0] === 'new')
|
|
3345
|
+
return runBrainNew(args.slice(1));
|
|
3346
|
+
if (args[0] === 'repair')
|
|
3347
|
+
return runBrainRepair(args.slice(1));
|
|
3348
|
+
if (args[0] === 'consolidate')
|
|
3349
|
+
return runBrainConsolidate(args.slice(1));
|
|
3350
|
+
if (args[0] === 'metrics')
|
|
3351
|
+
return runBrainMetrics(args.slice(1));
|
|
2874
3352
|
if (args[0] === 'final')
|
|
2875
3353
|
return runBrainFinal(args.slice(1));
|
|
2876
3354
|
if (args[0] !== 'init') {
|
|
2877
3355
|
console.log(`ใช้:
|
|
2878
3356
|
sanook brain init [path] สร้างโครงสร้าง second-brain (Obsidian vault)
|
|
2879
3357
|
sanook brain doctor ตรวจ health ของ second-brain ที่ config.brainPath
|
|
2880
|
-
sanook brain context แสดง context ที่ Sanook จะ inject
|
|
3358
|
+
sanook brain context แสดง context ที่ Sanook จะ inject (project auto-detect จาก cwd)
|
|
2881
3359
|
sanook brain context --task "..." ดู retrieval hits ต่อ task
|
|
3360
|
+
sanook brain context --project <slug> บังคับ project workspace
|
|
3361
|
+
sanook brain projects list แสดง Projects/<slug>/ + repo_path
|
|
2882
3362
|
sanook brain eval รัน second-brain benchmark sanity checks
|
|
2883
3363
|
sanook brain review curator review: inbox, packs, sessions, evals, note hygiene
|
|
3364
|
+
sanook brain pack list แสดง context packs ใน Shared/Context-Packs/
|
|
3365
|
+
sanook brain pack show <name> แสดง load order / done criteria ของ pack
|
|
3366
|
+
sanook brain new project --title "..." --repo /path [--verify "..."]
|
|
3367
|
+
sanook brain new <type> [--title "..."] สร้างโน้ตจาก template
|
|
3368
|
+
sanook brain repair [--dry-run] แก้ one-line fixes หลัง doctor/review
|
|
3369
|
+
sanook brain consolidate [--apply] [--apply --archive] [--memory]
|
|
3370
|
+
sleep-time consolidation (dry-run default)
|
|
3371
|
+
sanook brain metrics [--no-retrieval]
|
|
3372
|
+
vault counts, stale notes, index freshness, retrieval coverage
|
|
2884
3373
|
sanook brain final --task "..." [--from-diff] [--lite]
|
|
2885
3374
|
สร้าง final gate note ใน Sessions
|
|
2886
3375
|
|
|
@@ -2898,7 +3387,9 @@ async function runBrain(args) {
|
|
|
2898
3387
|
return;
|
|
2899
3388
|
}
|
|
2900
3389
|
const { scaffoldBrain, BRAIN_DEFAULTS, expandHome, wireBrainMcp } = await import('./brain.js');
|
|
2901
|
-
|
|
3390
|
+
// resolve to absolute before persisting — getBrainPath() is later read from an arbitrary cwd,
|
|
3391
|
+
// so a relative path (e.g. "./vault") would resolve differently per run
|
|
3392
|
+
const target = resolve(expandHome(pathArg ?? join(homedir(), 'Documents', BRAIN_DEFAULTS.vaultName)));
|
|
2902
3393
|
const today = new Date().toISOString().slice(0, 10);
|
|
2903
3394
|
try {
|
|
2904
3395
|
const res = await scaffoldBrain(target, { ...BRAIN_DEFAULTS, today });
|
|
@@ -2985,11 +3476,16 @@ async function runConfig(args) {
|
|
|
2985
3476
|
console.error('contextCompression ต้องเป็น off, selective หรือ headroom');
|
|
2986
3477
|
process.exit(1);
|
|
2987
3478
|
}
|
|
3479
|
+
else if (key === 'brainPath') {
|
|
3480
|
+
// store absolute — getBrainPath() is read from arbitrary cwd, so a relative path drifts
|
|
3481
|
+
const { expandHome } = await import('./brain.js');
|
|
3482
|
+
value = resolve(expandHome(raw.trim()));
|
|
3483
|
+
}
|
|
2988
3484
|
else if (key === 'thinking') {
|
|
2989
3485
|
// เก็บเป็น number (budget) หรือ boolean ให้ตรง ConfigSchema (ไม่เก็บ string)
|
|
2990
3486
|
value = parseThinkingConfigValue(raw);
|
|
2991
3487
|
if (value === undefined) {
|
|
2992
|
-
console.error('thinking ต้องเป็น on/off หรือ budget tokens (integer บวก เช่น 4000)');
|
|
3488
|
+
console.error('thinking ต้องเป็น on/off, true/false, yes/no หรือ budget tokens (integer บวก เช่น 4000)');
|
|
2993
3489
|
process.exit(1);
|
|
2994
3490
|
}
|
|
2995
3491
|
}
|
|
@@ -3065,7 +3561,7 @@ async function runMcpServe() {
|
|
|
3065
3561
|
const { runMcpServer } = await import('./mcp-server.js');
|
|
3066
3562
|
await runMcpServer();
|
|
3067
3563
|
}
|
|
3068
|
-
/** sanook mcp [search|info|install|test|doctor|list|add|remove] — จัดการ ~/.sanook/mcp.json */
|
|
3564
|
+
/** sanook mcp [search|info|install|test|doctor|enable|disable|list|add|remove] — จัดการ ~/.sanook/mcp.json */
|
|
3069
3565
|
async function runMcp(args) {
|
|
3070
3566
|
const readConfig = async (path) => {
|
|
3071
3567
|
try {
|
|
@@ -3132,34 +3628,22 @@ async function runMcp(args) {
|
|
|
3132
3628
|
return;
|
|
3133
3629
|
}
|
|
3134
3630
|
if (action === 'install') {
|
|
3135
|
-
const { buildMcpInstallPlan, getMcpRegistryServer, parseKeyValueList, formatRegistryInfo, } = await import('./mcp-registry.js');
|
|
3136
|
-
const
|
|
3137
|
-
if (!
|
|
3138
|
-
console.error(
|
|
3631
|
+
const { buildMcpInstallPlan, getMcpRegistryServer, parseKeyValueList, formatRegistryInfo, parseMcpRegistryInstallArgs, } = await import('./mcp-registry.js');
|
|
3632
|
+
const parsedInstall = parseMcpRegistryInstallArgs(rest);
|
|
3633
|
+
if (!parsedInstall.ok) {
|
|
3634
|
+
console.error(parsedInstall.message);
|
|
3139
3635
|
process.exit(1);
|
|
3140
3636
|
}
|
|
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');
|
|
3637
|
+
const { name, alias, transport, version, env, headers, project } = parsedInstall.value;
|
|
3153
3638
|
if (alias && !isValidMcpServerName(alias)) {
|
|
3154
3639
|
console.error('ชื่อ MCP server ต้องเป็น a-z/A-Z/0-9/_/- ความยาวไม่เกิน 64 และห้ามใช้ชื่อพิเศษ');
|
|
3155
3640
|
process.exit(1);
|
|
3156
3641
|
}
|
|
3157
|
-
const transport = valueOf('--transport');
|
|
3158
3642
|
if (transport && !['auto', 'remote', 'stdio'].includes(transport)) {
|
|
3159
3643
|
console.error('--transport ต้องเป็น auto, remote, หรือ stdio');
|
|
3160
3644
|
process.exit(1);
|
|
3161
3645
|
}
|
|
3162
|
-
const server = await getMcpRegistryServer(name, { version
|
|
3646
|
+
const server = await getMcpRegistryServer(name, { version });
|
|
3163
3647
|
if (!server) {
|
|
3164
3648
|
console.error(`ไม่เจอ MCP registry server: ${name}`);
|
|
3165
3649
|
process.exit(1);
|
|
@@ -3167,8 +3651,8 @@ async function runMcp(args) {
|
|
|
3167
3651
|
const plan = buildMcpInstallPlan(server, {
|
|
3168
3652
|
alias,
|
|
3169
3653
|
transport,
|
|
3170
|
-
env: parseKeyValueList(
|
|
3171
|
-
headers: parseKeyValueList(
|
|
3654
|
+
env: parseKeyValueList(env),
|
|
3655
|
+
headers: parseKeyValueList(headers),
|
|
3172
3656
|
});
|
|
3173
3657
|
if (!plan.ok) {
|
|
3174
3658
|
console.log(formatRegistryInfo(server));
|
|
@@ -3182,20 +3666,25 @@ async function runMcp(args) {
|
|
|
3182
3666
|
process.exit(1);
|
|
3183
3667
|
}
|
|
3184
3668
|
let targetPath = mcpPath;
|
|
3185
|
-
if (
|
|
3186
|
-
|
|
3669
|
+
if (project) {
|
|
3670
|
+
// Use trust status (not projectConfigPathIfTrusted, which requires a PRE-EXISTING file) so the
|
|
3671
|
+
// FIRST project-scoped install into a trusted project can create .sanook/mcp.json.
|
|
3672
|
+
const { projectTrustStatus, projectRoot } = await import('./trust.js');
|
|
3673
|
+
const { appProjectPath } = await import('./brand.js');
|
|
3187
3674
|
const root = await projectRoot(process.cwd());
|
|
3188
|
-
const
|
|
3189
|
-
if (!
|
|
3675
|
+
const trust = await projectTrustStatus(root);
|
|
3676
|
+
if (!trust.trusted) {
|
|
3190
3677
|
console.error(`project MCP ต้อง trust ก่อน: ${BRAND.cliName} trust add`);
|
|
3191
3678
|
process.exit(1);
|
|
3192
3679
|
}
|
|
3193
|
-
targetPath =
|
|
3680
|
+
targetPath = appProjectPath(root, 'mcp.json'); // may not exist yet — writeConfig creates it
|
|
3194
3681
|
}
|
|
3195
3682
|
cfg = await readConfig(targetPath);
|
|
3196
3683
|
cfg.mcpServers[plan.alias] = plan.config;
|
|
3197
3684
|
await writeConfig(targetPath, cfg);
|
|
3198
3685
|
console.log(`ติดตั้ง MCP "${plan.alias}" จาก ${server.name} (${plan.source}) → ${targetPath}`);
|
|
3686
|
+
const { inferRegistryServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3687
|
+
console.log(`risk: ${formatMcpRiskLabel(inferRegistryServerRisk(server))}`);
|
|
3199
3688
|
if (plan.requirements.length)
|
|
3200
3689
|
console.log(`requirements: ${plan.requirements.join(', ')}`);
|
|
3201
3690
|
for (const warning of plan.warnings)
|
|
@@ -3204,10 +3693,16 @@ async function runMcp(args) {
|
|
|
3204
3693
|
return;
|
|
3205
3694
|
}
|
|
3206
3695
|
if (action === 'test' || action === 'doctor') {
|
|
3207
|
-
const { loadMcpConfig, probeMcpServer } = await import('./mcp.js');
|
|
3696
|
+
const { loadMcpConfig, probeMcpServer, isMcpServerEnabled } = await import('./mcp.js');
|
|
3697
|
+
const { inferConfiguredServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3208
3698
|
const logs = [];
|
|
3209
3699
|
const merged = await loadMcpConfig((m) => logs.push(m));
|
|
3210
|
-
const
|
|
3700
|
+
const explicitName = action === 'test' && rest[0] ? rest[0] : undefined;
|
|
3701
|
+
const names = explicitName
|
|
3702
|
+
? [explicitName]
|
|
3703
|
+
: action === 'test'
|
|
3704
|
+
? Object.keys(merged).filter((n) => isMcpServerEnabled(merged[n]))
|
|
3705
|
+
: Object.keys(merged);
|
|
3211
3706
|
if (!names.length) {
|
|
3212
3707
|
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp search github`);
|
|
3213
3708
|
return;
|
|
@@ -3223,9 +3718,14 @@ async function runMcp(args) {
|
|
|
3223
3718
|
console.log(`[FAIL] ${n} — ไม่เจอใน config`);
|
|
3224
3719
|
continue;
|
|
3225
3720
|
}
|
|
3721
|
+
if (!explicitName && action === 'doctor' && !isMcpServerEnabled(server)) {
|
|
3722
|
+
console.log(`[SKIP] ${n} (disabled)`);
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3226
3725
|
const probe = await probeMcpServer(server);
|
|
3726
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, server, probe.tools));
|
|
3227
3727
|
if (probe.ok) {
|
|
3228
|
-
console.log(`[PASS] ${n} (${probe.transport}) — ${probe.tools.length} tool(s)`);
|
|
3728
|
+
console.log(`[PASS] ${n} (${probe.transport}) — ${probe.tools.length} tool(s) · risk: ${risk}`);
|
|
3229
3729
|
for (const tool of probe.tools.slice(0, action === 'doctor' ? 8 : 30))
|
|
3230
3730
|
console.log(` - ${tool.name}${tool.description ? ` — ${tool.description}` : ''}`);
|
|
3231
3731
|
if (action === 'doctor' && probe.tools.length > 8)
|
|
@@ -3233,13 +3733,39 @@ async function runMcp(args) {
|
|
|
3233
3733
|
}
|
|
3234
3734
|
else {
|
|
3235
3735
|
failed = true;
|
|
3236
|
-
console.log(`[FAIL] ${n} (${probe.transport}) — ${probe.error}`);
|
|
3736
|
+
console.log(`[FAIL] ${n} (${probe.transport}) — ${probe.error} · risk: ${risk}`);
|
|
3737
|
+
if (probe.authHints?.length)
|
|
3738
|
+
for (const hint of probe.authHints)
|
|
3739
|
+
console.log(` hint: ${hint}`);
|
|
3237
3740
|
}
|
|
3238
3741
|
}
|
|
3239
3742
|
if (failed)
|
|
3240
3743
|
process.exit(1);
|
|
3241
3744
|
return;
|
|
3242
3745
|
}
|
|
3746
|
+
if (action === 'enable' || action === 'disable') {
|
|
3747
|
+
const { findMcpServerConfigPath } = await import('./mcp.js');
|
|
3748
|
+
const [name] = rest;
|
|
3749
|
+
if (!name) {
|
|
3750
|
+
console.error(`ใช้: ${BRAND.cliName} mcp ${action} <name>`);
|
|
3751
|
+
process.exit(1);
|
|
3752
|
+
}
|
|
3753
|
+
const targetPath = await findMcpServerConfigPath(name);
|
|
3754
|
+
if (!targetPath) {
|
|
3755
|
+
console.error(`ไม่เจอ MCP server "${name}" ใน global หรือ trusted project config`);
|
|
3756
|
+
process.exit(1);
|
|
3757
|
+
}
|
|
3758
|
+
const targetCfg = await readConfig(targetPath);
|
|
3759
|
+
if (!targetCfg.mcpServers[name]) {
|
|
3760
|
+
console.error(`ไม่เจอ MCP server "${name}" ใน ${targetPath}`);
|
|
3761
|
+
process.exit(1);
|
|
3762
|
+
}
|
|
3763
|
+
targetCfg.mcpServers[name] = { ...targetCfg.mcpServers[name], enabled: action === 'enable' };
|
|
3764
|
+
await writeConfig(targetPath, targetCfg);
|
|
3765
|
+
console.log(`${action === 'enable' ? 'เปิด' : 'ปิด'} MCP server "${name}" แล้ว → ${targetPath}`);
|
|
3766
|
+
console.log(`ทดสอบ: ${BRAND.cliName} mcp test ${name}`);
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3243
3769
|
if (action === 'add') {
|
|
3244
3770
|
const [name, command, ...cmdArgs] = rest;
|
|
3245
3771
|
if (!name || !command) {
|
|
@@ -3269,10 +3795,11 @@ async function runMcp(args) {
|
|
|
3269
3795
|
return;
|
|
3270
3796
|
}
|
|
3271
3797
|
if (action !== 'list') {
|
|
3272
|
-
console.log(`ใช้: ${BRAND.cliName} mcp [search|info|install|test|doctor|preset|list|add|remove|serve]`);
|
|
3798
|
+
console.log(`ใช้: ${BRAND.cliName} mcp [search|info|install|test|doctor|enable|disable|preset|list|add|remove|serve]`);
|
|
3273
3799
|
return;
|
|
3274
3800
|
}
|
|
3275
|
-
const { loadMcpConfig } = await import('./mcp.js');
|
|
3801
|
+
const { loadMcpConfig, isMcpServerEnabled } = await import('./mcp.js');
|
|
3802
|
+
const { inferConfiguredServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3276
3803
|
const logs = [];
|
|
3277
3804
|
const merged = await loadMcpConfig((m) => logs.push(m));
|
|
3278
3805
|
const names = Object.keys(merged);
|
|
@@ -3286,13 +3813,23 @@ async function runMcp(args) {
|
|
|
3286
3813
|
console.log(`${names.length} MCP servers:`);
|
|
3287
3814
|
for (const n of names) {
|
|
3288
3815
|
const s = merged[n];
|
|
3289
|
-
|
|
3816
|
+
const enabled = isMcpServerEnabled(s);
|
|
3817
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, s));
|
|
3818
|
+
console.log(` ${n}${enabled ? '' : ' (disabled)'} — risk: ${risk} — ${s.url ? `${s.url} (http)` : `${s.command} ${(s.args ?? []).join(' ')}`}`);
|
|
3290
3819
|
}
|
|
3291
3820
|
if (rest.includes('--tools')) {
|
|
3292
3821
|
const { probeMcpServer } = await import('./mcp.js');
|
|
3293
3822
|
for (const n of names) {
|
|
3823
|
+
if (!isMcpServerEnabled(merged[n])) {
|
|
3824
|
+
console.log(`\n[SKIP] ${n} tools (disabled)`);
|
|
3825
|
+
continue;
|
|
3826
|
+
}
|
|
3294
3827
|
const probe = await probeMcpServer(merged[n]);
|
|
3295
|
-
|
|
3828
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, merged[n], probe.tools));
|
|
3829
|
+
console.log(`\n${probe.ok ? '[PASS]' : '[FAIL]'} ${n} tools${probe.ok ? ` (${probe.tools.length})` : ` — ${probe.error}`} · risk: ${risk}`);
|
|
3830
|
+
if (probe.authHints?.length)
|
|
3831
|
+
for (const hint of probe.authHints)
|
|
3832
|
+
console.log(` hint: ${hint}`);
|
|
3296
3833
|
for (const tool of probe.tools.slice(0, 30))
|
|
3297
3834
|
console.log(` - ${tool.name}`);
|
|
3298
3835
|
}
|
|
@@ -3505,11 +4042,15 @@ async function main() {
|
|
|
3505
4042
|
process.exit(1);
|
|
3506
4043
|
}
|
|
3507
4044
|
const argv = process.argv.slice(2);
|
|
3508
|
-
|
|
4045
|
+
// --version/--help win in ANY position before `--` (so `sanook --help foo`, `-m x -h` work),
|
|
4046
|
+
// not only as the sole argument. (Quoted prompts like `sanook "explain --help"` are one token → unaffected.)
|
|
4047
|
+
const optArgsEnd = argv.indexOf('--');
|
|
4048
|
+
const topOpts = optArgsEnd === -1 ? argv : argv.slice(0, optArgsEnd);
|
|
4049
|
+
if (topOpts.includes('-v') || topOpts.includes('--version')) {
|
|
3509
4050
|
console.log(VERSION);
|
|
3510
4051
|
return;
|
|
3511
4052
|
}
|
|
3512
|
-
if (
|
|
4053
|
+
if (topOpts.includes('-h') || topOpts.includes('--help')) {
|
|
3513
4054
|
console.log(HELP);
|
|
3514
4055
|
return;
|
|
3515
4056
|
}
|
|
@@ -3529,6 +4070,8 @@ async function main() {
|
|
|
3529
4070
|
return runPureOneShot(argv.slice(1));
|
|
3530
4071
|
if (argv[0] === 'chat')
|
|
3531
4072
|
return runChat(argv.slice(1));
|
|
4073
|
+
if (argv[0] === 'plan')
|
|
4074
|
+
return runPlan(argv.slice(1));
|
|
3532
4075
|
if (argv[0] === 'setup')
|
|
3533
4076
|
return runSetup(argv.slice(1));
|
|
3534
4077
|
if (argv[0] === 'model' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
@@ -3543,8 +4086,19 @@ async function main() {
|
|
|
3543
4086
|
return runSessions(argv.slice(1));
|
|
3544
4087
|
if (argv[0] === 'insights')
|
|
3545
4088
|
return runInsights(argv.slice(1));
|
|
4089
|
+
if (argv[0] === 'memory' && ['log', 'stats', undefined].includes(argv[1]))
|
|
4090
|
+
return runMemory(argv.slice(1));
|
|
3546
4091
|
if (argv[0] === 'dump')
|
|
3547
4092
|
return runDump(argv.slice(1));
|
|
4093
|
+
if (argv[0] === 'prompt-size' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4094
|
+
return runPromptSize(argv.slice(1));
|
|
4095
|
+
if (argv[0] === 'runtimes' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4096
|
+
return runRuntimes(argv.slice(1));
|
|
4097
|
+
if (argv[0] === 'dashboard' && (argv.length === 1 || !argv[1].startsWith('-') || argv[1] === '--port')) {
|
|
4098
|
+
return runDashboard(argv.slice(1));
|
|
4099
|
+
}
|
|
4100
|
+
if (argv[0] === 'web' && ['status', 'doctor', 'fetch', 'search', 'setup', undefined].includes(argv[1]))
|
|
4101
|
+
return runWeb(argv.slice(1));
|
|
3548
4102
|
if (argv[0] === 'tools' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
3549
4103
|
return runTools(argv.slice(1));
|
|
3550
4104
|
if (argv[0] === 'send')
|
|
@@ -3557,12 +4111,15 @@ async function main() {
|
|
|
3557
4111
|
if (argv[0] === 'cron' && ['add', 'list', 'rm', 'remove', undefined].includes(argv[1])) {
|
|
3558
4112
|
return runCron(argv.slice(1));
|
|
3559
4113
|
}
|
|
3560
|
-
if (argv[0] === 'skill' && ['list', 'add', 'remove', 'rm', undefined].includes(argv[1])) {
|
|
4114
|
+
if (argv[0] === 'skill' && ['list', 'add', 'install', 'remove', 'rm', undefined].includes(argv[1])) {
|
|
3561
4115
|
return runSkill(argv.slice(1));
|
|
3562
4116
|
}
|
|
4117
|
+
if (argv[0] === 'init' && (argv.length === 1 || argv[1].startsWith('-')))
|
|
4118
|
+
return runInit(argv.slice(1));
|
|
3563
4119
|
if (argv[0] === 'models')
|
|
3564
4120
|
return runModels(argv.slice(1));
|
|
3565
|
-
if (argv[0] === 'brain' &&
|
|
4121
|
+
if (argv[0] === 'brain' &&
|
|
4122
|
+
['init', 'doctor', 'context', 'eval', 'review', 'projects', 'pack', 'new', 'repair', 'consolidate', 'metrics', 'final', undefined].includes(argv[1]))
|
|
3566
4123
|
return runBrain(argv.slice(1));
|
|
3567
4124
|
if (argv[0] === 'config' && ['get', 'set', 'list', undefined].includes(argv[1]))
|
|
3568
4125
|
return runConfig(argv.slice(1));
|
|
@@ -3572,11 +4129,21 @@ async function main() {
|
|
|
3572
4129
|
return runSearch(argv.slice(1));
|
|
3573
4130
|
if (argv[0] === 'mcp' && argv[1] === 'serve')
|
|
3574
4131
|
return runMcpServe();
|
|
3575
|
-
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', 'search', 'info', 'install', 'test', 'doctor', 'preset', undefined].includes(argv[1]))
|
|
4132
|
+
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', 'search', 'info', 'install', 'test', 'doctor', 'enable', 'disable', 'preset', undefined].includes(argv[1]))
|
|
3576
4133
|
return runMcp(argv.slice(1));
|
|
3577
4134
|
if (argv[0] === 'trust' && ['status', 'add', 'remove', 'rm', undefined].includes(argv[1]))
|
|
3578
4135
|
return runTrust(argv.slice(1));
|
|
3579
|
-
|
|
4136
|
+
// A management command word whose subcommand didn't match any route above → don't silently
|
|
4137
|
+
// fall through and run it as an LLM task (costly + confusing). These words intentionally
|
|
4138
|
+
// require a valid subcommand; an NL prompt starting with one can still be quoted as a single arg.
|
|
4139
|
+
const MANAGEMENT_WORDS = new Set(['config', 'mcp', 'brain', 'web', 'trust', 'cron', 'skill', 'init', 'dashboard']);
|
|
4140
|
+
if (argv[0] && MANAGEMENT_WORDS.has(argv[0]) && argv[1] && !argv[1].startsWith('-')) {
|
|
4141
|
+
console.error(`${BRAND.cliName}: ไม่รู้จัก subcommand "${argv[0]} ${argv[1]}" — ดูวิธีใช้: ${BRAND.cliName} --help`);
|
|
4142
|
+
process.exit(1);
|
|
4143
|
+
}
|
|
4144
|
+
const { model, budget, budgetInvalid, json, quiet, prompt: argPrompt, planMode, yes, resume } = parseArgs(argv);
|
|
4145
|
+
if (budgetInvalid)
|
|
4146
|
+
process.stderr.write(`${BRAND.cliName}: ⚠ --budget ไม่ถูกต้อง (ต้องเป็นจำนวนบวก) — รันต่อโดยไม่มี spend cap\n`);
|
|
3580
4147
|
const resumeSession = await requestedResumeSession(argv, resume);
|
|
3581
4148
|
const budgetUsd = Number.isFinite(budget) ? budget : undefined;
|
|
3582
4149
|
// stdin piping: `git diff | sanook "review this"` → ผนวก stdin เข้า prompt (headless/CI)
|
|
@@ -3592,7 +4159,7 @@ async function main() {
|
|
|
3592
4159
|
}
|
|
3593
4160
|
// --continue / -c → โหลด session ล่าสุดมาต่อ (จำว่าทำถึงไหน)
|
|
3594
4161
|
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);
|
|
4162
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode, quiet, config.fallbackModel, planMode ? prompt : undefined);
|
|
3596
4163
|
return;
|
|
3597
4164
|
}
|
|
3598
4165
|
await maybePromptForInteractiveUpdate();
|