sanook-cli 0.5.1 → 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 (217) hide show
  1. package/.env.example +161 -3
  2. package/CHANGELOG.md +148 -10
  3. package/README.md +255 -26
  4. package/README.th.md +95 -7
  5. package/dist/approval.js +13 -0
  6. package/dist/bin.js +3552 -155
  7. package/dist/brain-consolidate.js +335 -0
  8. package/dist/brain-context.js +262 -0
  9. package/dist/brain-doctor.js +318 -0
  10. package/dist/brain-eval.js +186 -0
  11. package/dist/brain-final.js +377 -0
  12. package/dist/brain-metrics.js +277 -0
  13. package/dist/brain-new.js +402 -0
  14. package/dist/brain-pack.js +210 -0
  15. package/dist/brain-repair.js +280 -0
  16. package/dist/brain-review.js +382 -0
  17. package/dist/brain.js +15 -1
  18. package/dist/brand.js +1 -1
  19. package/dist/cli-args.js +190 -0
  20. package/dist/cli-option-values.js +16 -0
  21. package/dist/clipboard.js +65 -0
  22. package/dist/commands.js +266 -27
  23. package/dist/compaction.js +96 -11
  24. package/dist/config.js +149 -33
  25. package/dist/context-compression.js +191 -0
  26. package/dist/context-pack.js +145 -0
  27. package/dist/cost.js +49 -15
  28. package/dist/dashboard/api-helpers.js +87 -0
  29. package/dist/dashboard/server.js +179 -0
  30. package/dist/dashboard/static/app.js +277 -0
  31. package/dist/dashboard/static/index.html +39 -0
  32. package/dist/dashboard/static/styles.css +85 -0
  33. package/dist/diff.js +10 -2
  34. package/dist/first-run.js +21 -0
  35. package/dist/gateway/auth.js +49 -9
  36. package/dist/gateway/bluebubbles.js +205 -0
  37. package/dist/gateway/config.js +929 -0
  38. package/dist/gateway/deliver.js +399 -0
  39. package/dist/gateway/discord.js +124 -0
  40. package/dist/gateway/doctor.js +456 -0
  41. package/dist/gateway/email.js +501 -0
  42. package/dist/gateway/googlechat.js +207 -0
  43. package/dist/gateway/homeassistant.js +256 -0
  44. package/dist/gateway/ledger.js +38 -1
  45. package/dist/gateway/line.js +171 -0
  46. package/dist/gateway/lock.js +3 -1
  47. package/dist/gateway/matrix.js +366 -0
  48. package/dist/gateway/mattermost.js +322 -0
  49. package/dist/gateway/ntfy.js +218 -0
  50. package/dist/gateway/schedule.js +31 -4
  51. package/dist/gateway/serve.js +267 -7
  52. package/dist/gateway/server.js +253 -19
  53. package/dist/gateway/service.js +224 -0
  54. package/dist/gateway/session.js +362 -0
  55. package/dist/gateway/signal.js +351 -0
  56. package/dist/gateway/slack.js +124 -0
  57. package/dist/gateway/sms.js +169 -0
  58. package/dist/gateway/targets.js +576 -0
  59. package/dist/gateway/teams.js +106 -0
  60. package/dist/gateway/telegram.js +38 -15
  61. package/dist/gateway/webhooks.js +220 -0
  62. package/dist/gateway/whatsapp.js +230 -0
  63. package/dist/hooks.js +13 -2
  64. package/dist/hotkeys.js +21 -0
  65. package/dist/i18n/en.js +98 -0
  66. package/dist/i18n/index.js +19 -0
  67. package/dist/i18n/th.js +98 -0
  68. package/dist/i18n/types.js +1 -0
  69. package/dist/insights-args.js +55 -0
  70. package/dist/insights.js +86 -0
  71. package/dist/knowledge.js +55 -29
  72. package/dist/loop.js +157 -29
  73. package/dist/lsp/index.js +23 -5
  74. package/dist/mcp-hub.js +33 -0
  75. package/dist/mcp-registry.js +494 -0
  76. package/dist/mcp-risk.js +71 -0
  77. package/dist/mcp-server.js +1 -1
  78. package/dist/mcp.js +120 -10
  79. package/dist/memory-log.js +90 -0
  80. package/dist/memory-store.js +37 -1
  81. package/dist/memory.js +148 -37
  82. package/dist/model-picker.js +58 -0
  83. package/dist/orchestrate.js +51 -19
  84. package/dist/personality.js +58 -0
  85. package/dist/plan-handoff.js +17 -0
  86. package/dist/polyglot.js +162 -0
  87. package/dist/process-runner.js +96 -0
  88. package/dist/project-init.js +91 -0
  89. package/dist/project-registry.js +143 -0
  90. package/dist/project-scaffold.js +124 -0
  91. package/dist/prompt-size.js +155 -0
  92. package/dist/providers/codex-login.js +138 -0
  93. package/dist/providers/codex.js +89 -43
  94. package/dist/providers/keys.js +22 -1
  95. package/dist/providers/models.js +2 -2
  96. package/dist/providers/registry.js +14 -47
  97. package/dist/search/chunk.js +7 -8
  98. package/dist/search/cli.js +83 -0
  99. package/dist/search/embed-store.js +3 -0
  100. package/dist/search/embedding-config.js +22 -0
  101. package/dist/search/engine.js +2 -13
  102. package/dist/search/indexer.js +44 -1
  103. package/dist/search/store.js +23 -1
  104. package/dist/session-distill.js +84 -0
  105. package/dist/session.js +92 -16
  106. package/dist/skill-install.js +53 -13
  107. package/dist/skills.js +33 -0
  108. package/dist/slash-completion.js +155 -0
  109. package/dist/support-dump.js +206 -0
  110. package/dist/tool-catalog.js +59 -0
  111. package/dist/tools/edit.js +45 -15
  112. package/dist/tools/git.js +10 -5
  113. package/dist/tools/homeassistant.js +106 -0
  114. package/dist/tools/index.js +10 -0
  115. package/dist/tools/list.js +19 -6
  116. package/dist/tools/permission.js +992 -12
  117. package/dist/tools/polyglot.js +126 -0
  118. package/dist/tools/read.js +16 -4
  119. package/dist/tools/sandbox.js +38 -13
  120. package/dist/tools/schedule.js +19 -3
  121. package/dist/tools/search.js +226 -15
  122. package/dist/tools/task.js +40 -9
  123. package/dist/tools/timeout.js +23 -3
  124. package/dist/tools/web-fetch-tool.js +33 -0
  125. package/dist/trust.js +11 -1
  126. package/dist/turn-retrieval.js +83 -0
  127. package/dist/ui/app.js +878 -32
  128. package/dist/ui/banner.js +78 -4
  129. package/dist/ui/history.js +37 -5
  130. package/dist/ui/markdown.js +122 -0
  131. package/dist/ui/mentions.js +3 -2
  132. package/dist/ui/overlay.js +496 -0
  133. package/dist/ui/queue.js +23 -0
  134. package/dist/ui/render.js +20 -1
  135. package/dist/ui/session-panel.js +115 -0
  136. package/dist/ui/setup-providers.js +40 -0
  137. package/dist/ui/setup.js +172 -46
  138. package/dist/ui/status.js +142 -0
  139. package/dist/ui/thinking-panel.js +36 -0
  140. package/dist/ui/tool-trail.js +97 -0
  141. package/dist/ui/transcript.js +26 -0
  142. package/dist/ui/useBusyElapsed.js +19 -0
  143. package/dist/ui/useEditor.js +144 -5
  144. package/dist/ui/useGitBranch.js +57 -0
  145. package/dist/update.js +56 -17
  146. package/dist/web-fetch.js +637 -0
  147. package/dist/web-surface.js +190 -0
  148. package/dist/worktree.js +175 -4
  149. package/package.json +5 -5
  150. package/second-brain/AGENTS.md +6 -4
  151. package/second-brain/CLAUDE.md +7 -1
  152. package/second-brain/Evals/_Index.md +10 -2
  153. package/second-brain/Evals/quality-ledger.md +9 -1
  154. package/second-brain/Evals/second-brain-benchmarks.md +62 -0
  155. package/second-brain/GEMINI.md +5 -4
  156. package/second-brain/Home.md +1 -1
  157. package/second-brain/Projects/_Index.md +19 -4
  158. package/second-brain/Projects/sanook-cli/_Index.md +30 -0
  159. package/second-brain/Projects/sanook-cli/context.md +35 -0
  160. package/second-brain/Projects/sanook-cli/current-state.md +32 -0
  161. package/second-brain/Projects/sanook-cli/overview.md +41 -0
  162. package/second-brain/Projects/sanook-cli/repo.md +34 -0
  163. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
  164. package/second-brain/README.md +1 -1
  165. package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
  166. package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
  167. package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
  168. package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
  169. package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
  170. package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
  171. package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
  172. package/second-brain/Research/_Index.md +8 -1
  173. package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
  174. package/second-brain/Reviews/_Index.md +1 -1
  175. package/second-brain/Runbooks/_Index.md +6 -1
  176. package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
  177. package/second-brain/SANOOK.md +45 -0
  178. package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
  179. package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
  180. package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
  181. package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
  182. package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
  183. package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
  184. package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
  185. package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
  186. package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
  187. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
  188. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
  189. package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
  190. package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
  191. package/second-brain/Sessions/_Index.md +15 -1
  192. package/second-brain/Shared/AI-Context-Index.md +22 -0
  193. package/second-brain/Shared/Context-Packs/_Index.md +9 -1
  194. package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
  195. package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
  196. package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
  197. package/second-brain/Shared/Operating-State/current-state.md +14 -4
  198. package/second-brain/Shared/Scripts/_Index.md +3 -1
  199. package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
  200. package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
  201. package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
  202. package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
  203. package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
  204. package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
  205. package/second-brain/Shared/User-Memory/_Index.md +4 -1
  206. package/second-brain/Shared/User-Memory/response-examples.md +98 -0
  207. package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
  208. package/second-brain/Templates/_Index.md +9 -0
  209. package/second-brain/Templates/final-lite.md +111 -0
  210. package/second-brain/Templates/final.md +231 -0
  211. package/second-brain/Templates/project-workspace/_Index.md +31 -0
  212. package/second-brain/Templates/project-workspace/context.md +28 -0
  213. package/second-brain/Templates/project-workspace/current-state.md +29 -0
  214. package/second-brain/Templates/project-workspace/overview.md +39 -0
  215. package/second-brain/Templates/project-workspace/repo.md +33 -0
  216. package/second-brain/Vault Structure Map.md +2 -1
  217. package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/commands.js CHANGED
