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.
Files changed (127) hide show
  1. package/CHANGELOG.md +112 -2
  2. package/README.md +15 -3
  3. package/README.th.md +8 -1
  4. package/dist/approval.js +7 -0
  5. package/dist/bin.js +637 -56
  6. package/dist/brain-consolidate.js +335 -0
  7. package/dist/brain-context.js +42 -3
  8. package/dist/brain-final.js +15 -9
  9. package/dist/brain-link.js +73 -0
  10. package/dist/brain-metrics.js +277 -0
  11. package/dist/brain-new.js +402 -0
  12. package/dist/brain-pack.js +210 -0
  13. package/dist/brain-repair.js +280 -0
  14. package/dist/brain.js +3 -0
  15. package/dist/brand.js +4 -0
  16. package/dist/cli-args.js +47 -9
  17. package/dist/cli-option-values.js +1 -1
  18. package/dist/clipboard.js +65 -0
  19. package/dist/commands.js +98 -15
  20. package/dist/config.js +66 -34
  21. package/dist/context-pack.js +145 -0
  22. package/dist/cost.js +20 -0
  23. package/dist/dashboard/api-helpers.js +87 -0
  24. package/dist/dashboard/server.js +179 -0
  25. package/dist/dashboard/static/app.js +277 -0
  26. package/dist/dashboard/static/index.html +39 -0
  27. package/dist/dashboard/static/styles.css +85 -0
  28. package/dist/diff.js +10 -2
  29. package/dist/gateway/auth.js +14 -3
  30. package/dist/gateway/deliver.js +45 -3
  31. package/dist/gateway/doctor.js +456 -0
  32. package/dist/gateway/email.js +30 -1
  33. package/dist/gateway/ledger.js +20 -1
  34. package/dist/gateway/session.js +34 -11
  35. package/dist/hotkeys.js +21 -0
  36. package/dist/i18n/en.js +98 -0
  37. package/dist/i18n/index.js +19 -0
  38. package/dist/i18n/th.js +98 -0
  39. package/dist/i18n/types.js +1 -0
  40. package/dist/insights-args.js +24 -4
  41. package/dist/knowledge.js +55 -29
  42. package/dist/loop.js +65 -9
  43. package/dist/mcp-hub.js +33 -0
  44. package/dist/mcp-registry.js +153 -9
  45. package/dist/mcp-risk.js +71 -0
  46. package/dist/mcp.js +77 -5
  47. package/dist/memory-log.js +90 -0
  48. package/dist/memory-store.js +37 -1
  49. package/dist/memory.js +51 -7
  50. package/dist/model-picker.js +58 -0
  51. package/dist/orchestrate.js +7 -5
  52. package/dist/plan-handoff.js +17 -0
  53. package/dist/polyglot.js +162 -0
  54. package/dist/process-runner.js +96 -0
  55. package/dist/project-init.js +91 -0
  56. package/dist/project-registry.js +143 -0
  57. package/dist/project-scaffold.js +124 -0
  58. package/dist/prompt-size.js +155 -0
  59. package/dist/providers/codex-login.js +138 -0
  60. package/dist/providers/codex.js +20 -8
  61. package/dist/providers/keys.js +21 -0
  62. package/dist/providers/models.js +1 -1
  63. package/dist/providers/registry.js +11 -1
  64. package/dist/search/cli.js +9 -1
  65. package/dist/search/embedding-config.js +22 -0
  66. package/dist/search/engine.js +2 -13
  67. package/dist/search/indexer.js +10 -10
  68. package/dist/session-brain.js +103 -0
  69. package/dist/session-distill.js +84 -0
  70. package/dist/session.js +1 -11
  71. package/dist/skill-install.js +24 -1
  72. package/dist/skills.js +33 -0
  73. package/dist/slash-completion.js +155 -0
  74. package/dist/support-dump.js +31 -0
  75. package/dist/tool-catalog.js +59 -0
  76. package/dist/tools/index.js +5 -0
  77. package/dist/tools/permission.js +82 -16
  78. package/dist/tools/polyglot.js +126 -0
  79. package/dist/tools/sandbox.js +38 -13
  80. package/dist/tools/search.js +9 -2
  81. package/dist/tools/task.js +22 -2
  82. package/dist/tools/timeout.js +7 -5
  83. package/dist/tools/web-fetch-tool.js +33 -0
  84. package/dist/turn-retrieval.js +83 -0
  85. package/dist/ui/app.js +874 -35
  86. package/dist/ui/banner.js +78 -4
  87. package/dist/ui/markdown.js +122 -0
  88. package/dist/ui/overlay.js +496 -0
  89. package/dist/ui/queue.js +23 -0
  90. package/dist/ui/render.js +30 -2
  91. package/dist/ui/session-panel.js +115 -0
  92. package/dist/ui/setup-providers.js +40 -0
  93. package/dist/ui/setup.js +163 -50
  94. package/dist/ui/status.js +142 -0
  95. package/dist/ui/thinking-panel.js +36 -0
  96. package/dist/ui/tool-trail.js +97 -0
  97. package/dist/ui/transcript.js +26 -0
  98. package/dist/ui/useBusyElapsed.js +19 -0
  99. package/dist/ui/useEditor.js +144 -5
  100. package/dist/ui/useGitBranch.js +57 -0
  101. package/dist/update.js +32 -6
  102. package/dist/usage-cli.js +160 -0
  103. package/dist/usage-ledger.js +169 -0
  104. package/dist/web-fetch.js +637 -0
  105. package/dist/web-surface.js +190 -0
  106. package/package.json +4 -3
  107. package/scripts/postinstall.mjs +4 -4
  108. package/second-brain/Projects/_Index.md +17 -4
  109. package/second-brain/Projects/sanook-cli/_Index.md +7 -3
  110. package/second-brain/Projects/sanook-cli/context.md +35 -0
  111. package/second-brain/Projects/sanook-cli/current-state.md +32 -0
  112. package/second-brain/Projects/sanook-cli/overview.md +41 -0
  113. package/second-brain/Projects/sanook-cli/repo.md +34 -0
  114. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +52 -11
  115. package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
  116. package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
  117. package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
  118. package/second-brain/Research/_Index.md +2 -0
  119. package/second-brain/Shared/Operating-State/current-state.md +14 -23
  120. package/second-brain/Shared/Tech-Standards/_Index.md +2 -0
  121. package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
  122. package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
  123. package/second-brain/Templates/project-workspace/_Index.md +31 -0
  124. package/second-brain/Templates/project-workspace/context.md +28 -0
  125. package/second-brain/Templates/project-workspace/current-state.md +29 -0
  126. package/second-brain/Templates/project-workspace/overview.md +39 -0
  127. 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 "..."] แสดง context ที่ Sanook จะ inject + retrieval hits ต่อ 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 skill list | add <source> | remove <name> */
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('ใช้: sanook skill add <github "user/repo" | URL ของ SKILL.md | local path>');
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 { installSkill } = await import('./skill-install.js');
3051
+ const { installNamedSkill } = await import('./skill-install.js');
2700
3052
  try {
2701
- const installed = await installSkill(source, (m) => process.stderr.write(`${DIM}${m}${RESET}\n`));
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
- const target = expandHome(pathArg ?? join(homedir(), 'Documents', BRAIN_DEFAULTS.vaultName));
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 name = positionals(rest, new Set(['--name', '--transport', '--env', '--header', '--version']))[0];
3137
- if (!name) {
3138
- console.error(`ใช้: ${BRAND.cliName} mcp install <registry-server-name> [--name alias] [--transport auto|remote|stdio] [--env KEY=value] [--header KEY=value] [--project]`);
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 optionValues = (flag) => {
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: valueOf('--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(optionValues('--env')),
3171
- headers: parseKeyValueList(optionValues('--header')),
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 (rest.includes('--project')) {
3186
- const { projectConfigPathIfTrusted, projectRoot } = await import('./trust.js');
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 projectPath = await projectConfigPathIfTrusted('mcp.json', root);
3189
- if (!projectPath) {
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 = projectPath;
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 names = action === 'test' && rest[0] ? [rest[0]] : Object.keys(merged);
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
- console.log(` ${n} — ${s.url ? `${s.url} (http)` : `${s.command} ${(s.args ?? []).join(' ')}`}`);
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
- console.log(`\n${probe.ok ? '[PASS]' : '[FAIL]'} ${n} tools${probe.ok ? ` (${probe.tools.length})` : ` — ${probe.error}`}`);
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
- if (argv.length === 1 && (argv[0] === '-v' || argv[0] === '--version')) {
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 (argv.length === 1 && (argv[0] === '-h' || argv[0] === '--help')) {
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' && ['init', 'doctor', 'context', 'eval', 'review', 'final', undefined].includes(argv[1]))
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
- const { model, budget, json, quiet, prompt: argPrompt, planMode, yes, resume } = parseArgs(argv);
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();