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