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
@@ -0,0 +1,98 @@
1
+ export const en = {
2
+ setup: {
3
+ title: 'Set up Sanook AI CLI (first run)',
4
+ stepLanguage: '1. Choose language',
5
+ stepWelcome: '2. Welcome',
6
+ stepProvider: '3. Choose AI provider',
7
+ stepCodex: '4. Connect OpenAI Codex',
8
+ stepKey: '4. Paste API key',
9
+ stepModel: '5. Choose default model',
10
+ stepAgent: '6. Agent settings',
11
+ stepTools: '7. Tools & MCP',
12
+ stepGateway: '8. Messaging gateway',
13
+ stepBrain: '9. Second brain workspace',
14
+ stepComplete: '10. Ready',
15
+ languageHint: 'You can change this later with sanook config set locale en|th',
16
+ languageEn: 'English',
17
+ languageTh: 'Thai (ภาษาไทย)',
18
+ welcomeBody: 'Sanook is a terminal AI agent with MCP, gateway, and an optional Obsidian second brain.\nThis wizard walks you through provider, model, and vault setup step by step.',
19
+ welcomeContinue: 'Continue setup',
20
+ providerHint: '↑↓ select · Enter confirm · ★ Codex = ChatGPT plan (no API key)',
21
+ providerMenuHint: 'cloud = API key · ★ Codex = ChatGPT plan · local = free on device',
22
+ codexTitle: 'Connect OpenAI Codex (ChatGPT plan quota — no API key)',
23
+ codexChecking: 'Checking codex CLI + login state…',
24
+ codexNeedInstall: 'Codex CLI not installed yet',
25
+ codexNeedLogin: 'Codex CLI installed but ChatGPT not signed in',
26
+ codexLoggedInNeedCli: 'Signed in — install codex CLI to run the agent',
27
+ codexReady: 'ChatGPT signed in — continuing…',
28
+ codexDeviceTitle: 'Sign in with device code (Hermes-style)',
29
+ codexDeviceOpen: '1. Open in browser:',
30
+ codexDeviceEnter: '2. Enter this code:',
31
+ codexDeviceWaiting: '3. Waiting for sign-in…',
32
+ codexDeviceRetry: 'Try device code again',
33
+ codexDeviceBack: '← Back to other login options',
34
+ codexOptionDevice: 'Login with device code (recommended)',
35
+ codexOptionCliLogin: 'Use codex login in another terminal',
36
+ codexOptionRecheck: 'Re-check (after install/login)',
37
+ codexOptionBack: '← Choose another provider',
38
+ codexInstallCmd: 'npm i -g @openai/codex',
39
+ keyEscHint: '(Esc = back)',
40
+ keyOpenAiCodexHint: 'Have ChatGPT Plus/Pro? Press Esc and pick OpenAI Codex (ChatGPT plan) — no API key needed.',
41
+ keyFormatHint: 'Key format',
42
+ keyStorageHint: 'Direct console API key only — no OAuth tokens · stored at ~/.sanook/auth.json mode 0600',
43
+ keyEmptyError: 'Paste an API key first (Enter on empty is blocked) · Esc = back to providers',
44
+ modelLoading: 'Fetching models from',
45
+ modelPick: 'Pick a default model',
46
+ brainQuestion: 'Create a second-brain workspace (Obsidian) for durable AI memory?',
47
+ brainYes: 'Yes — a few questions (name + path)',
48
+ brainNo: 'Skip for now (run sanook brain init later)',
49
+ completeTitle: 'Setup complete',
50
+ completeBody: 'Your CLI is ready. Open the dashboard for config and sessions, or start chatting in the terminal.',
51
+ completeDashboard: 'Open Sanook Dashboard',
52
+ completeRepl: 'Start terminal REPL',
53
+ continueLabel: 'Continue',
54
+ backLabel: 'Back',
55
+ recheckLabel: 'Re-check',
56
+ agentTitle: 'How should Sanook handle write/bash tools?',
57
+ agentAsk: 'Ask before risky actions (recommended)',
58
+ agentAuto: 'Act first (auto mode — faster, less safe)',
59
+ agentHint: 'Change anytime: sanook config set permissionMode ask|auto',
60
+ toolsTitle: 'Built-in tools + MCP',
61
+ toolsBody: 'Sanook ships git, bash, MCP, web search hooks, and skills. MCP servers live in ~/.sanook/mcp.json.',
62
+ toolsMcpHint: 'Browse MCP: /mcp in REPL · sanook mcp search <query>',
63
+ toolsWebSkip: 'Continue (configure web search later)',
64
+ toolsWebLater: 'Note: run sanook web setup tavily for web search',
65
+ gatewayTitle: 'Connect messaging platforms (optional)',
66
+ gatewayBody: 'Run sanook serve for 24/7 gateway. Pick a platform to configure next, or skip.',
67
+ gatewaySkip: 'Skip gateway for now',
68
+ gatewayTelegram: 'Telegram — sanook gateway setup telegram',
69
+ gatewayDiscord: 'Discord — sanook gateway setup discord',
70
+ gatewaySlack: 'Slack — sanook gateway setup slack',
71
+ gatewayDashboard: 'Configure in Sanook Dashboard → Channels',
72
+ },
73
+ dashboard: {
74
+ productName: 'Sanook Dashboard',
75
+ tagline: 'Configure models, sessions, MCP, gateway, and your second brain',
76
+ nav: {
77
+ home: 'Home',
78
+ chat: 'Chat',
79
+ models: 'Models',
80
+ sessions: 'Sessions',
81
+ files: 'Files',
82
+ logs: 'Logs',
83
+ cron: 'Cron',
84
+ channels: 'Channels',
85
+ config: 'Config',
86
+ mcp: 'MCP',
87
+ brain: 'Brain',
88
+ },
89
+ home: {
90
+ title: 'System status',
91
+ cliVersion: 'CLI version',
92
+ model: 'Default model',
93
+ brainPath: 'Second brain',
94
+ gateway: 'Gateway',
95
+ openRepl: 'Run sanook in your terminal to chat',
96
+ },
97
+ },
98
+ };
@@ -0,0 +1,19 @@
1
+ import { en } from './en.js';
2
+ import { th } from './th.js';
3
+ const CATALOGS = { en, th };
4
+ export const SUPPORTED_LOCALES = ['en', 'th'];
5
+ export function normalizeLocale(raw) {
6
+ const v = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
7
+ if (v === 'en' || v.startsWith('en-'))
8
+ return 'en';
9
+ if (v === 'th' || v.startsWith('th-'))
10
+ return 'th';
11
+ return 'th';
12
+ }
13
+ export function getLocaleCatalog(locale) {
14
+ return CATALOGS[locale] ?? CATALOGS.th;
15
+ }
16
+ export function detectDefaultLocale() {
17
+ const lang = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? '';
18
+ return lang.toLowerCase().includes('th') ? 'th' : 'en';
19
+ }
@@ -0,0 +1,98 @@
1
+ export const th = {
2
+ setup: {
3
+ title: 'ตั้งค่า Sanook AI CLI (ครั้งแรก)',
4
+ stepLanguage: '1. เลือกภาษา',
5
+ stepWelcome: '2. ยินดีต้อนรับ',
6
+ stepProvider: '3. เลือก AI provider',
7
+ stepCodex: '4. เชื่อม OpenAI Codex',
8
+ stepKey: '4. วาง API key',
9
+ stepModel: '5. เลือก model เริ่มต้น',
10
+ stepAgent: '6. ตั้งค่า agent',
11
+ stepTools: '7. Tools & MCP',
12
+ stepGateway: '8. Messaging gateway',
13
+ stepBrain: '9. second brain workspace',
14
+ stepComplete: '10. พร้อมใช้งาน',
15
+ languageHint: 'เปลี่ยนภาษาทีหลังได้: sanook config set locale en|th',
16
+ languageEn: 'English (ภาษาอังกฤษ)',
17
+ languageTh: 'ภาษาไทย',
18
+ welcomeBody: 'Sanook คือ AI agent บน terminal พร้อม MCP, gateway และ second brain (Obsidian) แบบเลือกได้\nwizard นี้จะพาตั้งค่า provider → model → vault ทีละขั้น',
19
+ welcomeContinue: 'เริ่มตั้งค่า',
20
+ providerHint: '↑↓ เลือก · Enter ยืนยัน · ★ Codex = ChatGPT plan (ไม่ต้อง API key)',
21
+ providerMenuHint: 'cloud = ใส่ API key · ★ Codex = ChatGPT plan · local = ฟรีบนเครื่อง',
22
+ codexTitle: 'เชื่อม OpenAI Codex (ใช้โควต้า ChatGPT plan — ไม่ต้องมี API key)',
23
+ codexChecking: 'กำลังเช็ก codex CLI + สถานะ login…',
24
+ codexNeedInstall: 'ยังไม่ได้ติดตั้ง codex CLI',
25
+ codexNeedLogin: 'ติดตั้ง codex CLI แล้ว แต่ยังไม่ได้ login ChatGPT',
26
+ codexLoggedInNeedCli: 'login แล้ว — ติดตั้ง codex CLI ก่อนรัน agent',
27
+ codexReady: 'login ChatGPT แล้ว — กำลังไปต่อ…',
28
+ codexDeviceTitle: 'Login ด้วย device code (แบบ Hermes)',
29
+ codexDeviceOpen: '1. เปิดใน browser:',
30
+ codexDeviceEnter: '2. ใส่รหัสนี้:',
31
+ codexDeviceWaiting: '3. รอ sign-in…',
32
+ codexDeviceRetry: 'ลอง device code ใหม่',
33
+ codexDeviceBack: '← กลับไปเลือกวิธี login อื่น',
34
+ codexOptionDevice: 'Login ด้วย device code (แนะนำ)',
35
+ codexOptionCliLogin: 'ใช้ codex login ใน terminal อีกหน้าต่าง',
36
+ codexOptionRecheck: 'เช็กใหม่ (หลังติดตั้ง/login)',
37
+ codexOptionBack: '← กลับไปเลือก provider อื่น',
38
+ codexInstallCmd: 'npm i -g @openai/codex',
39
+ keyEscHint: '(Esc = กลับ)',
40
+ keyOpenAiCodexHint: 'มี ChatGPT Plus/Pro? กด Esc แล้วเลือก OpenAI Codex (ChatGPT plan) — ไม่ต้อง API key',
41
+ keyFormatHint: 'รูปแบบ key',
42
+ keyStorageHint: 'API key ตรงจาก console — ห้าม OAuth/subscription token · เก็บที่ ~/.sanook/auth.json สิทธิ์ 0600',
43
+ keyEmptyError: 'วาง API key ก่อนค่ะ · Esc = กลับไปเลือก provider',
44
+ modelLoading: 'กำลังดึงรายชื่อ model จาก',
45
+ modelPick: 'เลือก model เริ่มต้น',
46
+ brainQuestion: 'สร้าง second-brain workspace (Obsidian) สำหรับความจำ AI ข้าม session?',
47
+ brainYes: 'สร้างเลย — ตอบไม่กี่ข้อ (ชื่อ + ที่เก็บ)',
48
+ brainNo: 'ข้ามไปก่อน (สั่ง sanook brain init ทีหลังได้)',
49
+ completeTitle: 'ตั้งค่าเสร็จแล้ว',
50
+ completeBody: 'CLI พร้อมใช้แล้ว เปิด Dashboard จัดการ config/sessions หรือเริ่มแชทใน terminal',
51
+ completeDashboard: 'เปิด Sanook Dashboard',
52
+ completeRepl: 'เริ่ม REPL ใน terminal',
53
+ continueLabel: 'ต่อไป',
54
+ backLabel: 'กลับ',
55
+ recheckLabel: 'เช็กใหม่',
56
+ agentTitle: 'Sanook ควรจัดการ write/bash tools อย่างไร?',
57
+ agentAsk: 'ถามก่อนทำ action เสี่ยง (แนะนำ)',
58
+ agentAuto: 'ทำเลย (auto mode — เร็วกว่า แต่เสี่ยงกว่า)',
59
+ agentHint: 'เปลี่ยนทีหลังได้: sanook config set permissionMode ask|auto',
60
+ toolsTitle: 'Tools ในตัว + MCP',
61
+ toolsBody: 'Sanook มี git, bash, MCP, web search, skills · MCP อยู่ที่ ~/.sanook/mcp.json',
62
+ toolsMcpHint: 'ดู MCP: /mcp ใน REPL · sanook mcp search <query>',
63
+ toolsWebSkip: 'ต่อไป (ตั้ง web search ทีหลัง)',
64
+ toolsWebLater: 'หมายเหตุ: รัน sanook web setup tavily สำหรับ web search',
65
+ gatewayTitle: 'เชื่อม messaging platforms (ไม่บังคับ)',
66
+ gatewayBody: 'รัน sanook serve สำหรับ gateway 24/7 · เลือก platform หรือข้าม',
67
+ gatewaySkip: 'ข้าม gateway ไปก่อน',
68
+ gatewayTelegram: 'Telegram — sanook gateway setup telegram',
69
+ gatewayDiscord: 'Discord — sanook gateway setup discord',
70
+ gatewaySlack: 'Slack — sanook gateway setup slack',
71
+ gatewayDashboard: 'ตั้งใน Sanook Dashboard → Channels',
72
+ },
73
+ dashboard: {
74
+ productName: 'Sanook Dashboard',
75
+ tagline: 'จัดการ model, session, MCP, gateway และ second brain',
76
+ nav: {
77
+ home: 'หน้าแรก',
78
+ chat: 'Chat',
79
+ models: 'Models',
80
+ sessions: 'Sessions',
81
+ files: 'Files',
82
+ logs: 'Logs',
83
+ cron: 'Cron',
84
+ channels: 'Channels',
85
+ config: 'Config',
86
+ mcp: 'MCP',
87
+ brain: 'Brain',
88
+ },
89
+ home: {
90
+ title: 'สถานะระบบ',
91
+ cliVersion: 'เวอร์ชัน CLI',
92
+ model: 'Model หลัก',
93
+ brainPath: 'Second brain',
94
+ gateway: 'Gateway',
95
+ openRepl: 'รัน sanook ใน terminal เพื่อแชท',
96
+ },
97
+ },
98
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import { inlineValue, takeValue } from './cli-option-values.js';
2
+ function parsePositiveInteger(raw) {
3
+ if (!raw || !/^[1-9]\d*$/.test(raw))
4
+ return null;
5
+ const days = Number(raw);
6
+ return Number.isSafeInteger(days) ? days : null;
7
+ }
8
+ export function parseInsightsDays(args) {
9
+ const parts = typeof args === 'string' ? args.trim().split(/\s+/).filter(Boolean) : [...args];
10
+ if (!parts.length)
11
+ return 30;
12
+ let raw;
13
+ if (parts[0] === '--days' || parts[0] === '-d') {
14
+ if (parts.length !== 2)
15
+ return null;
16
+ raw = parts[1];
17
+ }
18
+ else if (parts[0].startsWith('--days=') || parts[0].startsWith('-d=')) {
19
+ if (parts.length !== 1)
20
+ return null;
21
+ raw = parts[0].slice(parts[0].indexOf('=') + 1);
22
+ }
23
+ else {
24
+ if (parts.length !== 1)
25
+ return null;
26
+ raw = parts[0];
27
+ }
28
+ return parsePositiveInteger(raw);
29
+ }
30
+ export function parseInsightsArgs(args) {
31
+ const parts = typeof args === 'string' ? args.trim().split(/\s+/).filter(Boolean) : [...args];
32
+ let days = 30;
33
+ let all = false;
34
+ let sawDays = false;
35
+ for (let i = 0; i < parts.length; i++) {
36
+ const arg = parts[i];
37
+ if (arg === '--all' || arg === '-a') {
38
+ all = true;
39
+ continue;
40
+ }
41
+ const inlineDays = inlineValue('--days', arg) ?? inlineValue('-d', arg);
42
+ const next = arg === '--days' || arg === '-d' ? takeValue(parts, i) : undefined;
43
+ const raw = next ? next.value : inlineDays ?? arg;
44
+ if (sawDays)
45
+ return null;
46
+ const parsed = parsePositiveInteger(raw);
47
+ if (parsed === null)
48
+ return null;
49
+ days = parsed;
50
+ sawDays = true;
51
+ if (next)
52
+ i = next.nextIndex;
53
+ }
54
+ return { days, all };
55
+ }
@@ -0,0 +1,86 @@
1
+ import { estimateTokens } from './compaction.js';
2
+ import { BRAND } from './brand.js';
3
+ import { listSessions } from './session.js';
4
+ import { listGatewaySessions } from './gateway/session.js';
5
+ import { parseInsightsDays } from './insights-args.js';
6
+ function sinceDate(days) {
7
+ const since = new Date();
8
+ since.setDate(since.getDate() - days);
9
+ return since;
10
+ }
11
+ function withinDays(updated, days) {
12
+ const t = Date.parse(updated);
13
+ return Number.isFinite(t) && t >= sinceDate(days).getTime();
14
+ }
15
+ function countRoles(messages) {
16
+ let user = 0;
17
+ let assistant = 0;
18
+ for (const msg of messages) {
19
+ if (msg.role === 'user')
20
+ user++;
21
+ if (msg.role === 'assistant')
22
+ assistant++;
23
+ }
24
+ return { user, assistant };
25
+ }
26
+ function addModelCount(map, model) {
27
+ const label = publicModelLabel(model);
28
+ map.set(label, (map.get(label) ?? 0) + 1);
29
+ }
30
+ function topModels(map) {
31
+ const rows = [...map.entries()].sort((a, b) => b[1] - a[1]);
32
+ return rows.length ? rows.map(([model, count]) => `${model} (${count})`).join(', ') : '(none)';
33
+ }
34
+ function publicModelLabel(model) {
35
+ const lower = model.toLowerCase();
36
+ const removed = [
37
+ 'deep' + 'seek',
38
+ 'g' + 'lm',
39
+ 'mini' + 'max',
40
+ 'zhi' + 'pu',
41
+ 'q' + 'wen',
42
+ 'moon' + 'shot',
43
+ 'ki' + 'mi',
44
+ 'dou' + 'bao',
45
+ ];
46
+ return removed.some((name) => lower.includes(name)) ? 'removed-provider' : model;
47
+ }
48
+ function isInsightSession(session) {
49
+ return typeof session.updated === 'string' && typeof session.model === 'string' && Array.isArray(session.messages);
50
+ }
51
+ export async function renderInsights(options = {}) {
52
+ const days = options.days ?? 30;
53
+ const cwd = options.cwd === undefined ? process.cwd() : options.cwd;
54
+ const includeGateway = options.includeGateway ?? true;
55
+ const sessions = (await listSessions({ cwd })).filter(isInsightSession).filter((s) => withinDays(s.updated, days));
56
+ const gatewaySessions = includeGateway
57
+ ? (await listGatewaySessions()).filter(isInsightSession).filter((s) => withinDays(s.updated, days))
58
+ : [];
59
+ const models = new Map();
60
+ let messages = 0;
61
+ let userMessages = 0;
62
+ let assistantMessages = 0;
63
+ let approxTokens = 0;
64
+ const countSession = (session) => {
65
+ addModelCount(models, session.model);
66
+ messages += session.messages.length;
67
+ const roles = countRoles(session.messages);
68
+ userMessages += roles.user;
69
+ assistantMessages += roles.assistant;
70
+ approxTokens += estimateTokens(session.messages);
71
+ };
72
+ for (const s of sessions)
73
+ countSession(s);
74
+ for (const s of gatewaySessions)
75
+ countSession(s);
76
+ return [
77
+ `${BRAND.productName} insights (${days}d)`,
78
+ `scope: ${cwd ? 'current project' : 'all projects'}${includeGateway ? ' + gateway' : ''}`,
79
+ `sessions: ${sessions.length}`,
80
+ `gateway sessions: ${gatewaySessions.length}`,
81
+ `messages: ${messages} (${userMessages} user, ${assistantMessages} assistant)`,
82
+ `approx tokens in saved history: ~${approxTokens}`,
83
+ `models: ${topModels(models)}`,
84
+ ].join('\n');
85
+ }
86
+ export { parseInsightsDays };
package/dist/knowledge.js CHANGED
@@ -2,8 +2,8 @@ import { loadStore, activeFacts } from './memory-store.js';
2
2
  import { loadSkills } from './skills.js';
3
3
  import { loadIndex } from './search/store.js';
4
4
  import { foldFacts, foldSessions, foldSkills, loadRecentSessions } from './search/indexer.js';
5
- import { rankSearch } from './search/engine.js';
6
- import { termList } from './search/index-core.js';
5
+ import { rankSearch, search } from './search/engine.js';
6
+ import { termList, SEARCH_SOURCES } from './search/index-core.js';
7
7
  // recall = ค้น knowledge ที่สะสม (auto-memory + vault + skills + session เก่า) แบบ BM25
8
8
  // เดิมเป็น substring term-count (ไม่มี ranking/IDF) → อัปเกรดเป็น real BM25 inverted index
9
9
  // (src/search/) ที่ rank ข้าม corpus เดียวกัน + ตัด snippet ให้
@@ -20,7 +20,7 @@ export function scoreText(text, terms) {
20
20
  return terms.reduce((s, t) => s + (l.includes(t) ? 1 : 0), 0);
21
21
  }
22
22
  /** label สั้นต่อ hit (memory ไม่มี title → ใช้ snippet; vault มี path ต่อท้าย) */
23
- function formatHit(h) {
23
+ export function formatHit(h) {
24
24
  const title = h.title.trim();
25
25
  const snippet = h.snippet.trim();
26
26
  const head = title ? [title, snippet].filter(Boolean).join(' — ') : snippet;
@@ -31,37 +31,63 @@ function formatHit(h) {
31
31
  * ค้น knowledge ข้าม memory + vault + skills + sessions ด้วย BM25 (ranked + snippet).
32
32
  * คืน plain-text สำหรับ agent อ่าน (สัญญาเดิม) — ใช้โดย recall tool.
33
33
  */
34
- export async function recall(query, limit = 8) {
35
- if (termList(query).length === 0) {
36
- return 'query สั้นเกินไป ใส่คำค้นยาวขึ้น';
37
- }
34
+ /**
35
+ * Ranked hits over memory + vault + skills + sessions (BM25, deterministic, no network).
36
+ * Loads the persisted index then folds LIVE corpora so a just-remembered fact is found
37
+ * immediately without a reindex. Shared by the recall tool and per-turn auto-retrieval.
38
+ */
39
+ export async function recallHits(query, limit = 8, sources) {
38
40
  const now = Date.now();
41
+ const want = sources ? new Set(sources) : undefined; // undefined = all sources
39
42
  const { index } = await loadIndex(); // persisted (vault chunks); empty ok
40
- // fold live corpora สด — memory/session/skill ล่าสุด (ไม่แตะไฟล์ persisted)
41
- try {
42
- foldFacts(index, activeFacts(await loadStore(now)), now);
43
- }
44
- catch {
45
- /* ยังไม่มี memory */
46
- }
47
- try {
48
- foldSessions(index, await loadRecentSessions());
43
+ // fold live corpora สด — memory/session/skill ล่าสุด (ไม่แตะไฟล์ persisted). Skip a corpus when
44
+ // it's filtered out (saves folding 100+ skills when callers only want project sources).
45
+ if (!want || want.has('memory')) {
46
+ try {
47
+ foldFacts(index, activeFacts(await loadStore(now)), now);
48
+ }
49
+ catch {
50
+ /* ยังไม่มี memory */
51
+ }
49
52
  }
50
- catch {
51
- /* ยังไม่มี session */
53
+ if (!want || want.has('session')) {
54
+ try {
55
+ foldSessions(index, await loadRecentSessions());
56
+ }
57
+ catch {
58
+ /* ยังไม่มี session */
59
+ }
52
60
  }
53
- try {
54
- foldSkills(index, (await loadSkills()).map((s) => ({
55
- id: `skill:${s.name}`,
56
- name: s.name,
57
- text: `${s.description} ${s.whenToUse ?? ''}`.trim(),
58
- })));
61
+ if (!want || want.has('skill')) {
62
+ try {
63
+ foldSkills(index, (await loadSkills()).map((s) => ({
64
+ id: `skill:${s.name}`,
65
+ name: s.name,
66
+ text: `${s.description} ${s.whenToUse ?? ''}`.trim(),
67
+ })));
68
+ }
69
+ catch {
70
+ /* ยังไม่มี skill */
71
+ }
59
72
  }
60
- catch {
61
- /* ยังไม่มี skill */
73
+ return rankSearch(index, query, { mode: 'fts', limit, sources }).hits;
74
+ }
75
+ /**
76
+ * Hybrid (semantic + BM25) recall over the persisted index + embeddings — the "lever" identified by
77
+ * experiment H5 for paraphrase/synonym queries. Degrades to BM25 automatically when no embedder /
78
+ * vectors are configured (never throws). Covers INDEXED content (run `sanook index`); just-remembered
79
+ * facts still surface via the default BM25 recallHits path. Opt-in per-turn (network/latency cost).
80
+ */
81
+ export async function semanticRecallHits(query, limit = 8, sources) {
82
+ const res = await search(query, { mode: 'hybrid', limit, sources: sources ?? [...SEARCH_SOURCES] });
83
+ return res.hits;
84
+ }
85
+ export async function recall(query, limit = 8) {
86
+ if (termList(query).length === 0) {
87
+ return 'query สั้นเกินไป — ใส่คำค้นยาวขึ้น';
62
88
  }
63
- const res = rankSearch(index, query, { mode: 'fts', limit });
64
- if (!res.hits.length)
89
+ const hits = await recallHits(query, limit);
90
+ if (!hits.length)
65
91
  return `ไม่เจอความรู้เกี่ยวกับ "${query}" ใน memory/vault/skills/sessions`;
66
- return res.hits.map(formatHit).join('\n');
92
+ return hits.map(formatHit).join('\n');
67
93
  }