@@ -1,37 +1,71 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { PROVIDERS, parseSpec } from './providers/registry.js';
3
+ import { canonicalSpec, consoleUrl, hasUsableEnvKey, PROVIDERS, parseSpec } from './providers/registry.js';
4
4
  import { appHomePath, BRAND } from './brand.js';
5
5
  import { parseFrontmatter } from './skills.js';
6
6
  import { projectConfigPathIfTrusted } from './trust.js';
7
- const HELP_TEXT = `คำสั่ง:
7
+ import { normalizePersonalityName, personalityListText } from './personality.js';
8
+ import { parseInsightsArgs } from './insights-args.js';
9
+ import { formatHotkeys } from './hotkeys.js';
10
+ import { formatToolCatalog } from './tool-catalog.js';
11
+ export const HELP_TEXT = `คำสั่ง:
8
12
  /help แสดงคำสั่งทั้งหมด
9
- /model [spec] ดู/เปลี่ยน model (เช่น /model opus, /model openai:gpt-5)
13
+ /new, /reset เริ่มบทสนทนาใหม่
14
+ /status ดูสถานะ session ปัจจุบัน
15
+ /model [spec] ดู/เปลี่ยน model — /model เปิด picker 2 ขั้น (provider → model)
16
+ /setup ดูขั้นตอน setup wizard (model · agent · tools · gateway · brain)
17
+ /dashboard เปิด Sanook Dashboard (local web UI)
18
+ /personality [name]
19
+ ดู/ตั้ง personality overlay
20
+ /details [thinking|tools] [hidden|collapsed|expanded]
21
+ คุมแผง thinking/tool trail แบบ Hermes-style
22
+ /platforms ดู providers + messaging platforms ที่รองรับ
10
23
  /tools ดู tools ที่ agent ใช้ได้
