sanook-cli 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +173 -0
  3. package/README.md +153 -20
  4. package/README.th.md +136 -0
  5. package/dist/agentContext.js +4 -0
  6. package/dist/approval.js +6 -0
  7. package/dist/bin.js +405 -57
  8. package/dist/brain.js +92 -59
  9. package/dist/brand.js +47 -0
  10. package/dist/checkpoint.js +37 -0
  11. package/dist/commands.js +86 -6
  12. package/dist/compaction.js +76 -5
  13. package/dist/config.js +100 -12
  14. package/dist/cost.js +60 -3
  15. package/dist/doctor.js +92 -0
  16. package/dist/gateway/auth.js +2 -2
  17. package/dist/gateway/ledger.js +2 -2
  18. package/dist/gateway/scheduler.js +1 -0
  19. package/dist/gateway/serve.js +6 -4
  20. package/dist/gateway/server.js +10 -2
  21. package/dist/git.js +11 -2
  22. package/dist/hooks.js +43 -17
  23. package/dist/knowledge.js +48 -49
  24. package/dist/loop.js +182 -66
  25. package/dist/lsp/client.js +173 -0
  26. package/dist/lsp/framing.js +56 -0
  27. package/dist/lsp/index.js +138 -0
  28. package/dist/lsp/servers.js +82 -0
  29. package/dist/mcp-server.js +244 -0
  30. package/dist/mcp.js +184 -29
  31. package/dist/memory-store.js +559 -0
  32. package/dist/memory.js +143 -29
  33. package/dist/orchestrate.js +150 -0
  34. package/dist/providers/codex.js +21 -7
  35. package/dist/providers/keys.js +3 -2
  36. package/dist/providers/models.js +22 -6
  37. package/dist/providers/registry.js +155 -1
  38. package/dist/repomap.js +93 -0
  39. package/dist/search/chunk.js +158 -0
  40. package/dist/search/embed-store.js +187 -0
  41. package/dist/search/engine.js +203 -0
  42. package/dist/search/fuse.js +35 -0
  43. package/dist/search/index-core.js +187 -0
  44. package/dist/search/indexer.js +241 -0
  45. package/dist/search/store.js +77 -0
  46. package/dist/session.js +42 -8
  47. package/dist/skill-install.js +10 -10
  48. package/dist/skills.js +12 -9
  49. package/dist/summarize.js +31 -0
  50. package/dist/tools/bash.js +21 -2
  51. package/dist/tools/diagnostics.js +41 -0
  52. package/dist/tools/edit.js +29 -7
  53. package/dist/tools/index.js +8 -1
  54. package/dist/tools/list.js +7 -2
  55. package/dist/tools/permission.js +90 -9
  56. package/dist/tools/read.js +23 -4
  57. package/dist/tools/remember.js +1 -1
  58. package/dist/tools/sandbox.js +61 -0
  59. package/dist/tools/search.js +105 -4
  60. package/dist/tools/task.js +195 -29
  61. package/dist/tools/timeout.js +35 -0
  62. package/dist/tools/util.js +10 -0
  63. package/dist/tools/write.js +6 -4
  64. package/dist/trust.js +89 -0
  65. package/dist/ui/app.js +228 -31
  66. package/dist/ui/banner.js +4 -9
  67. package/dist/ui/brain-wizard.js +2 -2
  68. package/dist/ui/history.js +30 -0
  69. package/dist/ui/mentions.js +44 -0
  70. package/dist/ui/render.js +55 -15
  71. package/dist/ui/setup.js +97 -12
  72. package/dist/ui/useEditor.js +83 -0
  73. package/dist/update.js +114 -0
  74. package/dist/worktree.js +173 -0
  75. package/package.json +11 -5
  76. package/scripts/postinstall.mjs +33 -0
  77. package/second-brain/.agents/_Index.md +30 -0
  78. package/second-brain/.agents/skills/_Index.md +30 -0
  79. package/second-brain/.agents/workflows/_Index.md +30 -0
  80. package/second-brain/AGENTS.md +4 -4
  81. package/second-brain/Acceptance/_Index.md +30 -0
  82. package/second-brain/Acceptance/golden-case-template.md +39 -0
  83. package/second-brain/Areas/_Index.md +30 -0
  84. package/second-brain/Bugs/System-OS/_Index.md +30 -0
  85. package/second-brain/Bugs/_Index.md +30 -0
  86. package/second-brain/CLAUDE.md +4 -1
  87. package/second-brain/Checklists/_Index.md +30 -0
  88. package/second-brain/Checklists/preflight-postflight-template.md +29 -0
  89. package/second-brain/Distillations/_Index.md +30 -0
  90. package/second-brain/Entities/_Index.md +30 -0
  91. package/second-brain/Entities/entity-template.md +33 -0
  92. package/second-brain/Evals/_Index.md +30 -0
  93. package/second-brain/Evals/correction-pairs.md +24 -0
  94. package/second-brain/Evals/failure-taxonomy.md +24 -0
  95. package/second-brain/Evals/golden-set.md +25 -0
  96. package/second-brain/Evals/quality-ledger.md +23 -0
  97. package/second-brain/Evals/self-eval-rubric.md +23 -0
  98. package/second-brain/GEMINI.md +4 -4
  99. package/second-brain/Goals/_Index.md +30 -0
  100. package/second-brain/Handoffs/_Index.md +30 -0
  101. package/second-brain/Home.md +7 -0
  102. package/second-brain/Intake/Raw Sources/_Index.md +30 -0
  103. package/second-brain/Intake/_Index.md +30 -0
  104. package/second-brain/Intake/_Quarantine/_Index.md +30 -0
  105. package/second-brain/Learning/_Index.md +30 -0
  106. package/second-brain/Playbooks/_Index.md +30 -0
  107. package/second-brain/Playbooks/playbook-template.md +23 -0
  108. package/second-brain/Projects/_Index.md +30 -0
  109. package/second-brain/Prompts/_Index.md +30 -0
  110. package/second-brain/README.md +2 -1
  111. package/second-brain/Research/_Index.md +30 -0
  112. package/second-brain/Retrospectives/_Index.md +30 -0
  113. package/second-brain/Reviews/_Index.md +30 -0
  114. package/second-brain/Runbooks/_Index.md +30 -0
  115. package/second-brain/Runbooks/eval-loop.md +24 -0
  116. package/second-brain/Sessions/_Index.md +30 -0
  117. package/second-brain/Shared/AI-Context-Index.md +20 -0
  118. package/second-brain/Shared/AI-Threads/_Index.md +30 -0
  119. package/second-brain/Shared/Archive/_Index.md +30 -0
  120. package/second-brain/Shared/Assets/_Index.md +30 -0
  121. package/second-brain/Shared/Context-Packs/_Index.md +30 -0
  122. package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
  123. package/second-brain/Shared/Coordination/NOW.md +28 -0
  124. package/second-brain/Shared/Coordination/_Index.md +30 -0
  125. package/second-brain/Shared/Coordination/agent-registry.md +24 -0
  126. package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
  127. package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
  128. package/second-brain/Shared/Coordination/task-board.md +32 -0
  129. package/second-brain/Shared/Core-Facts/_Index.md +30 -0
  130. package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
  131. package/second-brain/Shared/Glossary/_Index.md +30 -0
  132. package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
  133. package/second-brain/Shared/Operating-State/_Index.md +30 -0
  134. package/second-brain/Shared/Prompting/_Index.md +30 -0
  135. package/second-brain/Shared/Provenance/_Index.md +30 -0
  136. package/second-brain/Shared/Rules/_Index.md +30 -0
  137. package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
  138. package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
  139. package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
  140. package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
  141. package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
  142. package/second-brain/Shared/Rules/rules-formatting.md +34 -0
  143. package/second-brain/Shared/Scripts/_Index.md +30 -0
  144. package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
  145. package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
  146. package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
  147. package/second-brain/Shared/User-Memory/_Index.md +30 -0
  148. package/second-brain/Shared/User-Persona/_Index.md +30 -0
  149. package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
  150. package/second-brain/Shared/Working-Memory/_Index.md +30 -0
  151. package/second-brain/Shared/_Index.md +30 -0
  152. package/second-brain/Shared/mcp-servers/_Index.md +30 -0
  153. package/second-brain/Skills/_Index.md +30 -0
  154. package/second-brain/Templates/_Index.md +30 -0
  155. package/second-brain/Templates/bug.md +2 -0
  156. package/second-brain/Templates/handoff.md +2 -0
  157. package/second-brain/Templates/session.md +2 -0
  158. package/second-brain/Tools/_Index.md +30 -0
  159. package/second-brain/Traces/_Index.md +30 -0
  160. package/second-brain/Vault Structure Map.md +33 -1
  161. package/second-brain/copilot/_Index.md +30 -0
  162. package/skills/audit-license-compliance/SKILL.md +117 -0
  163. package/skills/author-codemod/SKILL.md +110 -0
  164. package/skills/build-audit-logging/SKILL.md +112 -0
  165. package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
  166. package/skills/build-cli-tool/SKILL.md +108 -0
  167. package/skills/build-data-table/SKILL.md +141 -0
  168. package/skills/build-native-mobile-ui/SKILL.md +154 -0
  169. package/skills/build-offline-first-sync/SKILL.md +118 -0
  170. package/skills/build-realtime-channel/SKILL.md +122 -0
  171. package/skills/build-vector-search/SKILL.md +131 -0
  172. package/skills/compose-local-dev-stack/SKILL.md +149 -0
  173. package/skills/configure-bundler-build/SKILL.md +166 -0
  174. package/skills/configure-dns-tls/SKILL.md +142 -0
  175. package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
  176. package/skills/configure-security-headers-csp/SKILL.md +122 -0
  177. package/skills/contract-testing/SKILL.md +140 -0
  178. package/skills/datetime-timezone-correctness/SKILL.md +125 -0
  179. package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
  180. package/skills/debug-flaky-tests/SKILL.md +128 -0
  181. package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
  182. package/skills/deliver-webhooks/SKILL.md +116 -0
  183. package/skills/design-api-pagination/SKILL.md +144 -0
  184. package/skills/design-authorization-model/SKILL.md +119 -0
  185. package/skills/design-backup-dr-recovery/SKILL.md +113 -0
  186. package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
  187. package/skills/design-multi-tenancy/SKILL.md +100 -0
  188. package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
  189. package/skills/design-relational-schema/SKILL.md +129 -0
  190. package/skills/design-search-index-infra/SKILL.md +151 -0
  191. package/skills/design-state-machine/SKILL.md +108 -0
  192. package/skills/design-token-system/SKILL.md +109 -0
  193. package/skills/distributed-locks-leases/SKILL.md +120 -0
  194. package/skills/encrypt-sensitive-data/SKILL.md +148 -0
  195. package/skills/feature-flags-rollout/SKILL.md +130 -0
  196. package/skills/file-upload-object-storage/SKILL.md +107 -0
  197. package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
  198. package/skills/harden-llm-app-reliability/SKILL.md +126 -0
  199. package/skills/i18n-localization-setup/SKILL.md +113 -0
  200. package/skills/idempotency-keys/SKILL.md +107 -0
  201. package/skills/implement-push-notifications/SKILL.md +142 -0
  202. package/skills/ingest-webhook-secure/SKILL.md +120 -0
  203. package/skills/integrate-oauth-oidc/SKILL.md +126 -0
  204. package/skills/load-stress-test/SKILL.md +129 -0
  205. package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
  206. package/skills/model-nosql-data/SKILL.md +118 -0
  207. package/skills/money-decimal-arithmetic/SKILL.md +123 -0
  208. package/skills/monitor-ml-drift/SKILL.md +109 -0
  209. package/skills/numeric-precision-units/SKILL.md +144 -0
  210. package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
  211. package/skills/optimize-react-rerenders/SKILL.md +124 -0
  212. package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
  213. package/skills/payments-billing-integration/SKILL.md +114 -0
  214. package/skills/pin-toolchain-versions/SKILL.md +116 -0
  215. package/skills/plan-strangler-migration/SKILL.md +95 -0
  216. package/skills/property-based-testing/SKILL.md +108 -0
  217. package/skills/publish-package-registry/SKILL.md +130 -0
  218. package/skills/recover-git-state/SKILL.md +119 -0
  219. package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
  220. package/skills/resilience-timeouts-retries/SKILL.md +104 -0
  221. package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
  222. package/skills/rewrite-git-history/SKILL.md +109 -0
  223. package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
  224. package/skills/schema-evolution-compatibility/SKILL.md +121 -0
  225. package/skills/send-transactional-email/SKILL.md +126 -0
  226. package/skills/serve-deploy-ml-model/SKILL.md +107 -0
  227. package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
  228. package/skills/setup-devcontainer-env/SKILL.md +131 -0
  229. package/skills/setup-lint-format-precommit/SKILL.md +140 -0
  230. package/skills/setup-monorepo-tooling/SKILL.md +125 -0
  231. package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
  232. package/skills/structured-output-llm/SKILL.md +86 -0
  233. package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
  234. package/skills/test-data-factories/SKILL.md +158 -0
  235. package/skills/threat-model-stride/SKILL.md +123 -0
  236. package/skills/train-evaluate-ml-model/SKILL.md +109 -0
  237. package/skills/unicode-text-correctness/SKILL.md +109 -0
  238. package/skills/visual-regression-testing/SKILL.md +120 -0
