sanook-cli 0.4.0 → 0.5.0

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 (235) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +144 -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 +394 -51
  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 +2 -2
  35. package/dist/providers/keys.js +3 -2
  36. package/dist/providers/registry.js +133 -1
  37. package/dist/repomap.js +93 -0
  38. package/dist/search/chunk.js +158 -0
  39. package/dist/search/embed-store.js +187 -0
  40. package/dist/search/engine.js +203 -0
  41. package/dist/search/fuse.js +35 -0
  42. package/dist/search/index-core.js +187 -0
  43. package/dist/search/indexer.js +241 -0
  44. package/dist/search/store.js +77 -0
  45. package/dist/session.js +42 -8
  46. package/dist/skill-install.js +10 -10
  47. package/dist/skills.js +12 -9
  48. package/dist/summarize.js +31 -0
  49. package/dist/tools/bash.js +21 -2
  50. package/dist/tools/diagnostics.js +41 -0
  51. package/dist/tools/edit.js +29 -7
  52. package/dist/tools/index.js +8 -1
  53. package/dist/tools/list.js +7 -2
  54. package/dist/tools/permission.js +90 -9
  55. package/dist/tools/read.js +23 -4
  56. package/dist/tools/remember.js +1 -1
  57. package/dist/tools/sandbox.js +61 -0
  58. package/dist/tools/search.js +105 -4
  59. package/dist/tools/task.js +195 -29
  60. package/dist/tools/timeout.js +35 -0
  61. package/dist/tools/util.js +10 -0
  62. package/dist/tools/write.js +6 -4
  63. package/dist/trust.js +89 -0
  64. package/dist/ui/app.js +218 -27
  65. package/dist/ui/banner.js +4 -9
  66. package/dist/ui/history.js +30 -0
  67. package/dist/ui/mentions.js +44 -0
  68. package/dist/ui/setup.js +6 -5
  69. package/dist/ui/useEditor.js +83 -0
  70. package/dist/update.js +114 -0
  71. package/dist/worktree.js +173 -0
  72. package/package.json +11 -5
  73. package/scripts/postinstall.mjs +33 -0
  74. package/second-brain/.agents/_Index.md +30 -0
  75. package/second-brain/.agents/skills/_Index.md +30 -0
  76. package/second-brain/.agents/workflows/_Index.md +30 -0
  77. package/second-brain/AGENTS.md +4 -4
  78. package/second-brain/Acceptance/_Index.md +30 -0
  79. package/second-brain/Acceptance/golden-case-template.md +39 -0
  80. package/second-brain/Areas/_Index.md +30 -0
  81. package/second-brain/Bugs/System-OS/_Index.md +30 -0
  82. package/second-brain/Bugs/_Index.md +30 -0
  83. package/second-brain/CLAUDE.md +4 -1
  84. package/second-brain/Checklists/_Index.md +30 -0
  85. package/second-brain/Checklists/preflight-postflight-template.md +29 -0
  86. package/second-brain/Distillations/_Index.md +30 -0
  87. package/second-brain/Entities/_Index.md +30 -0
  88. package/second-brain/Entities/entity-template.md +33 -0
  89. package/second-brain/Evals/_Index.md +30 -0
  90. package/second-brain/Evals/correction-pairs.md +24 -0
  91. package/second-brain/Evals/failure-taxonomy.md +24 -0
  92. package/second-brain/Evals/golden-set.md +25 -0
  93. package/second-brain/Evals/quality-ledger.md +23 -0
  94. package/second-brain/Evals/self-eval-rubric.md +23 -0
  95. package/second-brain/GEMINI.md +4 -4
  96. package/second-brain/Goals/_Index.md +30 -0
  97. package/second-brain/Handoffs/_Index.md +30 -0
  98. package/second-brain/Home.md +7 -0
  99. package/second-brain/Intake/Raw Sources/_Index.md +30 -0
  100. package/second-brain/Intake/_Index.md +30 -0
  101. package/second-brain/Intake/_Quarantine/_Index.md +30 -0
  102. package/second-brain/Learning/_Index.md +30 -0
  103. package/second-brain/Playbooks/_Index.md +30 -0
  104. package/second-brain/Playbooks/playbook-template.md +23 -0
  105. package/second-brain/Projects/_Index.md +30 -0
  106. package/second-brain/Prompts/_Index.md +30 -0
  107. package/second-brain/README.md +2 -1
  108. package/second-brain/Research/_Index.md +30 -0
  109. package/second-brain/Retrospectives/_Index.md +30 -0
  110. package/second-brain/Reviews/_Index.md +30 -0
  111. package/second-brain/Runbooks/_Index.md +30 -0
  112. package/second-brain/Runbooks/eval-loop.md +24 -0
  113. package/second-brain/Sessions/_Index.md +30 -0
  114. package/second-brain/Shared/AI-Context-Index.md +20 -0
  115. package/second-brain/Shared/AI-Threads/_Index.md +30 -0
  116. package/second-brain/Shared/Archive/_Index.md +30 -0
  117. package/second-brain/Shared/Assets/_Index.md +30 -0
  118. package/second-brain/Shared/Context-Packs/_Index.md +30 -0
  119. package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
  120. package/second-brain/Shared/Coordination/NOW.md +28 -0
  121. package/second-brain/Shared/Coordination/_Index.md +30 -0
  122. package/second-brain/Shared/Coordination/agent-registry.md +24 -0
  123. package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
  124. package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
  125. package/second-brain/Shared/Coordination/task-board.md +32 -0
  126. package/second-brain/Shared/Core-Facts/_Index.md +30 -0
  127. package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
  128. package/second-brain/Shared/Glossary/_Index.md +30 -0
  129. package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
  130. package/second-brain/Shared/Operating-State/_Index.md +30 -0
  131. package/second-brain/Shared/Prompting/_Index.md +30 -0
  132. package/second-brain/Shared/Provenance/_Index.md +30 -0
  133. package/second-brain/Shared/Rules/_Index.md +30 -0
  134. package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
  135. package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
  136. package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
  137. package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
  138. package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
  139. package/second-brain/Shared/Rules/rules-formatting.md +34 -0
  140. package/second-brain/Shared/Scripts/_Index.md +30 -0
  141. package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
  142. package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
  143. package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
  144. package/second-brain/Shared/User-Memory/_Index.md +30 -0
  145. package/second-brain/Shared/User-Persona/_Index.md +30 -0
  146. package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
  147. package/second-brain/Shared/Working-Memory/_Index.md +30 -0
  148. package/second-brain/Shared/_Index.md +30 -0
  149. package/second-brain/Shared/mcp-servers/_Index.md +30 -0
  150. package/second-brain/Skills/_Index.md +30 -0
  151. package/second-brain/Templates/_Index.md +30 -0
  152. package/second-brain/Templates/bug.md +2 -0
  153. package/second-brain/Templates/handoff.md +2 -0
  154. package/second-brain/Templates/session.md +2 -0
  155. package/second-brain/Tools/_Index.md +30 -0
  156. package/second-brain/Traces/_Index.md +30 -0
  157. package/second-brain/Vault Structure Map.md +33 -1
  158. package/second-brain/copilot/_Index.md +30 -0
  159. package/skills/audit-license-compliance/SKILL.md +117 -0
  160. package/skills/author-codemod/SKILL.md +110 -0
  161. package/skills/build-audit-logging/SKILL.md +112 -0
  162. package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
  163. package/skills/build-cli-tool/SKILL.md +108 -0
  164. package/skills/build-data-table/SKILL.md +141 -0
  165. package/skills/build-native-mobile-ui/SKILL.md +154 -0
  166. package/skills/build-offline-first-sync/SKILL.md +118 -0
  167. package/skills/build-realtime-channel/SKILL.md +122 -0
  168. package/skills/build-vector-search/SKILL.md +131 -0
  169. package/skills/compose-local-dev-stack/SKILL.md +149 -0
  170. package/skills/configure-bundler-build/SKILL.md +166 -0
  171. package/skills/configure-dns-tls/SKILL.md +142 -0
  172. package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
  173. package/skills/configure-security-headers-csp/SKILL.md +122 -0
  174. package/skills/contract-testing/SKILL.md +140 -0
  175. package/skills/datetime-timezone-correctness/SKILL.md +125 -0
  176. package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
  177. package/skills/debug-flaky-tests/SKILL.md +128 -0
  178. package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
  179. package/skills/deliver-webhooks/SKILL.md +116 -0
  180. package/skills/design-api-pagination/SKILL.md +144 -0
  181. package/skills/design-authorization-model/SKILL.md +119 -0
  182. package/skills/design-backup-dr-recovery/SKILL.md +113 -0
  183. package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
  184. package/skills/design-multi-tenancy/SKILL.md +100 -0
  185. package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
  186. package/skills/design-relational-schema/SKILL.md +129 -0
  187. package/skills/design-search-index-infra/SKILL.md +151 -0
  188. package/skills/design-state-machine/SKILL.md +108 -0
  189. package/skills/design-token-system/SKILL.md +109 -0
  190. package/skills/distributed-locks-leases/SKILL.md +120 -0
  191. package/skills/encrypt-sensitive-data/SKILL.md +148 -0
  192. package/skills/feature-flags-rollout/SKILL.md +130 -0
  193. package/skills/file-upload-object-storage/SKILL.md +107 -0
  194. package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
  195. package/skills/harden-llm-app-reliability/SKILL.md +126 -0
  196. package/skills/i18n-localization-setup/SKILL.md +113 -0
  197. package/skills/idempotency-keys/SKILL.md +107 -0
  198. package/skills/implement-push-notifications/SKILL.md +142 -0
  199. package/skills/ingest-webhook-secure/SKILL.md +120 -0
  200. package/skills/integrate-oauth-oidc/SKILL.md +126 -0
  201. package/skills/load-stress-test/SKILL.md +129 -0
  202. package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
  203. package/skills/model-nosql-data/SKILL.md +118 -0
  204. package/skills/money-decimal-arithmetic/SKILL.md +123 -0
  205. package/skills/monitor-ml-drift/SKILL.md +109 -0
  206. package/skills/numeric-precision-units/SKILL.md +144 -0
  207. package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
  208. package/skills/optimize-react-rerenders/SKILL.md +124 -0
  209. package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
  210. package/skills/payments-billing-integration/SKILL.md +114 -0
  211. package/skills/pin-toolchain-versions/SKILL.md +116 -0
  212. package/skills/plan-strangler-migration/SKILL.md +95 -0
  213. package/skills/property-based-testing/SKILL.md +108 -0
  214. package/skills/publish-package-registry/SKILL.md +130 -0
  215. package/skills/recover-git-state/SKILL.md +119 -0
  216. package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
  217. package/skills/resilience-timeouts-retries/SKILL.md +104 -0
  218. package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
  219. package/skills/rewrite-git-history/SKILL.md +109 -0
  220. package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
  221. package/skills/schema-evolution-compatibility/SKILL.md +121 -0
  222. package/skills/send-transactional-email/SKILL.md +126 -0
  223. package/skills/serve-deploy-ml-model/SKILL.md +107 -0
  224. package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
  225. package/skills/setup-devcontainer-env/SKILL.md +131 -0
  226. package/skills/setup-lint-format-precommit/SKILL.md +140 -0
  227. package/skills/setup-monorepo-tooling/SKILL.md +125 -0
  228. package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
  229. package/skills/structured-output-llm/SKILL.md +86 -0
  230. package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
  231. package/skills/test-data-factories/SKILL.md +158 -0
  232. package/skills/threat-model-stride/SKILL.md +123 -0
  233. package/skills/train-evaluate-ml-model/SKILL.md +109 -0
  234. package/skills/unicode-text-correctness/SKILL.md +109 -0
  235. package/skills/visual-regression-testing/SKILL.md +120 -0