11
- /skills ดูจำนวน skills (จัดการ: ${BRAND.cliName} skill list)
24
+ /mcp เปิด MCP Hub overlay
25
+ /skills เปิด Skills Hub overlay (จัดการ: ${BRAND.cliName} skill list)
26
+ /sessions เปิด Session Switcher overlay · /trail พับ/ขยาย tool trail
27
+ /tasks ดู background sub-agents (task_spawn)
12
28
  /diff ดู git diff (สิ่งที่ agent แก้ในรอบนี้)
29
+ /retry รัน prompt ล่าสุดอีกครั้ง
30
+ /stop หยุด turn ที่กำลังรัน
13
31
  /undo stash การแก้ไฟล์ล่าสุด (กู้คืนด้วย git stash pop)
14
32
  /rewind ย้อนกลับ 1 turn (คืนไฟล์ git + ตัดบทสนทนา, recoverable)
15
- /cost ดู token + cost รอบล่าสุด
33
+ /cost, /usage ดู token + cost รอบล่าสุด
34
+ /insights [--days N] [--all]
35
+ ดู usage/session insights ในเครื่อง
36
+ /hotkeys เปิด overlay คีย์ลัดใน REPL
37
+ /copy [last] copy คำตอบ assistant ล่าสุดไป clipboard/OSC52
16
38
  ↑/↓ ประวัติ · @ไฟล์ แนบ context/รูป · \\ ลงท้าย = บรรทัดใหม่
17
39
  /clear ล้าง conversation (เริ่มใหม่)