package/dist/bin.js CHANGED
@@ -1,19 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { runAgent } from './loop.js';
3
3
  import { redactKey } from './providers/keys.js';
4
- import { loadConfig, isFirstRun, loadKeysIntoEnv } from './config.js';
4
+ import { specKey, parseSpec, PROVIDERS, consoleUrl, detectEnvProvider, hasUsableEnvKey } from './providers/registry.js';
5
+ import { resolveKeyFromEnv } from './providers/keys.js';
6
+ import { hasPricingForKey } from './cost.js';
7
+ import { loadConfig, isFirstRun, loadKeysIntoEnv, parsePricingOverride } from './config.js';
5
8
  import { saveSession, latestSession, newSessionId } from './session.js';
6
- import { closeMcp } from './mcp.js';
9
+ import { closeMcp, isValidMcpServerName } from './mcp.js';
7
10
  import { readFileSync } from 'node:fs';
8
11
  import { homedir } from 'node:os';
9
12
  import { join, dirname } from 'node:path';
10
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
11
- const DIM = '\x1b[2m';
12
- const RESET = '\x1b[0m';
13
+ import { chmod, readFile, writeFile, mkdir } from 'node:fs/promises';
14
+ import { createInterface } from 'node:readline/promises';
15
+ import { appHomePath, BRAND, BRAND_ENV, envFlag } from './brand.js';
16
+ // สี: เคารพ NO_COLOR + auto-plain เมื่อ pipe/redirect (legacy Windows cmd ก็ไม่เห็น garbage ANSI); FORCE_COLOR บังคับได้
17
+ const useColor = !process.env.NO_COLOR && (Boolean(process.env.FORCE_COLOR) || process.stdout.isTTY === true);
18
+ const DIM = useColor ? '\x1b[2m' : '';
19
+ const RESET = useColor ? '\x1b[0m' : '';
13
20
  function parseArgs(argv) {
14
21
  let model;
15
22
  let budget;
16
23
  let json = false;
24
+ let quiet = false;
17
25
  let planMode = false;
18
26
  let yes = false;
19
27
  const rest = [];
@@ -25,6 +33,16 @@ function parseArgs(argv) {
25
33
  budget = Number.parseFloat(argv[++i] ?? '');
26
34
  else if (a === '--json')
27
35
  json = true;
36
+ else if (a === '-q' || a === '--quiet')
37
+ quiet = true;
38
+ else if (a === '--output-format') {
39
+ const v = argv[++i];
40
+ if (v === 'json')
41
+ json = true;
42
+ else if (v === 'final' || v === 'quiet')
43
+ quiet = true;
44
+ /* 'text' = default */
45
+ }
28
46
  else if (a === '--plan')
29
47
  planMode = true;
30
48
  else if (a === '--yes' || a === '-y')
@@ -35,17 +53,27 @@ function parseArgs(argv) {
35
53
  else
36
54
  rest.push(a);
37
55
  }
38
- return { model, budget, json, prompt: rest.join(' ').trim(), planMode, yes };
56
+ return { model, budget, json, quiet, prompt: rest.join(' ').trim(), planMode, yes };
39
57
  }
40
- async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'auto') {
58
+ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'ask', quiet = false, fallbackModel) {
41
59
  const controller = new AbortController();
42
60
  process.on('SIGINT', () => {
43
61
  controller.abort();
44
62
  process.exit(130);
45
63
  });
64
+ // budget cap ตั้งไว้แต่ไม่มี pricing สำหรับ model นี้ → cap จะไม่ทำงาน เตือนไม่ให้เงียบ (correctness)
65
+ if (budgetUsd != null && !hasPricingForKey(specKey(model)) && !json) {
66
+ process.stderr.write(`${DIM}⚠ budget $${budgetUsd} ตั้งไว้ แต่ไม่มี pricing สำหรับ ${model} → cap จะไม่ทำงาน ` +
67
+ `(ตั้งราคาเอง: ${BRAND.cliName} config set pricing '{"${specKey(model)}":{"input":1,"output":3}}')${RESET}\n`);
68
+ }
69
+ // เตือน fallback model ด้วย (budget cap re-key ไป fallback ตอน primary ล้ม) — ไม่ซ้ำถ้าทั้งคู่ไม่มี pricing
70
+ if (budgetUsd != null && fallbackModel && fallbackModel !== model && !hasPricingForKey(specKey(fallbackModel)) && !json) {
71
+ process.stderr.write(`${DIM}⚠ fallback model ${fallbackModel} ไม่มี pricing → budget cap จะไม่ทำงานถ้า fallback ถูกใช้${RESET}\n`);
72
+ }
46
73
  try {
47
74
  const { cost, messages } = await runAgent({
48
75
  model,
76
+ fallbackModel,
49
77
  prompt,
50
78
  history,
51
79
  budgetUsd,
@@ -60,15 +88,23 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
60
88
  }
61
89
  if (e.type === 'text')
62
90
  process.stdout.write(e.text ?? '');
63
- else if (e.type === 'tool-call')
91
+ else if (e.type === 'tool-call' && !quiet)
64
92
  process.stdout.write(`\n${DIM}→ ${e.tool}${RESET}\n`);
65
93
  },
66
94
  });