package/dist/brain.js CHANGED
@@ -1,7 +1,8 @@
1
- import { readFile, writeFile, mkdir, readdir, stat } from 'node:fs/promises';
1
+ import { chmod, readFile, writeFile, mkdir, readdir, stat } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { appHomePath } from './brand.js';
5
6
  /** ขยาย ~ ขึ้นต้น path เป็น home dir */
6
7
  export function expandHome(p) {
7
8
  return p === '~' || p.startsWith('~/') ? join(homedir(), p.slice(1)) : p;
@@ -18,54 +19,67 @@ export const BRAIN_DEFAULTS = {
18
19
  vaultName: 'Second Brain',
19
20
  autonomy: 'ask-on-risk',
20
21
  };
21
- /**
22
- * โฟลเดอร์ทั้งหมด + บทบาท (จาก GEMINI.md §B.0 Folder Role Table) → generate _Index.md ให้ทุกอัน
23
- * top-level parent = Home · Shared/<x> parent = Shared/_Index (Shared เองชี้ Home)
24
- */
25
- // ⚠ sync กับ second-brain/Vault Structure Map.md — แก้ role/โฟลเดอร์ ต้องแก้ทั้งสองที่ (มี test กัน drift)
26
22
  export const FOLDERS = [
27
- // Core (MVV)
28
- { dir: 'Projects', role: 'workspace ของงานจริง — 1 โฟลเดอร์ = 1 โปรเจค (overview/context/current-state)' },
29
- { dir: 'Sessions', role: 'flat chronological log ของงาน + checkpoint (YYYY-MM-DD-<topic>.md)' },
30
- { dir: 'Intake', role: 'จุดรับของใหม่เข้า vault (raw input + task framing) ก่อนกระจายเข้าปลายทาง' },
31
- { dir: 'Intake/_Quarantine', role: 'external content (web/paste) ที่ยัง untrusted scan injection ก่อน promote (ดู Runbooks/ingest-quarantine)' },
32
- { dir: 'Intake/Raw Sources', role: 'ต้นฉบับ external ที่ผ่าน scan แล้ว — immutable read-only, source:: ชี้มาที่นี่ได้' },
33
- { dir: 'Skills', role: 'reusable unit ที่ executable + ผ่าน verification command ไม่ใช่ prose (นั่นคือ Runbooks) ดู Shared/Rules/skills-admission' },
34
- { dir: 'Runbooks', role: 'prose how-to ที่อ่านแล้วทำตามเอง (setup/deploy/maintain) ไม่ใช่ runnable unit (นั่นคือ Skills)' },
35
- { dir: 'Templates', role: 'แม่แบบโน้ต — instantiate จากที่นี่ตอนสร้างโน้ตใหม่' },
36
- { dir: 'Bugs', role: 'bug report reproducible ลงวันที่ ไม่ลบ project bug ก็มาที่นี่ (global, flat) + link กลับ project' },
37
- { dir: 'Handoffs', role: 'เอกสารส่งมอบงานค้าง 1 ชิ้น (state + next steps) snapshot ครั้งเดียว ไม่ใช่ live coordination' },
38
- // Direction
39
- { dir: 'Goals', role: 'north-star + objective รายไตรมาส/ปี (finite, มีวันจบ) — ไม่เก็บ live status (นั่นคือ Operating-State)' },
40
- { dir: 'Areas', role: 'PARA โดเมนงานต่อเนื่องที่ไม่มีวันจบ (brand/trading/content...)' },
41
- // Knowledge pipeline
42
- { dir: 'Research', role: 'finding ที่อิงแหล่งภายนอก (มี source::) + market scan + reference synthesis' },
43
- { dir: 'Learning', role: 'knowledge ที่ตัวเองกลั่น/deep-dive ตาม topic (ไม่มี external source) curated MOC' },
44
- { dir: 'Distillations', role: 'หลักการ evergreen ที่กลั่นนิ่งแล้ว (เห็น ≥3 ครั้ง) atomic' },
45
- { dir: 'Retrospectives', role: 'reflection หลังงาน (event-triggered: what worked/failed)' },
46
- { dir: 'Reviews', role: 'review ตาม cadence (time-triggered: weekly/monthly) + vault health' },
47
- { dir: 'Traces', role: 'exploration/reasoning chain ยาว (คำถามใหญ่เกินโน้ตเดียว)' },
48
- { dir: 'Prompts', role: 'prompt text/template ที่หยิบมารันได้ทันที (input ให้ LLM)' },
49
- { dir: 'Acceptance', role: 'golden input→expected-output fixtures ที่ใช้ตัดสิน done/not-done ไม่ใช่ checklist, ไม่ใช่ runner (นั่นคือ Evals)' },
50
- { dir: 'Checklists', role: 'preflight/postflight gate (ticklist ก่อน-หลังลงมือ) ไม่เก็บ expected output' },
51
- // Frontier loops
52
- { dir: 'Playbooks', role: 'กลยุทธ์/ลำดับการตัดสินใจที่ปรับดีขึ้นจากผลจริง (how-to-decide) — ไม่ใช่ prompt text, ไม่ใช่ runnable unit' },
53
- { dir: 'Evals', role: 'quality loop ที่รัน Acceptance/golden-set แล้ว error-analysis + self-eval (runner + ผล, ไม่เก็บ case เอง)' },
54
- { dir: 'Entities', role: 'canonical page ต่อ entity/person/org/concept (LLM-wiki, bi-temporal)' },
55
- // Shared (สมองกลาง) Shared/_Index เองชี้ Home
56
- { dir: 'Shared', role: 'สมองกลาง: memory + rules + coordination (เข้าผ่าน AI-Context-Index)' },
57
- { dir: 'Shared/Operating-State', role: 'live status/metrics ตอนนี้ (current-state + health/queue)' },
58
- { dir: 'Shared/User-Memory', role: 'สิ่งที่ AI เรียนรู้เกี่ยวกับเจ้าของระหว่างทำงาน preference/response-example (mutable)' },
59
- { dir: 'Shared/Decision-Memory', role: 'การตัดสินใจที่ AI บันทึก locked (latest-wins + supersedes)' },
60
- { dir: 'Shared/Memory-Inbox', role: 'candidate durable memory ที่ยังไม่ชัด/ขัดกัน รอ promote (เคลียร์ทุก weekly)' },
61
- { dir: 'Shared/Rules', role: 'กฎ operating always-on (memory/frontmatter/context-assembly/graph)' },
62
- { dir: 'Shared/Tech-Standards', role: 'มาตรฐานเทคนิค (MCP/stack/DoD/verification)' },
63
- { dir: 'Shared/Core-Facts', role: 'ground truth ที่เจ้าของเขียนเอง — read-only, AI ไม่ supersede/ไม่แก้' },
64
- { dir: 'Shared/Coordination', role: 'live coordination ของหลาย agent พร้อมกัน (NOW.md baton + task-board + registry) ไม่ใช่เอกสารส่งมอบ (นั่นคือ Handoffs)' },
65
- { dir: 'Shared/Working-Memory', role: 'scratchpad ระหว่าง 1 task ลบทิ้งได้หลังจบ ไม่มีวัน promote' },
66
- { dir: 'Shared/User-Persona', role: 'identity profile ที่เปลี่ยนน้อยมาก (บทบาท/ค่านิยม/ภาษา/timezone) human-owned, read-only' },
67
- { dir: 'Shared/Provenance', role: 'lineage ledger ทุก claim ชี้ source:: ได้ (ingest-log)' },
68
- { dir: 'Shared/Archive', role: 'cold storage โน้ตที่ stale/retired ออกจาก retrieval (ไม่ลบ)' },
23
+ // ── Core (MVV) ──
24
+ { dir: 'Projects', role: 'workspace ของงานจริง — 1 โฟลเดอร์ = 1 โปรเจค', put: 'deliverable + overview/context/current-state ของ project', avoid: 'ความรู้ทั่วไป (→Learning) · log งาน (→Sessions)' },
25
+ { dir: 'Sessions', role: 'flat chronological log ของงาน (YYYY-MM-DD-<topic>.md)', put: 'session log 7 หัวข้อ + checkpoint', avoid: 'code/config · subfolder (Sessions = flat เสมอ)' },
26
+ { dir: 'Intake', role: 'จุดรับของใหม่เข้า vault ก่อนกระจายเข้าปลายทาง', put: 'task framing + raw input ที่รอจัด', avoid: 'durable knowledge (จัดเข้าปลายทางก่อน)' },
27
+ { dir: 'Intake/_Quarantine', role: 'external content (web/paste) ที่ยัง untrusted', put: 'web clip/paste/email ก่อน scan injection (ดู Runbooks/ingest-quarantine)', avoid: 'content ที่ scan ผ่านแล้ว (→Raw Sources)' },
28
+ { dir: 'Intake/Raw Sources', role: 'ต้นฉบับ external ที่ผ่าน scan แล้ว — immutable read-only', put: 'original หลัง scan · source:: ชี้มาที่นี่ได้', avoid: 'โน้ตที่ derived/สรุปแล้ว' },
29
+ { dir: 'Skills', role: 'reusable unit ที่ executable + ผ่าน verification command', put: 'script/command ที่รัน test ผ่าน (ดู Shared/Rules/skills-admission)', avoid: 'prose how-to (→Runbooks) · unverified (→Memory-Inbox)' },
30
+ { dir: 'Runbooks', role: 'prose how-to ที่อ่านแล้วทำตามเอง', put: 'ขั้นตอน setup/deploy/maintain + loop driver', avoid: 'runnable unit (Skills)' },
31
+ { dir: 'Templates', role: 'แม่แบบโน้ต — instantiate จากที่นี่', put: 'template ไว้ instantiate (session/bug/handoff/project)', avoid: 'โน้ตจริง' },
32
+ { dir: 'Bugs', role: 'bug report reproducible ลงวันที่ ไม่ลบ', put: 'bug report (global flat) + link กลับ project · system/OS → Bugs/System-OS/', avoid: 'bug ที่ reproduce ไม่ได้' },
33
+ { dir: 'Bugs/System-OS', role: 'bug report ระดับระบบ/OS/toolchain ที่ไม่ผูกกับ project เดียว', put: 'OS, shell, package manager, permission, filesystem, or app-runtime bugs', avoid: 'bug ของ project เฉพาะ (→Bugs หรือ Projects/<proj>/Bugs)' },
34
+ { dir: 'Handoffs', role: 'เอกสารส่งมอบงานค้าง 1 ชิ้น (snapshot)', put: 'state + next steps ส่งต่อ agent/session', avoid: 'live coordination (→Shared/Coordination)' },
35
+ // ── Direction ──
36
+ { dir: 'Goals', role: 'north-star + objective รายไตรมาส/ปี (finite, มีวันจบ)', put: 'objective + track progress', avoid: 'live status (→Operating-State) · โดเมนต่อเนื่อง (→Areas)' },
37
+ { dir: 'Areas', role: 'PARA — โดเมนงานต่อเนื่องที่ไม่มีวันจบ', put: 'brand/trading/content/products ฯลฯ', avoid: 'งานที่มีวันจบ (→Projects/Goals)' },
38
+ // ── Knowledge pipeline ──
39
+ { dir: 'Research', role: 'finding ที่อิงแหล่งภายนอก (มี source::)', put: 'สรุปอิงแหล่ง + market scan + citation', avoid: 'ความรู้ที่กลั่นเอง (→Learning)' },
40
+ { dir: 'Learning', role: 'knowledge ที่ตัวเองกลั่น/deep-dive ตาม topic', put: 'topic MOC ที่ไม่มี external source', avoid: 'finding อิงแหล่ง (→Research)' },
41
+ { dir: 'Distillations', role: 'หลักการ evergreen ที่กลั่นนิ่งแล้ว (≥3 ครั้ง) — atomic', put: 'principle ที่ reusable + นิ่งแล้ว', avoid: 'สิ่งที่ยังปรับเปลี่ยน (→Playbooks)' },
42
+ { dir: 'Retrospectives', role: 'reflection หลังงาน (event-triggered)', put: 'what worked/failed หลังงานเสร็จ', avoid: 'review ตามรอบเวลา (→Reviews)' },
43
+ { dir: 'Reviews', role: 'review ตาม cadence (time-triggered)', put: 'weekly/monthly + vault health + consolidation trace', avoid: 'reflection รายงาน (→Retrospectives)' },
44
+ { dir: 'Traces', role: 'exploration/reasoning chain ยาว', put: 'การสืบสวนหลายขั้น (คำถามใหญ่เกินโน้ตเดียว)', avoid: 'คำตอบสั้น (→โน้ตปกติ)' },
45
+ { dir: 'Prompts', role: 'prompt text/template ที่หยิบมารันได้ทันที', put: 'prompt/execution standard ต่อ task-family', avoid: 'fixtures (→Acceptance) · tactic (→Playbooks)' },
46
+ { dir: 'Acceptance', role: 'golden input→expected-output fixtures', put: 'case ที่ใช้ตัดสิน done/not-done', avoid: 'gate ticklist (→Checklists) · runner (→Evals)' },
47
+ { dir: 'Checklists', role: 'preflight/postflight gate (ticklist)', put: 'ticklist ก่อน-หลังลงมือ ต่อ task-family', avoid: 'expected output (→Acceptance)' },
48
+ // ── Frontier loops (self-improving) ──
49
+ { dir: 'Playbooks', role: 'กลยุทธ์/ลำดับการตัดสินใจที่ปรับดีขึ้นจากผลจริง (ACE)', put: 'tactic/heuristic ที่ดีขึ้นจากผล (counter [H/W])', avoid: 'prompt text (→Prompts) · runnable (→Skills)' },
50
+ { dir: 'Evals', role: 'quality loop (runner + ผล) — error-analysis + self-eval', put: 'failure-taxonomy/self-eval-rubric/golden-set/correction-pairs/quality-ledger', avoid: 'golden case เอง (→Acceptance)' },
51
+ { dir: 'Entities', role: 'canonical page ต่อ entity/person/org/concept (LLM-wiki, bi-temporal)', put: 'หน้า canonical ของ entity ที่เจอ ≥2 sessions', avoid: 'event log (→Sessions)' },
52
+ // ── Shared (สมองกลาง) Shared/_Index เองชี้ Home ──
53
+ { dir: 'Shared', role: 'สมองกลาง: memory + rules + coordination', put: 'เข้าผ่าน AI-Context-Index', avoid: 'โน้ตงานทั่วไป' },
54
+ { dir: 'Shared/Operating-State', role: 'live status/metrics ตอนนี้', put: 'current-state + health/queue + workbench', avoid: 'objective (→Goals)' },
55
+ { dir: 'Shared/User-Memory', role: 'สิ่งที่ AI เรียนรู้เกี่ยวกับเจ้าของ (mutable)', put: 'preference/response-example/signal', avoid: 'identity static (→User-Persona)' },
56
+ { dir: 'Shared/Decision-Memory', role: 'การตัดสินใจที่ AI บันทึก (latest-wins + supersedes)', put: 'decision locked + เหตุผล', avoid: 'ground truth คน (→Core-Facts)' },
57
+ { dir: 'Shared/Memory-Inbox', role: 'candidate durable memory ที่ยังไม่ชัด/ขัดกัน', put: 'observation รอ promote (เคลียร์ทุก weekly)', avoid: 'durable ที่ชัดแล้ว (→ปลายทาง)' },
58
+ { dir: 'Shared/Rules', role: 'กฎ operating always-on', put: 'memory-write-protocol/frontmatter/context-assembly/formatting/staleness', avoid: 'how-to ทำงาน (→Runbooks)' },
59
+ { dir: 'Shared/Tech-Standards', role: 'มาตรฐานเทคนิคกลาง', put: 'MCP/stack/DoD/verification rulebook', avoid: 'กฎ memory/format (→Rules)' },
60
+ { dir: 'Shared/Core-Facts', role: 'ground truth ที่เจ้าของเขียนเอง (read-only, invariant)', put: 'protected-facts ที่ AI ห้ามแก้/supersede', avoid: 'decision ที่ AI ตัด (→Decision-Memory)' },
61
+ { dir: 'Shared/Coordination', role: 'live coordination ของหลาย agent พร้อมกัน', put: 'NOW.md baton + task-board + agent-registry', avoid: 'เอกสารส่งมอบครั้งเดียว (→Handoffs)' },
62
+ { dir: 'Shared/Coordination/task-board', role: 'file-Kanban task cards สำหรับ multi-agent coordination', put: 'task file ต่อชิ้นงาน มี claimed_by/status/frontmatter', avoid: 'session narrative (→Sessions) · handoff snapshot (→Handoffs)' },
63
+ { dir: 'Shared/Working-Memory', role: 'scratchpad ระหว่าง 1 task (ลบทิ้งได้)', put: 'ของชั่วคราวระหว่างทำงาน', avoid: 'อะไรที่จะเก็บ (→Memory-Inbox)' },
64
+ { dir: 'Shared/User-Persona', role: 'identity profile ที่เปลี่ยนน้อยมาก (human-owned)', put: 'บทบาท/ค่านิยม/ภาษา/timezone (read-only)', avoid: 'สิ่งที่ AI เรียนรู้ (→User-Memory)' },
65
+ { dir: 'Shared/Provenance', role: 'lineage ledger — ทุก claim ชี้ source:: ได้', put: 'บรรทัด ingest ต่อแหล่ง (ingest-log)', avoid: 'โน้ต derived (ใส่ source:: แทน)' },
66
+ { dir: 'Shared/Archive', role: 'cold storage (ไม่ลบ)', put: 'โน้ต stale/retired ที่ออกจาก retrieval', avoid: 'ของที่ยังใช้' },
67
+ { dir: 'Shared/Scripts', role: 'automation maintenance (lint/graph audit/metrics)', put: 'สคริปต์ maintenance ที่รันจริง', avoid: 'one-off ที่ retired (→Scripts-Archive)' },
68
+ { dir: 'Shared/Scripts-Archive', role: 'สคริปต์ one-off ที่ retired', put: 'script เก่าเก็บเป็นประวัติ', avoid: 'script ที่ยังใช้ (→Scripts)' },
69
+ { dir: 'Shared/mcp-servers', role: 'vendored local MCP server bundle (code/README)', put: 'โค้ด/README ของ MCP server (config อยู่ Tech-Standards)', avoid: 'config การต่อ (→Tech-Standards/mcp.json)' },
70
+ { dir: 'Shared/Context-Packs', role: 'full-context bundle ต่อ domain/task-type', put: 'pack รวม context พร้อมโหลด', avoid: 'โน้ตเดี่ยว (→ปลายทางปกติ)' },
71
+ { dir: 'Shared/Context7-Docs', role: 'cached external lib doc (regenerable — gitignore)', put: 'cache ของ context7/lib doc', avoid: 'durable knowledge (→Learning/Research)' },
72
+ { dir: 'Shared/AI-Threads', role: 'saved AI reasoning/conversation trail (ไม่ใช่ source of truth)', put: 'thread ที่เก็บไว้ review/resume/promote', avoid: 'durable decision (promote → Decision-Memory)' },
73
+ { dir: 'Shared/Prompting', role: 'prompt-engineering pattern (style/structure)', put: 'pattern การเขียน prompt ที่ reuse', avoid: 'prompt asset ต่อ task (→Prompts)' },
74
+ { dir: 'Shared/Glossary', role: 'vocabulary กลาง (routes ไป category pages)', put: 'term + นิยาม กลาง', avoid: 'entity page (→Entities)' },
75
+ { dir: 'Shared/Assets', role: 'รูป/logo/binary ของ vault', put: 'image/logo/asset', avoid: 'โน้ต .md' },
76
+ // ── AI agent config / vendor exports ──
77
+ { dir: '.agents', role: 'agent-specific assets (skills/workflows) ของ vault นี้', put: 'skill + workflow guide ที่ agent ใช้', avoid: 'โน้ตงาน (→ปลายทางปกติ)' },
78
+ { dir: '.agents/skills', role: 'skill folders (SKILL.md) ที่ agent โหลด on-demand', put: 'SKILL.md ต่อ skill', avoid: 'prose how-to (→Runbooks)' },
79
+ { dir: '.agents/workflows', role: 'workflow guide (multi-step orchestration)', put: 'workflow ที่ทำซ้ำได้', avoid: 'one-off task' },
80
+ { dir: 'copilot', role: 'vendor export (conversation/custom-prompt/memory snapshot) — review/promote, ไม่ใช่ source of truth', put: 'export จาก Copilot ที่เก็บใน-vault', avoid: 'durable (promote เข้า durable layer)' },
81
+ // ── Optional / machine-local ที่ยังเกี่ยวกับ coding workflow ──
82
+ { dir: 'Tools', role: 'utility/tooling เฉพาะเครื่องหรือเฉพาะ vault', put: 'local helper, binary wrapper, one-off utility ที่ยังใช้อยู่', avoid: 'durable knowledge (→Learning/Runbooks) · verified executable skill (→Skills)' },
69
83
  ];
70
84
  /** แทน {{KEY}} ด้วยค่าจริงจาก config */
71
85
  export function substitute(text, cfg) {
@@ -82,11 +96,12 @@ export function substitute(text, cfg) {
82
96
  };
83
97
  return text.replace(/\{\{(\w+)\}\}/g, (whole, key) => (key in map ? map[key] : whole));
84
98
  }
85
- /** generate _Index.md ของโฟลเดอร์ — frontmatter + role + up:: (ตาม §18 / §B.3 rule 2-3) */
86
- function renderIndex(dir, role, cfg) {
87
- const name = dir.split('/').pop() ?? dir;
99
+ /** generate _Index.md ของโฟลเดอร์ — frontmatter + role + ใส่อะไร/ไม่ใส่อะไร + up:: (ตาม §18 / §B.3 rule 2-3) */
100
+ function renderIndex(f, cfg) {
101
+ const name = f.dir.split('/').pop() ?? f.dir;
88
102
  // parent = _Index ของโฟลเดอร์แม่ (nested) หรือ Home (top-level)
89
- const parent = dir.includes('/') ? `${dir.split('/').slice(0, -1).join('/')}/_Index` : 'Home';
103
+ const parent = f.dir.includes('/') ? `${f.dir.split('/').slice(0, -1).join('/')}/_Index` : 'Home';
104
+ const selfIndex = `${f.dir}/_Index`;
90
105
  const tag = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
91
106
  return `---
92
107
  tags: [index, moc, ${tag}]
@@ -98,7 +113,22 @@ parent: "[[${parent}]]"
98
113
 
99
114
  # ${name}
100
115
 
101
- > ${role}
116
+ > ${f.role}
117
+
118
+ ## ใส่ที่นี่
119
+ ${f.put ?? '_(ดู role ด้านบน)_'}
120
+
121
+ ## ไม่ใส่ที่นี่
122
+ ${f.avoid ?? '_(—)_'}
123
+
124
+ ## AI Routing Contract
125
+
126
+ - ก่อนเขียน: เช็กว่าเนื้อหาตรง "ใส่ที่นี่" และไม่เข้า "ไม่ใส่ที่นี่"; ถ้าก้ำกึ่งอ่าน [[Vault Structure Map]] ก่อน
127
+ - ก่อนสร้างไฟล์ใหม่: ค้นหาโน้ตเดิมในโฟลเดอร์นี้และโฟลเดอร์ใกล้เคียงก่อน เพื่อ merge/update แทน append ซ้ำ
128
+ - เมื่อสร้างโน้ตในโฟลเดอร์นี้: ตั้ง \`parent: "[[${selfIndex}]]"\` และท้ายไฟล์ \`up:: [[${selfIndex}]]\`
129
+ - หลังเขียน: เชื่อม link ไป source/project/session/decision ที่เกี่ยวข้อง และอัปเดต hub/index ถ้าโน้ตนี้ควรถูกค้นเจอในอนาคต
130
+
131
+ > รายละเอียดทุกโฟลเดอร์ + decision rules → [[Vault Structure Map]]
102
132
 
103
133
  _(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
104
134
 
@@ -146,13 +176,15 @@ async function walk(dir, base = dir) {
146
176
  export async function scaffoldBrain(targetPath, cfg) {
147
177
  const created = [];
148
178
  const skipped = [];
149
- // 1) folders + generated _Index
150
- for (const { dir, role } of FOLDERS) {
151
- await mkdir(join(targetPath, dir), { recursive: true });
152
- await writeIfMissing(join(targetPath, dir, '_Index.md'), renderIndex(dir, role, cfg), created, skipped);
179
+ // 1) folders + generated _Index (role + ใส่อะไร/ไม่ใส่อะไร)
180
+ for (const f of FOLDERS) {
181
+ await mkdir(join(targetPath, f.dir), { recursive: true });
182
+ await writeIfMissing(join(targetPath, f.dir, '_Index.md'), renderIndex(f, cfg), created, skipped);
153
183
  }
154
184
  // 2) rich seed files (substitute placeholders)
155
185
  for (const rel of await walk(TEMPLATE_DIR)) {
186
+ if (rel.split('/').pop() === '_Index.md')
187
+ continue; // generated จาก FOLDERS[] แล้ว ไม่ copy ซ้ำจาก template source
156
188
  const raw = await readFile(join(TEMPLATE_DIR, rel), 'utf8');
157
189
  await writeIfMissing(join(targetPath, rel), substitute(raw, cfg), created, skipped);
158
190
  }
@@ -165,7 +197,7 @@ export async function scaffoldBrain(targetPath, cfg) {
165
197
  * → agent อ่าน/เขียน vault ที่เพิ่ง scaffold ได้ทันที (ไม่ต้อง hand-author mcp.json)
166
198
  */
167
199
  export async function wireBrainMcp(vaultPath) {
168
- const mcpPath = join(homedir(), '.sanook', 'mcp.json');
200
+ const mcpPath = appHomePath('mcp.json');
169
201
  let cfg = {};
170
202
  try {
171
203
  cfg = JSON.parse(await readFile(mcpPath, 'utf8'));
@@ -181,6 +213,7 @@ export async function wireBrainMcp(vaultPath) {
181
213
  args: ['-y', '@modelcontextprotocol/server-filesystem', vaultPath],
182
214
  };
183
215
  await mkdir(dirname(mcpPath), { recursive: true });
184
- await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`);
216
+ await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
217
+ await chmod(mcpPath, 0o600).catch(() => { });
185
218
  return 'added';
186
219
  }
package/dist/brand.js ADDED
@@ -0,0 +1,47 @@
1
+ import { homedir, tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export const BRAND = {
4
+ productName: 'Sanook',
5
+ agentName: 'Sanook',
6
+ cliName: 'sanook',
7
+ configDirName: '.sanook',
8
+ memoryFileName: 'SANOOK.md',
9
+ modelEnvVar: 'SANOOK_MODEL',
10
+ gatewayServiceName: 'sanook-gateway',
11
+ mcpClientName: 'sanook',
12
+ autoMemoryTitle: 'Sanook Auto-Memory',
13
+ undoStashMessage: 'sanook /undo',
14
+ bannerWide: 'Sanook AI',
15
+ bannerNarrow: 'Sanook',
16
+ bannerTitle: 'Sanook AI CLI',
17
+ skillTempPrefix: 'sanook-skill-',
18
+ evalTempPrefix: 'sanook-eval-',
19
+ };
20
+ export const BRAND_ENV = {
21
+ allowOutsideWorkspace: 'SANOOK_ALLOW_OUTSIDE_WORKSPACE',
22
+ gatewayAllowWrite: 'SANOOK_GATEWAY_ALLOW_WRITE',
23
+ hooksInheritEnv: 'SANOOK_HOOKS_INHERIT_ENV',
24
+ disablePersistence: 'SANOOK_DISABLE_PERSISTENCE',
25
+ disableUpdateCheck: 'SANOOK_DISABLE_UPDATE_CHECK',
26
+ disableWorklog: 'SANOOK_DISABLE_WORKLOG',
27
+ trustProject: 'SANOOK_TRUST_PROJECT',
28
+ };
29
+ export function appHomePath(...parts) {
30
+ return join(homedir(), BRAND.configDirName, ...parts);
31
+ }
32
+ export function appProjectPath(cwd, ...parts) {
33
+ return join(cwd, BRAND.configDirName, ...parts);
34
+ }
35
+ export function appTempPath(name) {
36
+ return join(tmpdir(), name);
37
+ }
38
+ export function envFlag(name) {
39
+ const v = process.env[name];
40
+ return v === '1' || v?.toLowerCase() === 'true' || v?.toLowerCase() === 'yes';
41
+ }
42
+ export function persistenceEnabled() {
43
+ return !envFlag(BRAND_ENV.disablePersistence);
44
+ }
45
+ export function worklogEnabled() {
46
+ return !envFlag(BRAND_ENV.disableWorklog);
47
+ }
@@ -0,0 +1,37 @@
1
+ import { runGit, isGitRepo } from './git.js';
2
+ /** snapshot working tree แบบไม่แตะอะไร (git stash create) — คืน ref หรือ null ถ้าไม่ใช่ git repo */
3
+ export async function snapshotWorkTree(cwd = process.cwd()) {
4
+ if (!(await isGitRepo(cwd)))
5
+ return null;
6
+ try {
7
+ const sha = (await runGit(['stash', 'create'], cwd)).trim();
8
+ if (sha)
9
+ return sha;
10
+ // working tree clean → pin HEAD SHA จริง (ถ้าใช้ 'HEAD' lazy แล้ว HEAD ขยับ เช่นมี commit ระหว่างนั้น = restore ผิด)
11
+ const head = (await runGit(['rev-parse', 'HEAD'], cwd)).trim();
12
+ return head || null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /** restore tracked files กลับสู่ snapshot — stash ของปัจจุบันก่อน (recoverable) */
19
+ export async function restoreWorkTree(ref, cwd = process.cwd()) {
20
+ if (!(await isGitRepo(cwd)))
21
+ return { ok: false, reason: 'ไม่ใช่ git repo' };
22
+ try {
23
+ // safety: เก็บสถานะปัจจุบันเข้า stash ก่อน (กู้คืนได้ด้วย git stash pop)
24
+ const current = (await runGit(['stash', 'create'], cwd)).trim();
25
+ let recovery;
26
+ if (current) {
27
+ await runGit(['stash', 'store', '-m', 'sanook /rewind backup', current], cwd);
28
+ recovery = 'git stash pop';
29
+ }
30
+ // restore: ให้ index + worktree ตรงกับ snapshot (ลบ tracked files ที่ถูกเพิ่มหลัง snapshot ด้วย)
31
+ await runGit(['restore', `--source=${ref}`, '--staged', '--worktree', '.'], cwd);
32
+ return { ok: true, recovery };
33
+ }
34
+ catch (e) {
35
+ return { ok: false, reason: e.message };
36
+ }
37
+ }
package/dist/commands.js CHANGED
@@ -1,19 +1,46 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
1
3
  import { PROVIDERS, parseSpec } from './providers/registry.js';
4
+ import { appHomePath, BRAND } from './brand.js';
5
+ import { parseFrontmatter } from './skills.js';
6
+ import { projectConfigPathIfTrusted } from './trust.js';
2
7
  const HELP_TEXT = `คำสั่ง:
3
8
  /help แสดงคำสั่งทั้งหมด
4
9
  /model [spec] ดู/เปลี่ยน model (เช่น /model opus, /model openai:gpt-5)
5
10
  /tools ดู tools ที่ agent ใช้ได้
6
- /skills ดูจำนวน skills (จัดการ: sanook skill list)
11
+ /skills ดูจำนวน skills (จัดการ: ${BRAND.cliName} skill list)
12
+ /diff ดู git diff (สิ่งที่ agent แก้ในรอบนี้)
13
+ /undo stash การแก้ไฟล์ล่าสุด (กู้คืนด้วย git stash pop)
14
+ /rewind ย้อนกลับ 1 turn (คืนไฟล์ git + ตัดบทสนทนา, recoverable)
7
15
  /cost ดู token + cost รอบล่าสุด
16
+ ↑/↓ ประวัติ · @ไฟล์ แนบ context/รูป · \\ ลงท้าย = บรรทัดใหม่
8
17
  /clear ล้าง conversation (เริ่มใหม่)
9
- /compact บีบ context
10
- /quit ออก`;
18
+ /compact บีบ context (truncate · หรือ summarize ถ้าตั้ง compaction)
19
+ /quit ออก
20
+
21
+ นอก REPL (พิมพ์ใน shell):
22
+ ${BRAND.cliName} search "<q>" · index · brain init · serve · mcp serve · config set <k> <v>
23
+ ดูทั้งหมด: ${BRAND.cliName} --help
24
+
25
+ custom commands:
26
+ ~/.sanook/commands/<name>.md และ .sanook/commands/<name>.md (project ต้อง trust ก่อน)`;
11
27
  const TOOLS_LIST = [
12
- 'read_file write_file edit_file list_dir glob grep run_bash',
28
+ 'read_file (offset/limit) write_file edit_file (replace_all) list_dir glob grep run_bash',
13
29
  'git_status git_diff git_log git_commit',
14
30
  'remember recall · skill find_skills create_skill',
15
- 'schedule_task list_scheduled cancel_scheduled · task',
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)',
16
34
  ].join('\n ');
35
+ export function parseSlashInvocation(input) {
36
+ const trimmed = input.trim();
37
+ if (!trimmed.startsWith('/'))
38
+ return null;
39
+ const match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
40
+ if (!match)
41
+ return null;
42
+ return { name: match[1].toLowerCase(), args: match[2] ?? '' };
43
+ }
17
44
  /** /model (ไม่มี arg) — โชว์ model ปัจจุบัน + ตัวเลือกของ provider นั้น (alias จาก registry) */
18
45
  function modelMenu(current) {
19
46
  const { provider } = parseSpec(current);
@@ -57,10 +84,63 @@ export function parseCommand(input, ctx) {
57
84
  case 'tools':
58
85
  return { handled: true, message: `tools ที่ agent ใช้ได้ (+ MCP ที่ตั้งไว้):\n ${TOOLS_LIST}` };
59
86
  case 'skills':
60
- return { handled: true, message: 'skills โหลดจาก built-in + ~/.sanook/skills — จัดการด้วย "sanook skill list/add/remove"' };
87
+ return { handled: true, message: `skills โหลดจาก built-in + ${appHomePath('skills')} — จัดการด้วย "${BRAND.cliName} skill list/add/remove"` };
88
+ case 'diff':
89
+ return { handled: true, action: 'diff' };
90
+ case 'undo':
91
+ return { handled: true, action: 'undo' };
61
92
  case 'cost':
62
93
  return { handled: true, message: ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)' };
63
94
  default:
64
95
  return { handled: true, message: `ไม่รู้จักคำสั่ง /${cmd} — พิมพ์ /help` };
65
96
  }
66
97
  }
98
+ // ── custom slash commands: .sanook/commands/<name>.md → /<name> ──────────────
99
+ // ไฟล์ markdown (frontmatter optional) = prompt template ที่ส่งเข้า agent. $ARGUMENTS = ส่วนหลังชื่อคำสั่ง
100
+ // (เลียน Claude Code .claude/commands) — global ~/.sanook/commands + project .sanook/commands (project ทับ)
101
+ export const BUILTIN_COMMANDS = new Set([
102
+ 'help', '?', 'clear', 'compact', 'quit', 'exit', 'model', 'tools', 'skills', 'diff', 'undo', 'rewind', 'cost',
103
+ ]);
104
+ function isValidCommandName(name) {
105
+ return /^[a-z0-9][a-z0-9-]{0,40}$/.test(name);
106
+ }
107
+ /** scan custom commands จาก global + project (project override). ข้าม built-in ชื่อซ้ำ */
108
+ export async function loadCustomCommands(cwd = process.cwd()) {
109
+ const out = new Map();
110
+ const dirs = [appHomePath('commands')];
111
+ const projectCommands = await projectConfigPathIfTrusted('commands', cwd);
112
+ if (projectCommands)
113
+ dirs.push(projectCommands);
114
+ for (const dir of dirs) {
115
+ let files;
116
+ try {
117
+ files = await readdir(dir);
118
+ }
119
+ catch {
120
+ continue; // ไม่มีโฟลเดอร์ = ข้าม
121
+ }
122
+ for (const f of files) {
123
+ if (!f.endsWith('.md'))
124
+ continue;
125
+ const name = f.slice(0, -3).toLowerCase();
126
+ if (!isValidCommandName(name) || BUILTIN_COMMANDS.has(name))
127
+ continue;
128
+ try {
129
+ const { meta, body } = parseFrontmatter(await readFile(join(dir, f), 'utf8'));
130
+ out.set(name, { name, description: meta.description ?? '', body: body.trim() });
131
+ }
132
+ catch {
133
+ // อ่านไม่ได้ = ข้าม
134
+ }
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ /** แทน $ARGUMENTS / {{args}} ด้วย args; ถ้า template ไม่มี placeholder ก็ append args ต่อท้าย */
140
+ export function expandCustomCommand(cmd, args) {
141
+ const a = args.trim();
142
+ if (/\$ARGUMENTS|\{\{\s*args\s*\}\}/.test(cmd.body)) {
143
+ return cmd.body.replace(/\$ARGUMENTS|\{\{\s*args\s*\}\}/g, a);
144
+ }
145
+ return a ? `${cmd.body}\n\n${a}` : cmd.body;
146
+ }
@@ -67,11 +67,16 @@ export function estimateTokens(messages) {
67
67
  export function autoCompact(messages, tokenLimit, keepRecent = 20) {
68
68
  if (estimateTokens(messages) <= tokenLimit)
69
69
  return messages;
70
- const pruned = pruneToolResults(messages, 2);
71
- if (estimateTokens(pruned) <= tokenLimit)
72
- return pruned;
70
+ // เก็บ system message ที่นำหน้า (cached preamble: SYSTEM/skills/brain/git) ไว้เสมอ — ห้ามตัดทิ้ง
71
+ const firstNon = messages.findIndex((m) => m.role !== 'system');
72
+ const lead = firstNon > 0 ? messages.slice(0, firstNon) : [];
73
+ const body = lead.length ? messages.slice(lead.length) : messages;
74
+ const withLead = (rest) => (lead.length ? [...lead, ...rest] : rest);
75
+ const pruned = pruneToolResults(body, 2);
76
+ if (estimateTokens(withLead(pruned)) <= tokenLimit)
77
+ return withLead(pruned);
73
78
  if (pruned.length <= keepRecent + 1)
74
- return pruned;
79
+ return withLead(pruned);
75
80
  const firstUser = pruned.find((m) => m.role === 'user');
76
81
  let recent = pruned.slice(-keepRecent);
77
82
  // ตัด tool message ที่ค้างหัว — tool-result ที่ tool-call ถูกตัดไปแล้ว = orphan → API reject
@@ -81,5 +86,71 @@ export function autoCompact(messages, tokenLimit, keepRecent = 20) {
81
86
  role: 'user',
82
87
  content: '[บทสนทนาเก่าถูกตัดออกเพื่อประหยัด context — รายละเอียดดูได้จาก memory/session]',
83
88
  };
84
- return firstUser && !recent.includes(firstUser) ? [firstUser, marker, ...recent] : [marker, ...recent];
89
+ const tail = firstUser && !recent.includes(firstUser) ? [firstUser, marker, ...recent] : [marker, ...recent];
90
+ return withLead(tail);
91
+ }
92
+ /** flatten messages → readable transcript (สำหรับให้ model ย่อ) — ตัด tool-result ยาวกัน prompt บวม */
93
+ export function messagesToText(messages) {
94
+ const out = [];
95
+ for (const m of messages) {
96
+ if (typeof m.content === 'string') {
97
+ if (m.content.trim())
98
+ out.push(`${m.role}: ${m.content}`);
99
+ }
100
+ else if (Array.isArray(m.content)) {
101
+ for (const part of m.content) {
102
+ if (typeof part.text === 'string' && part.text.trim())
103
+ out.push(`${m.role}: ${part.text}`);
104
+ else if (part.type === 'tool-call')
105
+ out.push(`${m.role}: [call ${String(part.toolName ?? 'tool')}]`);
106
+ else if (part.type === 'tool-result' &&
107
+ part.output?.type === 'text' &&
108
+ typeof part.output.value === 'string') {
109
+ out.push(`tool: ${truncateText(part.output.value)}`);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ return out.join('\n');
115
+ }
116
+ /**
117
+ * compaction แบบ "ย่อ" (quality สูงกว่า truncate): เก็บ system lead + user แรก (intent) + N message ล่าสุด,
118
+ * ส่วนกลางถูก "ย่อ" ด้วย summarize() (cheap model) แทนการตัดทิ้ง → จำ context ได้ดีกว่าที่ budget เท่าเดิม.
119
+ * pure orchestration (inject summarize) → test ได้โดยไม่ต้องมี LLM. summarize ล้มเหลว → fallback autoCompact.
120
+ */
121
+ export async function summarizeCompact(messages, tokenLimit, summarize, keepRecent = 20) {
122
+ if (estimateTokens(messages) <= tokenLimit)
123
+ return messages;
124
+ const firstNon = messages.findIndex((m) => m.role !== 'system');
125
+ const lead = firstNon > 0 ? messages.slice(0, firstNon) : [];
126
+ const body = lead.length ? messages.slice(lead.length) : messages;
127
+ const withLead = (rest) => (lead.length ? [...lead, ...rest] : rest);
128
+ const pruned = pruneToolResults(body, 2);
129
+ if (estimateTokens(withLead(pruned)) <= tokenLimit)
130
+ return withLead(pruned);
131
+ if (pruned.length <= keepRecent + 1)
132
+ return withLead(pruned);
133
+ const firstUser = pruned.find((m) => m.role === 'user');
134
+ let recent = pruned.slice(-keepRecent);
135
+ while (recent.length && recent[0].role === 'tool')
136
+ recent = recent.slice(1); // กัน orphan tool-result หัว window
137
+ const recentSet = new Set(recent);
138
+ const middle = pruned.filter((m) => m !== firstUser && !recentSet.has(m));
139
+ if (!middle.length)
140
+ return withLead(firstUser ? [firstUser, ...recent] : recent);
141
+ let summary;
142
+ try {
143
+ summary = (await summarize(messagesToText(middle))).trim();
144
+ }
145
+ catch {
146
+ return autoCompact(messages, tokenLimit, keepRecent); // ย่อไม่ได้ → กลับไป truncate (ไม่บล็อกงาน)
147
+ }
148
+ if (!summary)
149
+ return autoCompact(messages, tokenLimit, keepRecent);
150
+ const summaryMsg = {
151
+ role: 'user',
152
+ content: `[สรุปบทสนทนาก่อนหน้า (ย่อเพื่อประหยัด context)]\n${summary}`,
153
+ };
154
+ const tail = firstUser ? [firstUser, summaryMsg, ...recent] : [summaryMsg, ...recent];
155
+ return withLead(tail);
85
156
  }