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,402 @@
1
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { inlineValue, takeValue } from './cli-option-values.js';
5
+ const TEMPLATE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', 'second-brain');
6
+ export const BRAIN_NOTE_TYPES = {
7
+ session: {
8
+ destDir: 'Sessions',
9
+ templateCandidates: ['Templates/session.md'],
10
+ defaultRelPath: ({ today, slug }) => `Sessions/${today}-${slug}.md`,
11
+ titlePlaceholders: ['<topic>', '<task/topic>'],
12
+ },
13
+ bug: {
14
+ destDir: 'Bugs',
15
+ templateCandidates: ['Templates/bug.md'],
16
+ defaultRelPath: ({ today, slug }) => `Bugs/${today}-${slug}.md`,
17
+ titlePlaceholders: ['<bug>'],
18
+ },
19
+ handoff: {
20
+ destDir: 'Handoffs',
21
+ templateCandidates: ['Templates/handoff.md'],
22
+ defaultRelPath: ({ slug }) => `Handoffs/${slug}-handoff.md`,
23
+ titlePlaceholders: ['<project>'],
24
+ },
25
+ project: {
26
+ destDir: 'Projects',
27
+ templateCandidates: ['Templates/project-workspace/overview.md', 'Templates/project.md'],
28
+ defaultRelPath: ({ slug }) => `Projects/${slug}/overview.md`,
29
+ titlePlaceholders: ['<Project Name>', '{{TITLE}}'],
30
+ },
31
+ 'golden-case': {
32
+ destDir: 'Acceptance',
33
+ templateCandidates: ['Templates/golden-case.md', 'Acceptance/golden-case-template.md'],
34
+ defaultRelPath: ({ slug }) => `Acceptance/${slug}.md`,
35
+ titlePlaceholders: ['Golden Case Template', '<input>', '<expected>'],
36
+ },
37
+ checklist: {
38
+ destDir: 'Checklists',
39
+ templateCandidates: ['Templates/checklist.md', 'Checklists/preflight-postflight-template.md'],
40
+ defaultRelPath: ({ slug }) => `Checklists/${slug}-checklist.md`,
41
+ titlePlaceholders: ['Preflight / Postflight Checklist Template'],
42
+ },
43
+ };
44
+ export function isBrainNoteType(value) {
45
+ return Object.hasOwn(BRAIN_NOTE_TYPES, value);
46
+ }
47
+ export function parseBrainNewArgs(args) {
48
+ const [typeArg, ...rest] = args;
49
+ if (!typeArg)
50
+ return { ok: false, message: 'ต้องระบุ note type' };
51
+ if (!isBrainNoteType(typeArg)) {
52
+ return { ok: false, message: `ไม่รู้จัก note type: ${typeArg}` };
53
+ }
54
+ const parsed = { type: typeArg, force: false };
55
+ const positional = [];
56
+ for (let i = 0; i < rest.length; i++) {
57
+ const arg = rest[i];
58
+ if (arg === '--force') {
59
+ parsed.force = true;
60
+ }
61
+ else if (arg === '--title' || arg.startsWith('--title=')) {
62
+ const next = arg === '--title' ? takeValue(rest, i) : undefined;
63
+ const value = next ? next.value : inlineValue('--title', arg);
64
+ if (next)
65
+ i = next.nextIndex;
66
+ if (!value?.trim())
67
+ return { ok: false, message: 'ต้องระบุค่าให้ --title' };
68
+ if (parsed.title)
69
+ return { ok: false, message: 'ระบุ title ได้ครั้งเดียว' };
70
+ parsed.title = value.trim();
71
+ }
72
+ else if (arg === '--repo' || arg.startsWith('--repo=')) {
73
+ const next = arg === '--repo' ? takeValue(rest, i) : undefined;
74
+ const value = next ? next.value : inlineValue('--repo', arg);
75
+ if (next)
76
+ i = next.nextIndex;
77
+ if (!value?.trim())
78
+ return { ok: false, message: 'ต้องระบุค่าให้ --repo' };
79
+ if (parsed.repo)
80
+ return { ok: false, message: 'ระบุ repo ได้ครั้งเดียว' };
81
+ parsed.repo = value.trim();
82
+ }
83
+ else if (arg === '--verify' || arg.startsWith('--verify=')) {
84
+ const next = arg === '--verify' ? takeValue(rest, i) : undefined;
85
+ const value = next ? next.value : inlineValue('--verify', arg);
86
+ if (next)
87
+ i = next.nextIndex;
88
+ if (!value?.trim())
89
+ return { ok: false, message: 'ต้องระบุค่าให้ --verify' };
90
+ if (parsed.verify)
91
+ return { ok: false, message: 'ระบุ verify ได้ครั้งเดียว' };
92
+ parsed.verify = value.trim();
93
+ }
94
+ else if (arg === '--default-branch' || arg.startsWith('--default-branch=')) {
95
+ const next = arg === '--default-branch' ? takeValue(rest, i) : undefined;
96
+ const value = next ? next.value : inlineValue('--default-branch', arg);
97
+ if (next)
98
+ i = next.nextIndex;
99
+ if (!value?.trim())
100
+ return { ok: false, message: 'ต้องระบุค่าให้ --default-branch' };
101
+ if (parsed.defaultBranch)
102
+ return { ok: false, message: 'ระบุ default-branch ได้ครั้งเดียว' };
103
+ parsed.defaultBranch = value.trim();
104
+ }
105
+ else if (arg === '--output') {
106
+ const next = takeValue(rest, i);
107
+ const value = next.value;
108
+ i = next.nextIndex;
109
+ if (!value?.trim())
110
+ return { ok: false, message: 'ต้องระบุค่าให้ --output' };
111
+ if (parsed.output !== undefined)
112
+ return { ok: false, message: 'ระบุ output ได้ครั้งเดียว' };
113
+ parsed.output = value.trim();
114
+ }
115
+ else if (arg.startsWith('--output=')) {
116
+ const value = arg.slice('--output='.length).trim();
117
+ if (!value)
118
+ return { ok: false, message: 'ต้องระบุค่าให้ --output' };
119
+ if (parsed.output !== undefined)
120
+ return { ok: false, message: 'ระบุ output ได้ครั้งเดียว' };
121
+ parsed.output = value;
122
+ }
123
+ else if (arg === '--') {
124
+ positional.push(...rest.slice(i + 1));
125
+ break;
126
+ }
127
+ else if (arg.startsWith('-')) {
128
+ return { ok: false, message: `ไม่รู้จัก option: ${arg}` };
129
+ }
130
+ else {
131
+ positional.push(arg);
132
+ }
133
+ }
134
+ const positionalTitle = positional.join(' ').trim();
135
+ if (parsed.title && positionalTitle)
136
+ return { ok: false, message: 'ระบุ title ได้ครั้งเดียว: ใช้ positional หรือ --title อย่างใดอย่างหนึ่ง' };
137
+ if (positionalTitle)
138
+ parsed.title = positionalTitle;
139
+ return { ok: true, value: parsed };
140
+ }
141
+ export function inferParentRelPath(relPath) {
142
+ const normalized = relPath.replace(/\\/g, '/');
143
+ const parts = normalized.split('/');
144
+ parts.pop();
145
+ if (!parts.length)
146
+ return 'Home';
147
+ return `${parts.join('/')}/_Index`;
148
+ }
149
+ export function destinationIndexRelPath(type, relPath) {
150
+ const config = BRAIN_NOTE_TYPES[type];
151
+ const normalized = relPath.replace(/\\/g, '/');
152
+ if (normalized.startsWith(`${config.destDir}/`) && normalized.includes('/')) {
153
+ const nested = normalized.slice(config.destDir.length + 1);
154
+ if (nested.includes('/')) {
155
+ const subDir = nested.split('/')[0];
156
+ return `${config.destDir}/${subDir}/_Index`;
157
+ }
158
+ }
159
+ return `${config.destDir}/_Index`;
160
+ }
161
+ export function validateNoteOutputPath(type, relPath) {
162
+ const config = BRAIN_NOTE_TYPES[type];
163
+ const normalized = relPath.replace(/\\/g, '/');
164
+ if (normalized.endsWith('/_Index.md') || normalized === '_Index.md') {
165
+ return { ok: false, message: 'Cannot create a note at an _Index.md path.' };
166
+ }
167
+ if (!normalized.startsWith(`${config.destDir}/`)) {
168
+ return { ok: false, message: `${type} notes must stay under ${config.destDir}/.` };
169
+ }
170
+ return { ok: true };
171
+ }
172
+ export function instantiateNoteTemplate(raw, options) {
173
+ let content = raw.replaceAll('YYYY-MM-DD', options.today).replaceAll('{{DATE}}', options.today);
174
+ const config = BRAIN_NOTE_TYPES[options.type];
175
+ for (const placeholder of config.titlePlaceholders) {
176
+ content = content.replaceAll(placeholder, options.title);
177
+ }
178
+ content = content.replace(/^# .+$/m, (heading) => {
179
+ if (heading.includes(options.title))
180
+ return heading;
181
+ if (options.type === 'session')
182
+ return `# ${options.today} — ${options.title}`;
183
+ if (options.type === 'bug')
184
+ return `# ${options.today} — ${options.title}`;
185
+ if (options.type === 'handoff')
186
+ return `# ${options.title} — Handoff`;
187
+ if (options.type === 'project')
188
+ return `# ${options.title}`;
189
+ if (options.type === 'golden-case')
190
+ return `# ${options.title}`;
191
+ if (options.type === 'checklist')
192
+ return `# ${options.title}`;
193
+ return heading;
194
+ });
195
+ const parentValue = `"[[${options.parent}]]"`;
196
+ if (/^parent:/m.test(content)) {
197
+ content = content.replace(/^parent:.*$/m, `parent: ${parentValue}`);
198
+ }
199
+ else if (/^---[\s\S]*?---/m.test(content)) {
200
+ content = content.replace(/^---\n/m, `---\nparent: ${parentValue}\n`);
201
+ }
202
+ const upLink = `up:: [[${options.parent}]]`;
203
+ if (content.includes('up:: [['))
204
+ content = content.replace(/^up:: \[\[[^\]]+\]\]\s*$/m, upLink);
205
+ else
206
+ content = `${content.trimEnd()}\n\n${upLink}\n`;
207
+ if (options.type === 'golden-case')
208
+ content = content.replace(/^note_type:\s*template/m, 'note_type: golden-case');
209
+ if (options.type === 'checklist')
210
+ content = content.replace(/^note_type:\s*template/m, 'note_type: checklist');
211
+ content = content.replace(/^tags: \[template,/m, 'tags: [');
212
+ return content;
213
+ }
214
+ export async function createBrainNote(options) {
215
+ const title = (options.title ?? defaultTitleForType(options.type)).trim() || defaultTitleForType(options.type);
216
+ const warnings = [];
217
+ if (!options.brainPath) {
218
+ return { ok: false, title, indexed: false, warnings: ['No second-brain path is configured.'] };
219
+ }
220
+ const brainPath = resolve(options.brainPath);
221
+ if (!(await pathExistsAsDir(brainPath))) {
222
+ return { ok: false, brainPath, title, indexed: false, warnings: ['Configured second-brain path does not exist or is not a directory.'] };
223
+ }
224
+ const today = options.today ?? new Date().toISOString().slice(0, 10);
225
+ if (options.type === 'project' && !options.output) {
226
+ const { scaffoldProjectWorkspace } = await import('./project-scaffold.js');
227
+ const scaffold = await scaffoldProjectWorkspace({
228
+ brainPath,
229
+ title,
230
+ repoPath: options.repo,
231
+ verify: options.verify,
232
+ defaultBranch: options.defaultBranch,
233
+ today,
234
+ force: options.force,
235
+ });
236
+ return {
237
+ ok: scaffold.ok,
238
+ brainPath,
239
+ type: 'project',
240
+ title: scaffold.title,
241
+ template: 'Templates/project-workspace/*',
242
+ path: join(brainPath, scaffold.relDir),
243
+ relPath: `${scaffold.relDir}/overview.md`,
244
+ indexed: scaffold.indexed,
245
+ warnings: [
246
+ ...scaffold.warnings,
247
+ ...(scaffold.created.length ? [`created ${scaffold.created.length} file(s) under ${scaffold.relDir}/`] : []),
248
+ ],
249
+ };
250
+ }
251
+ const slug = slugify(title);
252
+ const config = BRAIN_NOTE_TYPES[options.type];
253
+ const relPath = normalizeRelPath(options.output ?? config.defaultRelPath({ today, slug }));
254
+ const valid = validateNoteOutputPath(options.type, relPath);
255
+ if (!valid.ok) {
256
+ return { ok: false, brainPath, type: options.type, title, indexed: false, warnings: [valid.message] };
257
+ }
258
+ const destIndexRel = destinationIndexRelPath(options.type, relPath);
259
+ const indexPath = join(brainPath, `${destIndexRel}.md`);
260
+ if (!(await fileExists(indexPath))) {
261
+ warnings.push(`Destination index missing: ${destIndexRel}.md`);
262
+ }
263
+ else {
264
+ await readFile(indexPath, 'utf8');
265
+ }
266
+ const outputPath = resolve(isAbsolute(relPath) ? relPath : join(brainPath, relPath));
267
+ const insideVault = !relative(brainPath, outputPath).startsWith('..') && !isAbsolute(relative(brainPath, outputPath));
268
+ if (!insideVault) {
269
+ return { ok: false, brainPath, type: options.type, title, indexed: false, warnings: ['--output must stay inside the configured second-brain vault.'] };
270
+ }
271
+ if ((await fileExists(outputPath)) && !options.force) {
272
+ return {
273
+ ok: false,
274
+ brainPath,
275
+ type: options.type,
276
+ title,
277
+ path: outputPath,
278
+ relPath,
279
+ indexed: false,
280
+ warnings: ['Note already exists. Re-run with --force or choose --output.'],
281
+ };
282
+ }
283
+ const template = await readNoteTemplate(brainPath, options.type);
284
+ if (!template.path) {
285
+ return { ok: false, brainPath, type: options.type, title, indexed: false, warnings: [template.message] };
286
+ }
287
+ const parent = destinationIndexRelPath(options.type, relPath);
288
+ const content = instantiateNoteTemplate(template.content, { today, title, parent, type: options.type });
289
+ await mkdir(dirname(outputPath), { recursive: true });
290
+ await writeFile(outputPath, content, 'utf8');
291
+ const indexed = await maybeAppendDestinationIndex(brainPath, destIndexRel, relPath, title, options.type);
292
+ return {
293
+ ok: true,
294
+ brainPath,
295
+ type: options.type,
296
+ title,
297
+ template: template.path,
298
+ path: outputPath,
299
+ relPath,
300
+ indexed,
301
+ warnings,
302
+ };
303
+ }
304
+ export function formatBrainNewReport(report) {
305
+ const lines = ['Sanook brain new'];
306
+ lines.push(`vault: ${report.brainPath ?? '(not configured)'}`);
307
+ if (report.type)
308
+ lines.push(`type: ${report.type}`);
309
+ lines.push(`title: ${report.title}`);
310
+ if (report.template)
311
+ lines.push(`template: ${report.template}`);
312
+ if (report.path)
313
+ lines.push(`created: ${report.path}`);
314
+ if (report.relPath)
315
+ lines.push(`link: [[${report.relPath.replace(/\.md$/i, '')}]]`);
316
+ if (report.indexed)
317
+ lines.push('index: updated');
318
+ for (const warning of report.warnings)
319
+ lines.push(`warning: ${warning}`);
320
+ return lines.join('\n');
321
+ }
322
+ function defaultTitleForType(type) {
323
+ switch (type) {
324
+ case 'session':
325
+ return 'session note';
326
+ case 'bug':
327
+ return 'bug report';
328
+ case 'handoff':
329
+ return 'project handoff';
330
+ case 'project':
331
+ return 'project overview';
332
+ case 'golden-case':
333
+ return 'golden case';
334
+ case 'checklist':
335
+ return 'preflight postflight';
336
+ }
337
+ }
338
+ async function readNoteTemplate(brainPath, type) {
339
+ for (const candidate of BRAIN_NOTE_TYPES[type].templateCandidates) {
340
+ const vaultPath = join(brainPath, candidate);
341
+ const fromVault = await readText(vaultPath);
342
+ if (fromVault)
343
+ return { path: candidate, content: fromVault, message: '' };
344
+ const bundledPath = join(TEMPLATE_ROOT, candidate);
345
+ const fromBundled = await readText(bundledPath);
346
+ if (fromBundled)
347
+ return { path: candidate, content: fromBundled, message: '' };
348
+ }
349
+ return { content: '', message: `No template found for ${type}.` };
350
+ }
351
+ async function maybeAppendDestinationIndex(brainPath, indexRel, noteRel, title, type) {
352
+ const indexPath = join(brainPath, `${indexRel}.md`);
353
+ const content = await readText(indexPath);
354
+ if (!content)
355
+ return false;
356
+ const note = noteRel.replace(/\.md$/i, '');
357
+ const link = `[[${note}]]`;
358
+ if (content.includes(link))
359
+ return false;
360
+ const line = `- ${link} — ${type}: ${title}`;
361
+ const marker = '\nup:: [[Home]]';
362
+ const next = content.includes(marker) ? content.replace(marker, `\n${line}\n${marker}`) : `${content.trimEnd()}\n${line}\n`;
363
+ await writeFile(indexPath, next, 'utf8');
364
+ return true;
365
+ }
366
+ function slugify(value) {
367
+ const slug = value
368
+ .normalize('NFKD')
369
+ .toLowerCase()
370
+ .replace(/[^\p{Letter}\p{Number}]+/gu, '-')
371
+ .replace(/^-+|-+$/g, '')
372
+ .slice(0, 80)
373
+ .replace(/-+$/g, '');
374
+ return slug || 'note';
375
+ }
376
+ function normalizeRelPath(path) {
377
+ return path.replace(/\\/g, '/').replace(/^\/+/, '');
378
+ }
379
+ async function pathExistsAsDir(path) {
380
+ try {
381
+ return (await stat(path)).isDirectory();
382
+ }
383
+ catch {
384
+ return false;
385
+ }
386
+ }
387
+ async function fileExists(path) {
388
+ try {
389
+ return (await stat(path)).isFile();
390
+ }
391
+ catch {
392
+ return false;
393
+ }
394
+ }
395
+ async function readText(path) {
396
+ try {
397
+ return await readFile(path, 'utf8');
398
+ }
399
+ catch {
400
+ return '';
401
+ }
402
+ }
@@ -0,0 +1,210 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const CONTEXT_PACKS_DIR = join('Shared', 'Context-Packs');
4
+ export function parseBrainPackArgs(args) {
5
+ const [action, ...rest] = args;
6
+ if (action === 'list') {
7
+ if (rest.length)
8
+ return { ok: false, message: `ไม่รู้จัก option: ${rest.join(' ')}` };
9
+ return { ok: true, action: 'list' };
10
+ }
11
+ if (action === 'show') {
12
+ const name = rest.join(' ').trim();
13
+ if (!name)
14
+ return { ok: false, message: 'ต้องระบุชื่อ context pack' };
15
+ return { ok: true, action: 'show', name };
16
+ }
17
+ if (!action)
18
+ return { ok: false, message: 'ต้องระบุ subcommand: list หรือ show <name>' };
19
+ return { ok: false, message: `ไม่รู้จัก subcommand: ${action}` };
20
+ }
21
+ export function normalizePackName(name) {
22
+ return name
23
+ .trim()
24
+ .replace(/^Shared\/Context-Packs\//i, '')
25
+ .replace(/\.md$/i, '');
26
+ }
27
+ export function extractPackDescription(indexContent, packName) {
28
+ const link = `[[Shared/Context-Packs/${packName}]]`;
29
+ for (const line of indexContent.split('\n')) {
30
+ const trimmed = line.trim();
31
+ if (!trimmed.startsWith('- ') || !trimmed.includes(link))
32
+ continue;
33
+ const dash = trimmed.indexOf('—');
34
+ if (dash >= 0)
35
+ return trimmed.slice(dash + 1).trim();
36
+ const hyphen = trimmed.indexOf(' - ');
37
+ if (hyphen >= 0)
38
+ return trimmed.slice(hyphen + 3).trim();
39
+ }
40
+ return undefined;
41
+ }
42
+ export function extractBlockquotePurpose(content) {
43
+ const body = content.replace(/^---[\s\S]*?---\n?/, '');
44
+ const match = body.match(/^>\s*(.+)$/m);
45
+ return match?.[1]?.trim() ?? '';
46
+ }
47
+ export function extractSectionBullets(content, heading) {
48
+ const lines = content.split('\n');
49
+ const start = lines.findIndex((line) => line.trim().toLowerCase() === `## ${heading.toLowerCase()}`);
50
+ if (start < 0)
51
+ return [];
52
+ const out = [];
53
+ for (const line of lines.slice(start + 1)) {
54
+ if (/^#{1,6}\s+/.test(line.trim()))
55
+ break;
56
+ const trimmed = line.trim();
57
+ if (trimmed.startsWith('- '))
58
+ out.push(trimmed.slice(2).trim());
59
+ else {
60
+ const numbered = trimmed.match(/^\d+\.\s+(.+)$/);
61
+ if (numbered)
62
+ out.push(numbered[1].trim());
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ export function extractWikiLinks(value) {
68
+ return [...value.matchAll(/\[\[([^\]]+)\]\]/g)].map((match) => match[1].trim());
69
+ }
70
+ export function buildContextPackSummary(packName, content, indexContent) {
71
+ const relPath = `${CONTEXT_PACKS_DIR}/${packName}.md`;
72
+ const link = `[[Shared/Context-Packs/${packName}]]`;
73
+ return {
74
+ name: packName,
75
+ relPath,
76
+ description: extractPackDescription(indexContent, packName) || extractBlockquotePurpose(content),
77
+ indexed: indexContent.includes(link),
78
+ hasLoadOrder: content.includes('## Load Order'),
79
+ hasDoneCriteria: content.includes('## Done Criteria'),
80
+ };
81
+ }
82
+ export function buildContextPackDetail(packName, content, indexContent) {
83
+ const summary = buildContextPackSummary(packName, content, indexContent);
84
+ const loadOrder = extractSectionBullets(content, 'Load Order');
85
+ const doneCriteria = extractSectionBullets(content, 'Done Criteria');
86
+ const roleLines = extractSectionBullets(content, 'Required Role');
87
+ return {
88
+ ...summary,
89
+ useCase: extractBlockquotePurpose(content),
90
+ loadOrder,
91
+ doneCriteria,
92
+ requiredRole: roleLines.length ? roleLines.join(' · ') : undefined,
93
+ sources: uniqueSorted(loadOrder.flatMap(extractWikiLinks)),
94
+ };
95
+ }
96
+ export async function listContextPacks(brainPath) {
97
+ const warnings = [];
98
+ if (!(await pathExistsAsDir(brainPath))) {
99
+ return { ok: false, packs: [], warnings: ['Configured second-brain path does not exist or is not a directory.'] };
100
+ }
101
+ const dir = join(brainPath, CONTEXT_PACKS_DIR);
102
+ const indexPath = join(dir, '_Index.md');
103
+ const indexContent = await readText(indexPath);
104
+ if (!indexContent)
105
+ warnings.push('Context-Packs index is missing.');
106
+ let entries;
107
+ try {
108
+ entries = await readdir(dir, { withFileTypes: true });
109
+ }
110
+ catch {
111
+ return { ok: false, brainPath, packs: [], warnings: ['Context-Packs directory is missing.'] };
112
+ }
113
+ const packs = entries
114
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md') && entry.name !== '_Index.md')
115
+ .map((entry) => normalizePackName(entry.name))
116
+ .sort((a, b) => a.localeCompare(b));
117
+ const summaries = [];
118
+ for (const name of packs) {
119
+ const content = await readText(join(dir, `${name}.md`));
120
+ if (!content) {
121
+ warnings.push(`Unreadable context pack: ${name}.md`);
122
+ continue;
123
+ }
124
+ summaries.push(buildContextPackSummary(name, content, indexContent));
125
+ }
126
+ return { ok: true, brainPath, packs: summaries, warnings };
127
+ }
128
+ export async function showContextPack(brainPath, rawName) {
129
+ const warnings = [];
130
+ const name = normalizePackName(rawName);
131
+ if (!(await pathExistsAsDir(brainPath))) {
132
+ return { ok: false, warnings: ['Configured second-brain path does not exist or is not a directory.'] };
133
+ }
134
+ const dir = join(brainPath, CONTEXT_PACKS_DIR);
135
+ const packPath = join(dir, `${name}.md`);
136
+ const content = await readText(packPath);
137
+ if (!content) {
138
+ return { ok: false, brainPath, warnings: [`Context pack not found: ${name}`] };
139
+ }
140
+ const indexContent = await readText(join(dir, '_Index.md'));
141
+ if (!indexContent)
142
+ warnings.push('Context-Packs index is missing.');
143
+ const pack = buildContextPackDetail(name, content, indexContent);
144
+ if (!pack.indexed)
145
+ warnings.push(`${name} is not linked from Context-Packs/_Index.md.`);
146
+ return { ok: true, brainPath, pack, warnings };
147
+ }
148
+ export function formatBrainPackListReport(report) {
149
+ const lines = ['Sanook brain pack list', `vault: ${report.brainPath ?? '(not configured)'}`];
150
+ lines.push(`packs: ${report.packs.length}`);
151
+ for (const pack of report.packs) {
152
+ const flags = [
153
+ pack.indexed ? 'indexed' : 'missing-index-link',
154
+ pack.hasLoadOrder ? 'load-order' : 'no-load-order',
155
+ pack.hasDoneCriteria ? 'done-criteria' : 'no-done-criteria',
156
+ ].join(', ');
157
+ lines.push(`- ${pack.name} — ${pack.description || '(no description)'} [${flags}]`);
158
+ lines.push(` ${pack.relPath}`);
159
+ }
160
+ for (const warning of report.warnings)
161
+ lines.push(`warning: ${warning}`);
162
+ return lines.join('\n');
163
+ }
164
+ export function formatBrainPackShowReport(report) {
165
+ const lines = ['Sanook brain pack show', `vault: ${report.brainPath ?? '(not configured)'}`];
166
+ if (!report.pack) {
167
+ for (const warning of report.warnings)
168
+ lines.push(`warning: ${warning}`);
169
+ return lines.join('\n');
170
+ }
171
+ const pack = report.pack;
172
+ lines.push(`name: ${pack.name}`);
173
+ lines.push(`path: ${pack.relPath}`);
174
+ lines.push(`use-case: ${pack.useCase || '(none)'}`);
175
+ if (pack.requiredRole)
176
+ lines.push(`role: ${pack.requiredRole}`);
177
+ lines.push('load-order:');
178
+ for (const item of pack.loadOrder.length ? pack.loadOrder : ['(missing ## Load Order section)'])
179
+ lines.push(` - ${item}`);
180
+ lines.push('done-criteria:');
181
+ for (const item of pack.doneCriteria.length ? pack.doneCriteria : ['(missing ## Done Criteria section)'])
182
+ lines.push(` - ${item}`);
183
+ if (pack.sources.length) {
184
+ lines.push('sources:');
185
+ for (const source of pack.sources)
186
+ lines.push(` - [[${source}]]`);
187
+ }
188
+ for (const warning of report.warnings)
189
+ lines.push(`warning: ${warning}`);
190
+ return lines.join('\n');
191
+ }
192
+ async function pathExistsAsDir(path) {
193
+ try {
194
+ return (await stat(path)).isDirectory();
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ }
200
+ async function readText(path) {
201
+ try {
202
+ return await readFile(path, 'utf8');
203
+ }
204
+ catch {
205
+ return '';
206
+ }
207
+ }
208
+ function uniqueSorted(values) {
209
+ return [...new Set(values.filter(Boolean))].sort((a, b) => a.localeCompare(b));
210
+ }