67
- if (!json)
95
+ if (!json && !quiet)
68
96
  process.stdout.write(`\n${DIM}${cost.summary()}${RESET}\n`);
97
+ else if (quiet)
98
+ process.stdout.write('\n');
69
99
  // จำ session ไว้ทำงานต่อได้ (sanook --continue "...") — แก้ concern AI ลืมว่าทำถึงไหน
70
100
  const now = new Date().toISOString();
71
101
  await saveSession({ id: newSessionId(), created: now, updated: now, model, cwd: process.cwd(), messages });
102
+ // auto-worklog เข้า second-brain (ถ้าตั้ง brainPath) — "vault จำว่าวันนี้ทำอะไร"
103
+ const { getBrainPath, appendBrainWorklog } = await import('./memory.js');
104
+ const brain = await getBrainPath();
105
+ if (brain) {
106
+ await appendBrainWorklog(brain, { prompt, summary: cost.summary(), model, today: now.slice(0, 10) }).catch(() => { });
107
+ }
72
108
  }
73
109
  catch (err) {
74
110
  const msg = redactKey(err.message);
@@ -80,38 +116,49 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
80
116
  }
81
117
  }
82
118
  // อ่านจาก package.json (single source of truth) — กัน version constant drift
83
- const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
84
- const HELP = `Sanook — a terminal AI coding agent (BYOK)
119
+ const PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
120
+ const VERSION = PACKAGE.version;
121
+ const PACKAGE_NAME = PACKAGE.name;
122
+ const HELP = `${BRAND.productName} — a terminal AI coding agent (BYOK)
85
123
 