18
- /compact บีบ context (truncate · หรือ summarize ถ้าตั้ง compaction)
40
+ /compact, /compress
41
+ บีบ context (truncate · หรือ summarize ถ้าตั้ง compaction)
19
42
  /quit ออก
20
43
 
21
44
  นอก REPL (พิมพ์ใน shell):
22
- ${BRAND.cliName} search "<q>" · index · brain init · serve · mcp serve · config set <k> <v>
45
+ ${BRAND.cliName} search "<q>" · index · brain init · brain context · brain eval · brain review · brain final · serve · mcp serve · config set <k> <v>
23
46
  ดูทั้งหมด: ${BRAND.cliName} --help
24
47
 
25
48
  custom commands:
26
- ~/.sanook/commands/<name>.md และ .sanook/commands/<name>.md (project ต้อง trust ก่อน)`;
27
- const TOOLS_LIST = [
28
- 'read_file (offset/limit) write_file edit_file (replace_all) list_dir glob grep run_bash',
29
- 'git_status git_diff git_log git_commit',
30
- 'remember recall · skill find_skills create_skill',
31
- 'schedule_task list_scheduled cancel_scheduled',
32
- 'task task_parallel task_spawn task_collect task_cancel task_status ← sub-agent (ขนาน/background)',
33
- 'diagnostics ← type error/lint จาก language server (LSP)',
34
- ].join('\n ');
49
+ ~/.sanook/commands/<name>.md และ .sanook/commands/<name>.md (project ต้อง trust ก่อน)
50
+ args: ใช้ $ARGUMENTS หรือ {{ args }}; ถ้าไม่มี placeholder จะ append args ต่อท้าย`;
51
+ const MESSAGING_PLATFORMS = [
52
+ 'telegram',
53
+ 'discord',
54
+ 'slack',
55
+ 'mattermost',
56
+ 'homeassistant',
57
+ 'email',
58
+ 'line',
59
+ 'sms',
60
+ 'ntfy',
61
+ 'signal',
62
+ 'whatsapp',
63
+ 'matrix',
64
+ 'googlechat',
65
+ 'bluebubbles',
66
+ 'teams',
67
+ 'webhooks',
68
+ ];
35
69
  export function parseSlashInvocation(input) {
36
70
  const trimmed = input.trim();
37
71
  if (!trimmed.startsWith('/'))
@@ -39,7 +73,10 @@ export function parseSlashInvocation(input) {
39
73
  const match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
40
74
  if (!match)
41
75
  return null;
42
- return { name: match[1].toLowerCase(), args: match[2] ?? '' };
76
+ const name = match[1].toLowerCase();
77
+ if (name !== '?' && !isValidCommandName(name))
78
+ return null;
79
+ return { name, args: match[2] ?? '' };
43
80
  }
44
81
  /** /model (ไม่มี arg) — โชว์ model ปัจจุบัน + ตัวเลือกของ provider นั้น (alias จาก registry) */
45
82
  function modelMenu(current) {
@@ -60,37 +97,198 @@ function modelMenu(current) {
60
97
  .filter(Boolean)
61
98
  .join('\n');
62
99
  }
100
+ function missingKeyHint(provider) {
101
+ const cfg = PROVIDERS[provider];
102
+ if (!cfg?.requiresKey || hasUsableEnvKey(provider))
103
+ return undefined;
104
+ const url = consoleUrl(provider);
105
+ const lines = [
106
+ `⚠ ยังไม่มี API key ของ ${cfg.label} (${cfg.envVar}) — model นี้จะยังรันไม่ได้จนกว่าจะตั้ง key`,
107
+ url ? ` • เอา key ที่: ${url}` : undefined,
108
+ ` • ตั้ง: export ${cfg.envVar}="..." หรือรัน ${BRAND.cliName} เพื่อเข้า setup wizard`,
109
+ ].filter(Boolean);
110
+ if (provider === 'openai') {
111
+ lines.push(' • ถ้าต้องการใช้ ChatGPT plan ไม่ใช้ API key: /model codex แล้วรัน codex login');
112
+ }
113
+ return lines.join('\n');
114
+ }
115
+ function platformMenu() {
116
+ return [
117
+ `providers: ${Object.keys(PROVIDERS).join(' · ')}`,
118
+ `messaging: ${MESSAGING_PLATFORMS.join(' · ')}`,
119
+ `setup: ${BRAND.cliName} setup หรือ ${BRAND.cliName} gateway setup <platform>`,
120
+ ].join('\n');
121
+ }
122
+ function statusMenu(ctx) {
123
+ const { provider } = parseSpec(ctx.model);
124
+ const cfg = PROVIDERS[provider];
125
+ return [
126
+ `session: REPL`,
127
+ `model: ${ctx.model}`,
128
+ `provider: ${cfg?.label ?? provider}`,
129
+ `usage: ${ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)'}`,
130
+ `platforms: พิมพ์ /platforms`,
131
+ `system status: ${BRAND.cliName} status`,
132
+ ].join('\n');
133
+ }
134
+ function modelChange(spec) {
135
+ const canonical = canonicalSpec(spec);
136
+ const { provider, model } = parseSpec(canonical);
137
+ if (!PROVIDERS[provider]) {
138
+ return {
139
+ handled: true,
140
+ message: `provider ไม่รองรับ: "${provider}" — รองรับ: ${Object.keys(PROVIDERS).join(' · ')}`,
141
+ };
142
+ }
143
+ if (!model) {
144
+ return {
145
+ handled: true,
146
+ message: `model spec ไม่ครบ: "${spec}" — ใช้ /model <alias> หรือ /model <provider:model>`,
147
+ };
148
+ }
149
+ const hint = missingKeyHint(provider);
150
+ return {
151
+ handled: true,
152
+ modelChange: canonical,
153
+ message: [`เปลี่ยน model → ${canonical}`, hint].filter(Boolean).join('\n'),
154
+ };
155
+ }
63
156
  /** parse input — ถ้าขึ้นต้น / = slash command, ไม่งั้น handled=false (ส่งเข้า agent) */
64
157
  export function parseCommand(input, ctx) {
65
158
  const trimmed = input.trim();
66
159
  if (!trimmed.startsWith('/'))
67
160
  return { handled: false };
68
- const [cmd, ...args] = trimmed.slice(1).split(/\s+/);
161
+ const [rawCmd, ...args] = trimmed.slice(1).split(/\s+/);
162
+ const cmd = rawCmd.toLowerCase();
69
163
  switch (cmd) {
70
164
  case 'help':
71
165
  case '?':
72
166
  return { handled: true, action: 'help', message: HELP_TEXT };
73
167
  case 'clear':
168
+ case 'new':
169
+ case 'reset':
74
170
  return { handled: true, action: 'clear', message: 'ล้าง conversation แล้ว' };
171
+ case 'status':
172
+ return { handled: true, message: statusMenu(ctx) };
173
+ case 'hotkeys':
174
+ return { handled: true, action: 'hotkeys', message: formatHotkeys() };
75
175
  case 'compact':
176
+ case 'compress':
76
177
  return { handled: true, action: 'compact', message: 'บีบ context แล้ว' };
178
+ case 'copy': {
179
+ const target = args[0]?.toLowerCase();
180
+ if (!target || target === 'last' || target === 'assistant')
181
+ return { handled: true, action: 'copyLast' };
182
+ return { handled: true, message: 'ใช้ /copy หรือ /copy last' };
183
+ }
77
184
  case 'quit':
78
185
  case 'exit':
79
186
  return { handled: true, action: 'quit' };
80
187
  case 'model':
81
188
  if (!args[0])
82
- return { handled: true, message: modelMenu(ctx.model) };
83
- return { handled: true, modelChange: args[0], message: `เปลี่ยน model → ${args[0]}` };
189
+ return { handled: true, action: 'modelPicker', message: modelMenu(ctx.model) };
190
+ return modelChange(args[0]);
191
+ case 'setup':
192
+ return {
193
+ handled: true,
194
+ message: [
195
+ `${BRAND.productName} setup (Hermes-style sections):`,
196
+ ` 1. ${BRAND.cliName} setup model — provider + model wizard`,
197
+ ` 2. ${BRAND.cliName} setup agent — permissionMode, budget, personality`,
198
+ ` 3. ${BRAND.cliName} setup tools — built-in tools + MCP`,
199
+ ` 4. ${BRAND.cliName} setup gateway — Telegram/Discord/Slack/…`,
200
+ ` 5. ${BRAND.cliName} setup brain — second-brain vault`,
201
+ ` หรือรัน ${BRAND.cliName} ครั้งแรก → wizard 10 ขั้น (ภาษา → … → gateway → brain)`,
202
+ ].join('\n'),
203
+ };
204
+ case 'dashboard':
205
+ return {
206
+ handled: true,
207
+ message: `Sanook Dashboard — รัน: ${BRAND.cliName} dashboard\n แล้วเปิด http://127.0.0.1:9119 (Chat · Files · Logs · Cron · Channels)`,
208
+ };
209
+ case 'personality': {
210
+ const raw = args.join(' ').trim();
211
+ if (!raw)
212
+ return { handled: true, message: personalityListText() };
213
+ const name = normalizePersonalityName(raw);
214
+ if (!name)
215
+ return { handled: true, message: `ไม่รู้จัก personality: ${raw}\n\n${personalityListText()}` };
216
+ return {
217
+ handled: true,
218
+ action: 'personality',
219
+ personalityChange: name === 'none' ? '' : name,
220
+ message: name === 'none' ? 'ปิด personality overlay แล้ว' : `ตั้ง personality → ${name}`,
221
+ };
222
+ }
84
223
  case 'tools':
85
- return { handled: true, message: `tools ที่ agent ใช้ได้ (+ MCP ที่ตั้งไว้):\n ${TOOLS_LIST}` };
224
+ return { handled: true, action: 'toolsHub', message: `tools ที่ agent ใช้ได้ (+ MCP ที่ตั้งไว้):\n ${formatToolCatalog()}` };
225
+ case 'trail': {
226
+ const rawMode = args[0]?.toLowerCase();
227
+ if (!rawMode)
228
+ return { handled: true, action: 'toolTrail', message: 'toggle tool trail view' };
229
+ if (['compact', 'collapse', 'collapsed', 'hide', 'summary'].includes(rawMode)) {
230
+ return { handled: true, action: 'toolTrail', message: 'tool trail → compact', toolTrailMode: 'compact' };
231
+ }
232
+ if (['expanded', 'expand', 'full', 'show'].includes(rawMode)) {
233
+ return { handled: true, action: 'toolTrail', message: 'tool trail → expanded', toolTrailMode: 'expanded' };
234
+ }
235
+ return { handled: true, message: 'ใช้ /trail, /trail compact, หรือ /trail expanded' };
236
+ }
237
+ case 'details': {
238
+ const section = args[0]?.toLowerCase();
239
+ const mode = args[1]?.toLowerCase();
240
+ const usage = 'ใช้ /details thinking|tools hidden|collapsed|expanded';
241
+ if (!section && !mode)
242
+ return { handled: true, message: usage };
243
+ if (section !== 'thinking' && section !== 'tools')
244
+ return { handled: true, message: usage };
245
+ if (mode !== 'hidden' && mode !== 'collapsed' && mode !== 'expanded')
246
+ return { handled: true, message: usage };
247
+ return {
248
+ handled: true,
249
+ action: 'details',
250
+ detailMode: mode,
251
+ detailSection: section,
252
+ message: `details ${section} → ${mode}`,
253
+ };
254
+ }
255
+ case 'platforms':
256
+ return { handled: true, message: platformMenu() };
257
+ case 'mcp':
258
+ return {
259
+ handled: true,
260
+ action: 'mcpHub',
261
+ message: `MCP servers — จัดการด้วย "${BRAND.cliName} mcp list/search/install/doctor"`,
262
+ };
86
263
  case 'skills':
87
- return { handled: true, message: `skills โหลดจาก built-in + ${appHomePath('skills')} — จัดการด้วย "${BRAND.cliName} skill list/add/remove"` };
264
+ return {
265
+ handled: true,
266
+ action: 'skillsHub',
267
+ message: `skills โหลดจาก built-in + ${appHomePath('skills')} — จัดการด้วย "${BRAND.cliName} skill list/add/remove"`,
268
+ };
269
+ case 'sessions':
270
+ return { handled: true, action: 'sessionsHub', message: `saved sessions — จัดการด้วย "${BRAND.cliName} sessions"` };
271
+ case 'tasks':
272
+ return { handled: true, action: 'tasksHub', message: 'background tasks — จาก task_spawn (Enter ดูรายละเอียด)' };
88
273
  case 'diff':
89
274
  return { handled: true, action: 'diff' };
275
+ case 'retry':
276
+ return { handled: true, action: 'retry' };
277
+ case 'stop':
278
+ return { handled: true, action: 'stop', message: 'ไม่มี turn ที่กำลังทำงาน' };
90
279
  case 'undo':
91
280
  return { handled: true, action: 'undo' };
281
+ case 'rewind':
282
+ return { handled: true, action: 'rewind' };
92
283
  case 'cost':
284
+ case 'usage':
93
285
  return { handled: true, message: ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)' };
286
+ case 'insights': {
287
+ const parsed = parseInsightsArgs(args);
288
+ if (parsed === null)
289
+ return { handled: true, message: 'ใช้: /insights [--days N] [--all] (N ต้องเป็นจำนวนวันบวก)' };
290
+ return { handled: true, action: 'insights', insightsDays: parsed.days, insightsAll: parsed.all };
291
+ }
94
292
  default:
95
293
  return { handled: true, message: `ไม่รู้จักคำสั่ง /${cmd} — พิมพ์ /help` };
96
294
  }
@@ -99,11 +297,51 @@ export function parseCommand(input, ctx) {
99
297
  // ไฟล์ markdown (frontmatter optional) = prompt template ที่ส่งเข้า agent. $ARGUMENTS = ส่วนหลังชื่อคำสั่ง
100
298
  // (เลียน Claude Code .claude/commands) — global ~/.sanook/commands + project .sanook/commands (project ทับ)
101
299
  export const BUILTIN_COMMANDS = new Set([
102
- 'help', '?', 'clear', 'compact', 'quit', 'exit', 'model', 'tools', 'skills', 'diff', 'undo', 'rewind', 'cost',
300
+ 'help',
301
+ '?',
302
+ 'clear',
303
+ 'new',
304
+ 'reset',
305
+ 'status',
306
+ 'hotkeys',
307
+ 'compact',
308
+ 'compress',
309
+ 'copy',
310
+ 'quit',
311
+ 'exit',
312
+ 'model',
313
+ 'personality',
314
+ 'details',
315
+ 'platforms',
316
+ 'trail',
317
+ 'tools',
318
+ 'mcp',
319
+ 'skills',
320
+ 'sessions',
321
+ 'tasks',
322
+ 'diff',
323
+ 'retry',
324
+ 'stop',
325
+ 'undo',
326
+ 'rewind',
327
+ 'cost',
328
+ 'usage',
329
+ 'insights',
103
330
  ]);
104
331
  function isValidCommandName(name) {
105
332
  return /^[a-z0-9][a-z0-9-]{0,40}$/.test(name);
106
333
  }
334
+ function compareCommandFiles(a, b) {
335
+ const an = a.toLowerCase();
336
+ const bn = b.toLowerCase();
337
+ if (an !== bn)
338
+ return an.localeCompare(bn);
339
+ if (a === an && b !== bn)
340
+ return 1;
341
+ if (a !== an && b === bn)
342
+ return -1;
343
+ return a.localeCompare(b);
344
+ }
107
345
  /** scan custom commands จาก global + project (project override). ข้าม built-in ชื่อซ้ำ */
108
346
  export async function loadCustomCommands(cwd = process.cwd()) {
109
347
  const out = new Map();
@@ -119,10 +357,11 @@ export async function loadCustomCommands(cwd = process.cwd()) {
119
357
  catch {
120
358
  continue; // ไม่มีโฟลเดอร์ = ข้าม
121
359
  }
122
- for (const f of files) {
123
- if (!f.endsWith('.md'))
360
+ for (const f of files.sort(compareCommandFiles)) {
361
+ const normalizedFile = f.toLowerCase();
362
+ if (!normalizedFile.endsWith('.md'))
124
363
  continue;
125
- const name = f.slice(0, -3).toLowerCase();
364
+ const name = normalizedFile.slice(0, -3);
126
365
  if (!isValidCommandName(name) || BUILTIN_COMMANDS.has(name))
127
366
  continue;
128
367
  try {
@@ -140,7 +379,7 @@ export async function loadCustomCommands(cwd = process.cwd()) {
140
379
  export function expandCustomCommand(cmd, args) {
141
380
  const a = args.trim();
142
381
  if (/\$ARGUMENTS|\{\{\s*args\s*\}\}/.test(cmd.body)) {
143
- return cmd.body.replace(/\$ARGUMENTS|\{\{\s*args\s*\}\}/g, a);
382
+ return cmd.body.replace(/\$ARGUMENTS|\{\{\s*args\s*\}\}/g, () => a);
144
383
  }
145
384
  return a ? `${cmd.body}\n\n${a}` : cmd.body;
146
385
  }
@@ -1,6 +1,42 @@
1
+ import { selectiveCompressText } from './context-compression.js';
1
2
  const TRUNC_HEAD = 400;
2
3
  const TRUNC_TAIL = 600;
3
4
  const CHARS_PER_TOKEN = 4; // ประมาณคร่าวๆ (จริง ~3.5-4 ต่อ token)
5
+ const SELECTIVE_TOOL_TARGET_CHARS = 6_000;
6
+ const SELECTIVE_TOOL_MIN_CHARS = 8_000;
7
+ function textFromMessageContent(content) {
8
+ if (typeof content === 'string')
9
+ return content;
10
+ if (!Array.isArray(content))
11
+ return '';
12
+ return content
13
+ .map((part) => {
14
+ if (typeof part === 'object' && part && 'type' in part && part.type === 'text' && 'text' in part && typeof part.text === 'string')
15
+ return part.text;
16
+ return '';
17
+ })
18
+ .filter(Boolean)
19
+ .join('\n');
20
+ }
21
+ function latestUserText(messages) {
22
+ for (let i = messages.length - 1; i >= 0; i--) {
23
+ if (messages[i].role !== 'user')
24
+ continue;
25
+ const text = textFromMessageContent(messages[i].content).trim();
26
+ if (text)
27
+ return text;
28
+ }
29
+ return undefined;
30
+ }
31
+ function adaptiveStaleTarget(baseTarget, rank, count) {
32
+ if (count <= 1)
33
+ return baseTarget;
34
+ const recency = rank / Math.max(1, count - 1); // 0 = oldest, 1 = newest stale
35
+ return Math.max(1_500, Math.floor(baseTarget * (0.35 + 0.65 * recency)));
36
+ }
37
+ function adaptiveMinChars(targetChars, baseMinChars) {
38
+ return Math.min(baseMinChars, Math.max(targetChars + 1_000, Math.floor(targetChars * 1.45)));
39
+ }
4
40
  /** ตัดข้อความยาว เก็บหัว (intent) + ท้าย (error/result) */
5
41
  export function truncateText(s) {
6
42
  if (s.length <= TRUNC_HEAD + TRUNC_TAIL + 40)
@@ -16,23 +52,72 @@ export function truncateText(s) {
16
52
  */
17
53
  export function pruneToolResults(messages, keepTail = 4) {
18
54
  const cut = Math.max(0, messages.length - keepTail);
19
- return messages.map((m, i) => {
55
+ let changed = false;
56
+ const out = messages.map((m, i) => {
20
57
  if (i >= cut)
21
58
  return m;
22
59
  if (m.role !== 'tool' || !Array.isArray(m.content))
23
60
  return m;
24
- return {
25
- ...m,
26
- content: m.content.map((part) => {
27
- if (part.type === 'tool-result' &&
28
- part.output?.type === 'text' &&
29
- typeof part.output.value === 'string') {
30
- return { ...part, output: { ...part.output, value: truncateText(part.output.value) } };
61
+ const content = m.content.map((part) => {
62
+ if (part.type === 'tool-result' &&
63
+ part.output?.type === 'text' &&
64
+ typeof part.output.value === 'string') {
65
+ const compressed = selectiveCompressText(part.output.value, {
66
+ targetChars: SELECTIVE_TOOL_TARGET_CHARS,
67
+ minChars: SELECTIVE_TOOL_MIN_CHARS,
68
+ });
69
+ if (compressed.changed) {
70
+ changed = true;
71
+ return { ...part, output: { ...part.output, value: compressed.text } };
72
+ }
73
+ const truncated = truncateText(part.output.value);
74
+ if (truncated !== part.output.value) {
75
+ changed = true;
76
+ return { ...part, output: { ...part.output, value: truncated } };
77
+ }
78
+ }
79
+ return part;
80
+ });
81
+ return content === m.content ? m : { ...m, content };
82
+ });
83
+ return changed ? out : messages;
84
+ }
85
+ /**
86
+ * Per-step token optimizer (zero LLM cost).
87
+ * Compresses stale, very large tool results before each model request while keeping the latest tail full.
88
+ */
89
+ export function selectivelyCompressStaleToolResults(messages, keepTail = 6, targetChars = SELECTIVE_TOOL_TARGET_CHARS, minChars = SELECTIVE_TOOL_MIN_CHARS, query = latestUserText(messages)) {
90
+ const cut = Math.max(0, messages.length - keepTail);
91
+ const staleToolIndexes = messages
92
+ .map((m, i) => ({ m, i }))
93
+ .filter(({ m, i }) => i < cut && m.role === 'tool' && Array.isArray(m.content))
94
+ .map(({ i }) => i);
95
+ const rankByIndex = new Map(staleToolIndexes.map((index, rank) => [index, rank]));
96
+ let changed = false;
97
+ const out = messages.map((m, i) => {
98
+ if (i >= cut)
99
+ return m;
100
+ if (m.role !== 'tool' || !Array.isArray(m.content))
101
+ return m;
102
+ let messageChanged = false;
103
+ const adaptiveTarget = adaptiveStaleTarget(targetChars, rankByIndex.get(i) ?? 0, staleToolIndexes.length);
104
+ const adaptiveMin = adaptiveMinChars(adaptiveTarget, minChars);
105
+ const content = m.content.map((part) => {
106
+ if (part.type === 'tool-result' &&
107
+ part.output?.type === 'text' &&
108
+ typeof part.output.value === 'string') {
109
+ const compressed = selectiveCompressText(part.output.value, { targetChars: adaptiveTarget, minChars: adaptiveMin, query });
110
+ if (compressed.changed) {
111
+ changed = true;
112
+ messageChanged = true;
113
+ return { ...part, output: { ...part.output, value: compressed.text } };
31
114
  }
32
- return part;
33
- }),
34
- };
115
+ }
116
+ return part;
117
+ });
118
+ return messageChanged ? { ...m, content } : m;
35
119
  });
120
+ return changed ? out : messages;
36
121
  }
37
122
  /** ประมาณ token ของ conversation (chars/4) — ไม่เป๊ะแต่พอใช้ตัดสิน compact */
38
123
  export function estimateTokens(messages) {