86
124
  usage:
87
- sanook "<task>" run one task (headless)
88
- sanook interactive REPL
89
- sanook --json "<task>" headless, JSONL output (for CI/scripts)
125
+ ${BRAND.cliName} "<task>" run one task (headless)
126
+ ${BRAND.cliName} interactive REPL
127
+ ${BRAND.cliName} --json "<task>" headless, JSONL output (for CI/scripts)
128
+ ${BRAND.cliName} update update ${BRAND.cliName} to the latest npm release
129
+ ${BRAND.cliName} doctor ตรวจการติดตั้ง + วิธีแก้ PATH (เมื่อพิมพ์ "${BRAND.cliName}" แล้วไม่เจอ)
90
130
 
91
131
  gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
92
- sanook serve [--port 8787] เปิด gateway (OpenAI-compat /v1/chat/completions + scheduler)
93
- sanook cron add "<when>" "<task>" ตั้งงานล่วงหน้า (when: "every 30m" | "09:00" | ISO | now)
94
- sanook cron list ดู task ทั้งหมด
95
- sanook cron rm <id> ลบ task
132
+ ${BRAND.cliName} serve [--port 8787] เปิด gateway (OpenAI-compat /v1/chat/completions + scheduler)
133
+ ${BRAND.cliName} cron add "<when>" "<task>" ตั้งงานล่วงหน้า (when: "every 30m" | "09:00" | ISO | now)
134
+ ${BRAND.cliName} cron list ดู task ทั้งหมด
135
+ ${BRAND.cliName} cron rm <id> ลบ task
96
136
 
97
- skills (69 built-in + ติดตั้งเพิ่มได้):
98
- sanook skill list ดู skill ทั้งหมด
99
- sanook skill add <user/repo|url|path> ติดตั้ง skill จาก GitHub / URL / local
100
- sanook skill remove <name> ลบ skill ที่ติดตั้ง
101
- sanook models [provider] ดู/verify model id (เทียบ provider จริงถ้ามี key)
137
+ skills (built-in + ติดตั้งเพิ่มได้):
138
+ ${BRAND.cliName} skill list ดู skill ทั้งหมด
139
+ ${BRAND.cliName} skill add <user/repo|url|path> ติดตั้ง skill จาก GitHub / URL / local
140
+ ${BRAND.cliName} skill remove <name> ลบ skill ที่ติดตั้ง
141
+ ${BRAND.cliName} models [provider] ดู/verify model id (เทียบ provider จริงถ้ามี key)
102
142
 
103
143
  second brain (Obsidian workspace สำหรับจัดเก็บงาน + ความจำ AI):
104
- sanook brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
144
+ ${BRAND.cliName} brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
145
+
146
+ search (BM25 + optional BYOK semantic เหนือ vault + memory + sessions + skills):
147
+ ${BRAND.cliName} index (re)index vault+memory แบบ incremental (O(delta))
148
+ ${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]
149
+ ${BRAND.cliName} mcp serve expose brain เป็น MCP server (stdio) ให้ Claude Desktop/Cursor
105
150
 
106
151
  config & mcp:
107
- sanook config [get|set <k> <v>] ดู/แก้ ~/.sanook/config.json (model/budgetUsd/permissionMode)
108
- sanook mcp [list|add <name> <cmd> …|remove <name>] จัดการ MCP servers
152
+ ${BRAND.cliName} config [get|set <k> <v>] ดู/แก้ ${appHomePath('config.json')} (model/budgetUsd/permissionMode/cacheTtl/compaction/thinking)
153
+ ${BRAND.cliName} mcp [list|add <name> <cmd> …|remove <name>] จัดการ MCP servers
154
+ ${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
109
155
 
110
156
  flags:
111
157
  -m, --model <spec> sonnet/opus/haiku/fable · gpt/codex · gemini · grok · deepseek · mistral · groq · ollama/lmstudio
112
158
  or "provider:model-id" (e.g. openai:gpt-5-codex, groq:fast, google:gemini-2.5-flash)
113
159
  -b, --budget <usd> stop when estimated cost exceeds this
114
- -c, --continue resume the latest session (จำว่าทำถึงไหน ทำต่อ)
160
+ -c, --continue resume the latest session ของ project นี้
161
+ --continue-any resume latest session ข้าม project (explicit)
115
162
  --plan plan mode — สำรวจ+วางแผนเท่านั้น ไม่แก้ไฟล์ (read-only)
116
163
  -y, --yes อนุมัติ tool อัตโนมัติ (ข้าม ask-mode permission)
117
164
  --json machine-readable JSONL output
@@ -119,7 +166,8 @@ flags:
119
166
  -h, --help
120
167
 
121
168
  env (BYOK — direct API key only):
122
- ANTHROPIC_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / OPENAI_API_KEY`;
169
+ ANTHROPIC_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / OPENAI_API_KEY
170
+ ${BRAND_ENV.disableUpdateCheck}=1 disable interactive update prompts`;
123
171
  /** sanook serve [--port N] [--model spec] — เปิด gateway (HTTP loopback + cron scheduler) อยู่ยาว */
124
172
  async function runServe(args) {
125
173
  const portIdx = args.indexOf('--port');
@@ -131,11 +179,12 @@ async function runServe(args) {
131
179
  const mIdx = args.findIndex((a) => a === '--model' || a === '-m');
132
180
  const config = await loadConfig({ model: mIdx !== -1 ? args[mIdx + 1] : undefined });
133
181
  const { startGateway } = await import('./gateway/serve.js');
134
- process.stdout.write(`${DIM}Sanook gateway — model: ${config.model}${RESET}\n`);
182
+ process.stdout.write(`${DIM}${BRAND.productName} gateway — model: ${config.model}${RESET}\n`);
135
183
  const stop = await startGateway({
136
184
  port,
137
185
  model: config.model,
138
186
  budgetUsd: config.budgetUsd,
187
+ permissionMode: envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : config.permissionMode,
139
188
  onLog: (m) => process.stdout.write(`${DIM}[gateway] ${m}${RESET}\n`),
140
189
  });
141
190
  const shutdown = () => {
@@ -190,7 +239,7 @@ async function runCron(args) {
190
239
  if (action === 'list' || action === undefined) {
191
240
  const tasks = await listTasks();
192
241
  if (!tasks.length) {
193
- console.log('ยังไม่มี task — เพิ่มด้วย: sanook cron add "every 1h" "เช็คข่าว AI"');
242
+ console.log(`ยังไม่มี task — เพิ่มด้วย: ${BRAND.cliName} cron add "every 1h" "เช็คข่าว AI"`);
194
243
  return;
195
244
  }
196
245
  for (const t of tasks) {
@@ -330,10 +379,10 @@ async function readStdin() {
330
379
  async function runConfig(args) {
331
380
  const { readGlobalConfigRaw, patchGlobalConfig } = await import('./config.js');
332
381
  const [action, key, ...rest] = args;
333
- const ALLOWED = ['model', 'budgetUsd', 'permissionMode', 'brainPath'];
382
+ const ALLOWED = ['model', 'fallbackModel', 'budgetUsd', 'maxSteps', 'permissionMode', 'brainPath', 'pricing', 'cacheTtl', 'compaction', 'thinking', 'summaryModel'];
334
383
  if (action === 'set') {
335
384
  if (!key || rest.length === 0) {
336
- console.error(`ใช้: sanook config set <key> <value> (key: ${ALLOWED.join(' | ')})`);
385
+ console.error(`ใช้: ${BRAND.cliName} config set <key> <value> (key: ${ALLOWED.join(' | ')})`);
337
386
  process.exit(1);
338
387
  }
339
388
  if (!ALLOWED.includes(key)) {
@@ -341,7 +390,60 @@ async function runConfig(args) {
341
390
  process.exit(1);
342
391
  }
343
392
  const raw = rest.join(' ');
344
- await patchGlobalConfig({ [key]: key === 'budgetUsd' ? Number(raw) : raw });
393
+ let value = raw;
394
+ if (key === 'budgetUsd') {
395
+ const n = Number(raw);
396
+ if (!Number.isFinite(n) || n <= 0) {
397
+ console.error('budgetUsd ต้องเป็นตัวเลขบวก เช่น 0.25');
398
+ process.exit(1);
399
+ }
400
+ value = n;
401
+ }
402
+ else if (key === 'maxSteps') {
403
+ const n = Number(raw);
404
+ if (!Number.isInteger(n) || n <= 0) {
405
+ console.error('maxSteps ต้องเป็น integer บวก เช่น 20');
406
+ process.exit(1);
407
+ }
408
+ value = n;
409
+ }
410
+ else if (key === 'permissionMode' && raw !== 'auto' && raw !== 'ask') {
411
+ console.error('permissionMode ต้องเป็น auto หรือ ask');
412
+ process.exit(1);
413
+ }
414
+ else if (key === 'cacheTtl' && raw !== '5m' && raw !== '1h') {
415
+ console.error('cacheTtl ต้องเป็น 5m หรือ 1h');
416
+ process.exit(1);
417
+ }
418
+ else if (key === 'compaction' && raw !== 'truncate' && raw !== 'summarize') {
419
+ console.error('compaction ต้องเป็น truncate หรือ summarize');
420
+ process.exit(1);
421
+ }
422
+ else if (key === 'thinking') {
423
+ // เก็บเป็น number (budget) หรือ boolean ให้ตรง ConfigSchema (ไม่เก็บ string)
424
+ if (raw === 'on' || raw === 'true')
425
+ value = true;
426
+ else if (raw === 'off' || raw === 'false')
427
+ value = false;
428
+ else {
429
+ const n = Number(raw);
430
+ if (!Number.isInteger(n) || n <= 0) {
431
+ console.error('thinking ต้องเป็น on/off หรือ budget tokens (integer บวก เช่น 4000)');
432
+ process.exit(1);
433
+ }
434
+ value = n;
435
+ }
436
+ }
437
+ else if (key === 'pricing') {
438
+ try {
439
+ value = parsePricingOverride(raw); // { "provider:model": { input, output, cacheRead?, cacheWrite? } }
440
+ }
441
+ catch (e) {
442
+ console.error(`pricing ต้องเป็น JSON เช่น '{"openai:gpt-5.5":{"input":1.25,"output":10}}' — ${e.message}`);
443
+ process.exit(1);
444
+ }
445
+ }
446
+ await patchGlobalConfig({ [key]: value });
345
447
  console.log(`ตั้ง ${key} = ${raw}`);
346
448
  return;
347
449
  }
@@ -350,11 +452,63 @@ async function runConfig(args) {
350
452
  console.log(cfg[key] ?? '(ไม่ได้ตั้ง)');
351
453
  return;
352
454
  }
353
- console.log(`~/.sanook/config.json:\n${JSON.stringify(await readGlobalConfigRaw(), null, 2)}`);
455
+ console.log(`${appHomePath('config.json')}:\n${JSON.stringify(await readGlobalConfigRaw(), null, 2)}`);
456
+ }
457
+ /** sanook index — incremental (re)index of vault + memory + sessions + skills */
458
+ async function runIndex(_args) {
459
+ const { reindex } = await import('./search/indexer.js');
460
+ console.log('indexing…');
461
+ const r = await reindex();
462
+ console.log(`done: +${r.added} ~${r.updated} -${r.removed} (skipped ${r.skipped}) · ` +
463
+ `memory=${r.memory} sessions=${r.sessions} skills=${r.skills}\nvault: ${r.vaultPath ?? '(not set — `' + BRAND.cliName + ' brain init` or set config.brainPath)'}`);
464
+ }
465
+ /** sanook search "<query>" [--mode ..] [--limit N] [--source a,b] — one-shot ranked search */
466
+ async function runSearch(args) {
467
+ const queryParts = [];
468
+ let mode = 'auto';
469
+ let limit = 8;
470
+ let sources;
471
+ for (let i = 0; i < args.length; i++) {
472
+ const a = args[i];
473
+ if (a === '--mode')
474
+ mode = args[++i] ?? 'auto';
475
+ else if (a === '--limit')
476
+ limit = Number.parseInt(args[++i] ?? '8', 10) || 8;
477
+ else if (a === '--source' || a === '--sources')
478
+ sources = (args[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
479
+ else
480
+ queryParts.push(a);
481
+ }
482
+ const query = queryParts.join(' ').trim();
483
+ if (!query) {
484
+ console.error(`ใช้: ${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]`);
485
+ process.exit(1);
486
+ }
487
+ const { search } = await import('./search/engine.js');
488
+ const res = await search(query, { mode: mode, limit, sources: sources });
489
+ if (res.degraded)
490
+ console.log(`${DIM}(mode=${res.mode}, degraded: ${res.degraded})${RESET}`);
491
+ else
492
+ console.log(`${DIM}(mode=${res.mode}, ${res.hits.length} hits)${RESET}`);
493
+ if (!res.hits.length) {
494
+ console.log(`ไม่เจอ "${query}" — ลองรัน ${BRAND.cliName} index ก่อน (ถ้ายังไม่เคย index vault)`);
495
+ return;
496
+ }
497
+ for (const h of res.hits) {
498
+ const title = h.title.trim();
499
+ const head = title ? `${title} — ${h.snippet}` : h.snippet;
500
+ const where = h.path ? ` ${DIM}(${h.path})${RESET}` : '';
501
+ console.log(`${DIM}[${h.source}]${RESET} ${head}${where}`);
502
+ }
503
+ }
504
+ /** sanook mcp serve — run the stdio MCP server exposing sanook's brain */
505
+ async function runMcpServe() {
506
+ const { runMcpServer } = await import('./mcp-server.js');
507
+ await runMcpServer();
354
508
  }
355
509
  /** sanook mcp [list | add <name> <command> [args...] | remove <name>] — จัดการ ~/.sanook/mcp.json */
356
510
  async function runMcp(args) {
357
- const mcpPath = join(homedir(), '.sanook', 'mcp.json');
511
+ const mcpPath = appHomePath('mcp.json');
358
512
  let cfg = { mcpServers: {} };
359
513
  try {
360
514
  const parsed = JSON.parse(await readFile(mcpPath, 'utf8'));
@@ -365,17 +519,24 @@ async function runMcp(args) {
365
519
  }
366
520
  const write = async () => {
367
521
  await mkdir(dirname(mcpPath), { recursive: true });
368
- await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`);
522
+ await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
523
+ await chmod(mcpPath, 0o600).catch(() => { });
369
524
  };
370
525
  const [action, name, command, ...cmdArgs] = args;
371
526
  if (action === 'add') {
372
527
  if (!name || !command) {
373
- console.error('ใช้: sanook mcp add <name> <command> [args...] (เช่น: mcp add fs npx -y @modelcontextprotocol/server-filesystem /path)');
528
+ console.error(`ใช้: ${BRAND.cliName} mcp add <name> <command> [args...] (เช่น: mcp add fs npx -y @modelcontextprotocol/server-filesystem /path)`);
529
+ console.error(` remote: ${BRAND.cliName} mcp add <name> https://host/mcp (Streamable-HTTP)`);
530
+ process.exit(1);
531
+ }
532
+ if (!isValidMcpServerName(name)) {
533
+ console.error('ชื่อ MCP server ต้องเป็น a-z/A-Z/0-9/_/- ความยาวไม่เกิน 64 และห้ามใช้ชื่อพิเศษ');
374
534
  process.exit(1);
375
535
  }
376
- cfg.mcpServers[name] = { command, args: cmdArgs };
536
+ // command เป็น http(s):// → remote MCP (Streamable-HTTP), ไม่งั้น stdio
537
+ cfg.mcpServers[name] = /^https?:\/\//.test(command) ? { url: command } : { command, args: cmdArgs };
377
538
  await write();
378
- console.log(`เพิ่ม MCP server "${name}"`);
539
+ console.log(`เพิ่ม MCP server "${name}"${/^https?:\/\//.test(command) ? ' (remote http)' : ''}`);
379
540
  return;
380
541
  }
381
542
  if (action === 'remove' || action === 'rm') {
@@ -390,23 +551,174 @@ async function runMcp(args) {
390
551
  }
391
552
  const names = Object.keys(cfg.mcpServers);
392
553
  if (!names.length) {
393
- console.log('ยังไม่มี MCP server — เพิ่ม: sanook mcp add <name> <command> [args...]');
554
+ console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp add <name> <command> [args...]`);
394
555
  return;
395
556
  }
396
557
  console.log(`${names.length} MCP servers:`);
397
- for (const n of names)
398
- console.log(` ${n} — ${cfg.mcpServers[n].command} ${(cfg.mcpServers[n].args ?? []).join(' ')}`);
558
+ for (const n of names) {
559
+ const s = cfg.mcpServers[n];
560
+ console.log(` ${n} — ${s.url ? `${s.url} (http)` : `${s.command} ${(s.args ?? []).join(' ')}`}`);
561
+ }
562
+ }
563
+ /** sanook trust [status|add|remove] — trust project .sanook content that can steer/execute code */
564
+ async function runTrust(args) {
565
+ const action = args[0] ?? 'status';
566
+ const { projectTrustStatus, trustProject, untrustProject } = await import('./trust.js');
567
+ if (action === 'status') {
568
+ const s = await projectTrustStatus();
569
+ console.log(`${s.trusted ? 'trusted' : 'untrusted'} — ${s.root}${s.reason === 'env' ? ' (env override)' : ''}`);
570
+ return;
571
+ }
572
+ if (action === 'add') {
573
+ const root = await trustProject();
574
+ console.log(`trusted project: ${root}`);
575
+ return;
576
+ }
577
+ if (action === 'remove' || action === 'rm') {
578
+ const root = await untrustProject();
579
+ console.log(`removed trust: ${root}`);
580
+ return;
581
+ }
582
+ console.error(`ไม่รู้จัก: trust ${action} — ใช้ status / add / remove`);
583
+ process.exit(1);
584
+ }
585
+ /** sanook update — one-command update path for globally installed CLI */
586
+ async function runUpdate(args) {
587
+ const checkOnly = args.includes('--check');
588
+ const unknown = args.filter((a) => a !== '--check');
589
+ if (unknown.length) {
590
+ console.error(`ใช้: ${BRAND.cliName} update [--check]`);
591
+ process.exit(1);
592
+ }
593
+ const { checkForUpdate, installLatest } = await import('./update.js');
594
+ try {
595
+ console.log(`เช็กอัปเดต ${PACKAGE_NAME}...`);
596
+ const check = await checkForUpdate({ name: PACKAGE_NAME, version: VERSION });
597
+ if (!check.isOutdated) {
598
+ console.log(`คุณใช้เวอร์ชันล่าสุดแล้ว (${check.currentVersion})`);
599
+ return;
600
+ }
601
+ console.log(`มีเวอร์ชันใหม่: ${check.currentVersion} → ${check.latestVersion}`);
602
+ console.log(`คำสั่งอัปเดต: ${check.installCommand}`);
603
+ if (checkOnly) {
604
+ console.log(`รัน "${BRAND.cliName} update" เพื่ออัปเดต`);
605
+ return;
606
+ }
607
+ const code = await installLatest({ name: PACKAGE_NAME, version: VERSION });
608
+ if (code !== 0) {
609
+ console.error(`อัปเดตไม่สำเร็จ (npm exit ${code}) — ลองรันเอง: ${check.installCommand}`);
610
+ process.exit(code);
611
+ }
612
+ console.log(`อัปเดตสำเร็จ — ตรวจสอบด้วย: ${BRAND.cliName} --version`);
613
+ }
614
+ catch (e) {
615
+ console.error(`เช็ก/อัปเดตไม่สำเร็จ: ${redactKey(e.message)}`);
616
+ console.error(`ลองรันเอง: npm install -g ${PACKAGE_NAME}@latest`);
617
+ process.exit(1);
618
+ }
619
+ }
620
+ const UPDATE_CACHE_PATH = appHomePath('update-check.json');
621
+ async function readUpdateCache() {
622
+ try {
623
+ const parsed = JSON.parse(await readFile(UPDATE_CACHE_PATH, 'utf8'));
624
+ return parsed && typeof parsed === 'object' ? parsed : {};
625
+ }
626
+ catch {
627
+ return {};
628
+ }
629
+ }
630
+ async function writeUpdateCache(latestVersion) {
631
+ await mkdir(dirname(UPDATE_CACHE_PATH), { recursive: true });
632
+ await writeFile(UPDATE_CACHE_PATH, `${JSON.stringify({ checkedAt: new Date().toISOString(), latestVersion }, null, 2)}\n`, { mode: 0o600 });
633
+ await chmod(UPDATE_CACHE_PATH, 0o600).catch(() => { });
634
+ }
635
+ async function askYesNo(question) {
636
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
637
+ try {
638
+ const answer = (await rl.question(question)).trim().toLowerCase();
639
+ return answer === '' || answer === 'y' || answer === 'yes';
640
+ }
641
+ finally {
642
+ rl.close();
643
+ }
644
+ }
645
+ async function maybePromptForInteractiveUpdate() {
646
+ if (envFlag(BRAND_ENV.disableUpdateCheck) || process.env.CI)
647
+ return;
648
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
649
+ return;
650
+ const { checkForUpdate, installLatest, shouldCheckForUpdate } = await import('./update.js');
651
+ const cache = await readUpdateCache();
652
+ if (!shouldCheckForUpdate(cache))
653
+ return;
654
+ try {
655
+ const check = await checkForUpdate({ name: PACKAGE_NAME, version: VERSION }, { timeoutMs: 2500 });
656
+ await writeUpdateCache(check.latestVersion).catch(() => { });
657
+ if (!check.isOutdated)
658
+ return;
659
+ process.stdout.write(`\nมี ${BRAND.productName} CLI เวอร์ชันใหม่: ${check.currentVersion} → ${check.latestVersion}\n` +
660
+ `อัปเดตตอนนี้ด้วย "${BRAND.cliName} update" ไหม? [Y/n] `);
661
+ const ok = await askYesNo('');
662
+ if (!ok) {
663
+ process.stdout.write(`ข้ามอัปเดตตอนนี้ — อัปเดตภายหลังได้ด้วย: ${BRAND.cliName} update\n\n`);
664
+ return;
665
+ }
666
+ const code = await installLatest({ name: PACKAGE_NAME, version: VERSION });
667
+ if (code !== 0) {
668
+ process.stdout.write(`อัปเดตไม่สำเร็จ (npm exit ${code}) — ลองรันเอง: ${check.installCommand}\n\n`);
669
+ return;
670
+ }
671
+ process.stdout.write(`อัปเดตสำเร็จ — เปิด ${BRAND.cliName} ใหม่เพื่อใช้เวอร์ชันล่าสุด\n\n`);
672
+ process.exit(0);
673
+ }
674
+ catch {
675
+ // update notifier ต้องไม่ block การเปิด TUI ถ้า offline/registry ล่ม/cache พัง
676
+ }
677
+ }
678
+ /** headless: model ต้อง key แต่ env ยังไม่มี → คืนข้อความแนะวิธีเริ่ม (null = พร้อมใช้) */
679
+ function headlessKeyHint(modelSpec) {
680
+ const { provider } = parseSpec(modelSpec);
681
+ const cfg = PROVIDERS[provider];
682
+ if (!cfg?.requiresKey || resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks))
683
+ return null;
684
+ const url = consoleUrl(provider);
685
+ const lines = [
686
+ `⚠ ยังไม่มี API key สำหรับ ${cfg.label} (${cfg.envVar})`,
687
+ `เริ่มใช้งาน:`,
688
+ ` • รัน "${BRAND.cliName}" (ไม่ใส่ task) → setup wizard ทีละขั้น (แนะนำ)`,
689
+ ` • หรือ: export ${cfg.envVar}="..."${url ? ` · เอา key ที่: ${url}` : ''}`,
690
+ ];
691
+ const other = detectEnvProvider();
692
+ if (other && other.provider !== provider) {
693
+ lines.push(` • เจอ key ของ ${other.label} อยู่แล้ว → ใช้เลย: ${BRAND.cliName} -m ${other.provider} "<task>"`);
694
+ }
695
+ return lines.join('\n');
399
696
  }
400
697
  async function main() {
698
+ // Node ≥ 22 required (uses node:fs glob, AbortSignal.timeout, ฯลฯ) — บอกชัดแทนปล่อย crash งงๆ
699
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
700
+ if (Number.isFinite(nodeMajor) && nodeMajor < 22) {
701
+ console.error(`${BRAND.productName} ต้องใช้ Node.js เวอร์ชัน 22 ขึ้นไป — ตอนนี้ใช้ ${process.version}\n` +
702
+ `อัปเดต Node ที่ https://nodejs.org (หรือ nvm/fnm/volta) แล้วลองใหม่`);
703
+ process.exit(1);
704
+ }
401
705
  const argv = process.argv.slice(2);
402
- if (argv.includes('-v') || argv.includes('--version')) {
706
+ if (argv.length === 1 && (argv[0] === '-v' || argv[0] === '--version')) {
403
707
  console.log(VERSION);
404
708
  return;
405
709
  }
406
- if (argv.includes('-h') || argv.includes('--help')) {
710
+ if (argv.length === 1 && (argv[0] === '-h' || argv[0] === '--help')) {
407
711
  console.log(HELP);
408
712
  return;
409
713
  }
714
+ if (argv[0] === 'update')
715
+ return runUpdate(argv.slice(1));
716
+ // doctor — ไม่ต้องโหลด key/mcp; ตรวจ Node/PATH/global-bin แล้วบอกวิธีแก้ "sanook ไม่เจอ"
717
+ if (argv[0] === 'doctor') {
718
+ const { runDoctor } = await import('./doctor.js');
719
+ console.log(await runDoctor(PACKAGE_NAME));
720
+ return;
721
+ }
410
722
  // โหลด API key จาก ~/.sanook/auth.json เข้า env (ไม่ override env ที่ตั้งไว้แล้ว)
411
723
  await loadKeysIntoEnv();
412
724
  process.on('exit', closeMcp); // ปิด MCP server (kill child) ตอนจบ
@@ -425,34 +737,70 @@ async function main() {
425
737
  return runBrain(argv.slice(1));
426
738
  if (argv[0] === 'config' && ['get', 'set', 'list', undefined].includes(argv[1]))
427
739
  return runConfig(argv.slice(1));
740
+ if (argv[0] === 'index' && (argv.length === 1 || argv[1].startsWith('--')))
741
+ return runIndex(argv.slice(1));
742
+ if (argv[0] === 'search' && argv.length > 1)
743
+ return runSearch(argv.slice(1));
744
+ if (argv[0] === 'mcp' && argv[1] === 'serve')
745
+ return runMcpServe();
428
746
  if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', undefined].includes(argv[1]))
429
747
  return runMcp(argv.slice(1));
430
- const { model, budget, json, prompt: argPrompt, planMode, yes } = parseArgs(argv);
748
+ if (argv[0] === 'trust' && ['status', 'add', 'remove', 'rm', undefined].includes(argv[1]))
749
+ return runTrust(argv.slice(1));
750
+ const { model, budget, json, quiet, prompt: argPrompt, planMode, yes } = parseArgs(argv);
431
751
  const budgetUsd = Number.isFinite(budget) ? budget : undefined;
432
752
  // stdin piping: `git diff | sanook "review this"` → ผนวก stdin เข้า prompt (headless/CI)
433
753
  const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
434
754
  const prompt = piped ? `${argPrompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : argPrompt;
435
755
  if (prompt) {
436
756
  const config = await loadConfig({ model, budgetUsd });
757
+ // headless + ยังไม่มี key → บอกวิธีเริ่มแบบ actionable แทนปล่อยให้ throw error ดิบ (กัน dead-end ของ flow ที่ README แนะนำ)
758
+ const noKey = headlessKeyHint(config.model);
759
+ if (noKey) {
760
+ process.stderr.write(`${noKey}\n`);
761
+ process.exit(1);
762
+ }
437
763
  // --continue / -c → โหลด session ล่าสุดมาต่อ (จำว่าทำถึงไหน)
438
- const history = argv.includes('--continue') || argv.includes('-c') ? (await latestSession())?.messages : undefined;
439
- await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode);
764
+ const wantsContinue = argv.includes('--continue') || argv.includes('-c') || argv.includes('--continue-any');
765
+ const history = wantsContinue
766
+ ? (await latestSession(argv.includes('--continue-any') ? null : process.cwd()))?.messages
767
+ : undefined;
768
+ await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode, quiet, config.fallbackModel);
440
769
  return;
441
770
  }
442
- // interactive — ครั้งแรก (ยังไม่มี config) → setup wizard ก่อนเข้า REPL
771
+ await maybePromptForInteractiveUpdate();
772
+ // interactive — ครั้งแรก (ยังไม่มี config): ถ้าไม่มี key ใช้ได้ใน env → ต้องโชว์ wizard
773
+ let needsSetup = false;
443
774
  if (await isFirstRun()) {
444
- const { startSetup } = await import('./ui/render.js');
445
- await startSetup();
775
+ // provider เป้าหมาย: เคารพ -m ที่ user ใส่ก่อน (กันขึ้น "พร้อมใช้" ผิด provider), ไม่งั้น scan env ตามนิยม
776
+ const flagProvider = model ? parseSpec(model).provider : undefined;
777
+ const target = flagProvider ?? detectEnvProvider()?.provider;
778
+ const tcfg = target ? PROVIDERS[target] : undefined;
779
+ if (target && tcfg && hasUsableEnvKey(target)) {
780
+ // มี key ใช้ได้จริง (ผ่าน policy ไม่ใช่ OAuth) → ข้าม wizard, ตั้ง default, บอกว่าพร้อมใช้
781
+ const { saveGlobalConfig } = await import('./config.js');
782
+ await saveGlobalConfig({ model: model ?? `${target}:${tcfg.models.default}`, provider: target });
783
+ console.log(`✅ ${tcfg.label} พร้อมใช้เลย (ข้าม setup wizard)\n`);
784
+ }
785
+ else {
786
+ needsSetup = true; // ไม่มี provider ที่ key ใช้ได้ (หรือ -m provider ไม่มี key) → wizard (รัน Ink เดียวกับ REPL)
787
+ }
446
788
  }
447
789
  const config = await loadConfig({ model, budgetUsd });
448
790
  // --continue / -c → โหลด conversation ล่าสุดเข้า REPL (เดิม resume ได้แค่ headless)
449
- const initialHistory = argv.includes('--continue') || argv.includes('-c') ? (await latestSession())?.messages : undefined;
450
- const { startRepl } = await import('./ui/render.js');
451
- startRepl({
452
- initialModel: config.model,
453
- budgetUsd: config.budgetUsd,
454
- permissionMode: yes ? 'auto' : config.permissionMode,
455
- initialHistory,
791
+ const initialHistory = argv.includes('--continue') || argv.includes('-c') || argv.includes('--continue-any')
792
+ ? (await latestSession(argv.includes('--continue-any') ? null : process.cwd()))?.messages
793
+ : undefined;
794
+ const { startApp } = await import('./ui/render.js');
795
+ startApp({
796
+ needsSetup,
797
+ appProps: {
798
+ initialModel: config.model,
799
+ fallbackModel: config.fallbackModel,
800
+ budgetUsd: config.budgetUsd,
801
+ permissionMode: yes ? 'auto' : config.permissionMode,
802
+ initialHistory,
803
+ },
456
804
  });
457
805
  }
458
806
  main().catch((err) => {