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.
- package/.env.example +161 -3
- package/CHANGELOG.md +148 -10
- package/README.md +255 -26
- package/README.th.md +95 -7
- package/dist/approval.js +13 -0
- package/dist/bin.js +3552 -155
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +262 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +377 -0
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +15 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +190 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +266 -27
- package/dist/compaction.js +96 -11
- package/dist/config.js +149 -33
- package/dist/context-compression.js +191 -0
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +49 -15
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +49 -9
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +399 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +501 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +38 -1
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +362 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +55 -0
- package/dist/insights.js +86 -0
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +157 -29
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +494 -0
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +120 -10
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +148 -37
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +51 -19
- package/dist/personality.js +58 -0
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +89 -43
- package/dist/providers/keys.js +22 -1
- package/dist/providers/models.js +2 -2
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +83 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session-distill.js +84 -0
- package/dist/session.js +92 -16
- package/dist/skill-install.js +53 -13
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +206 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +992 -12
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/read.js +16 -4
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +226 -15
- package/dist/tools/task.js +40 -9
- package/dist/tools/timeout.js +23 -3
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/trust.js +11 -1
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +878 -32
- package/dist/ui/banner.js +78 -4
- package/dist/ui/history.js +37 -5
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +20 -1
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +172 -46
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +56 -17
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/dist/worktree.js +175 -4
- package/package.json +5 -5
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +19 -4
- package/second-brain/Projects/sanook-cli/_Index.md +30 -0
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +8 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -4
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, posix, win32 } from 'node:path';
|
|
3
|
+
import { appHomePath } from './brand.js';
|
|
4
|
+
import { FOLDERS } from './brain.js';
|
|
5
|
+
import { INDEX_PATH } from './search/store.js';
|
|
6
|
+
export const BRAIN_HOT_FILES = [
|
|
7
|
+
'SANOOK.md',
|
|
8
|
+
'Shared/AI-Context-Index.md',
|
|
9
|
+
'Vault Structure Map.md',
|
|
10
|
+
'Shared/Operating-State/current-state.md',
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_INDEX_FRESHNESS_TOLERANCE_MS = 1000;
|
|
13
|
+
const SKIP_DIRS = new Set(['.git', '.obsidian', 'node_modules']);
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function normalizeSlashes(path) {
|
|
18
|
+
return path.trim().replace(/\\/g, '/');
|
|
19
|
+
}
|
|
20
|
+
function normalizeFolderReference(path) {
|
|
21
|
+
const normalized = posix
|
|
22
|
+
.normalize(normalizeSlashes(path))
|
|
23
|
+
.replace(/^\/+/, '')
|
|
24
|
+
.replace(/\/+$/, '');
|
|
25
|
+
return normalized === '.' ? '' : normalized;
|
|
26
|
+
}
|
|
27
|
+
function isVaultRelativeFolderReference(path) {
|
|
28
|
+
const trimmed = path.trim();
|
|
29
|
+
const slashed = normalizeSlashes(trimmed);
|
|
30
|
+
const hasWindowsDriveSpecifier = /^[A-Za-z]:/.test(trimmed);
|
|
31
|
+
return (!posix.isAbsolute(slashed) &&
|
|
32
|
+
!win32.isAbsolute(trimmed) &&
|
|
33
|
+
!hasWindowsDriveSpecifier &&
|
|
34
|
+
!slashed.split('/').includes('..'));
|
|
35
|
+
}
|
|
36
|
+
function normalizeExpectedFolders(expectedFolders) {
|
|
37
|
+
const folders = new Set();
|
|
38
|
+
const invalid = new Set();
|
|
39
|
+
for (const dir of expectedFolders) {
|
|
40
|
+
const slashed = normalizeSlashes(dir);
|
|
41
|
+
if (!slashed)
|
|
42
|
+
continue;
|
|
43
|
+
const folder = normalizeFolderReference(dir);
|
|
44
|
+
if (!isVaultRelativeFolderReference(dir)) {
|
|
45
|
+
invalid.add(slashed.replace(/\/+$/, ''));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!folder)
|
|
49
|
+
continue;
|
|
50
|
+
folders.add(folder);
|
|
51
|
+
}
|
|
52
|
+
return { folders: [...folders], invalid: [...invalid] };
|
|
53
|
+
}
|
|
54
|
+
function extractFolderReferences(map) {
|
|
55
|
+
const references = new Set();
|
|
56
|
+
for (const match of map.matchAll(/`([^`]+)`/g)) {
|
|
57
|
+
if (!isVaultRelativeFolderReference(match[1]))
|
|
58
|
+
continue;
|
|
59
|
+
const normalized = normalizeFolderReference(match[1]);
|
|
60
|
+
if (normalized)
|
|
61
|
+
references.add(normalized);
|
|
62
|
+
}
|
|
63
|
+
return references;
|
|
64
|
+
}
|
|
65
|
+
async function fileExists(path) {
|
|
66
|
+
try {
|
|
67
|
+
return (await stat(path)).isFile();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function directoryExists(path) {
|
|
74
|
+
try {
|
|
75
|
+
return (await stat(path)).isDirectory();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function latestMarkdownMtimeMs(root) {
|
|
82
|
+
let latest = 0;
|
|
83
|
+
async function walk(dir) {
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
94
|
+
await walk(join(dir, entry.name));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
latest = Math.max(latest, (await stat(join(dir, entry.name))).mtimeMs);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// File disappeared between readdir and stat; ignore the race.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await walk(root);
|
|
108
|
+
return latest;
|
|
109
|
+
}
|
|
110
|
+
export async function checkBrainHotFiles(brainPath) {
|
|
111
|
+
const missing = [];
|
|
112
|
+
for (const rel of BRAIN_HOT_FILES) {
|
|
113
|
+
if (!(await fileExists(join(brainPath, rel))))
|
|
114
|
+
missing.push(rel);
|
|
115
|
+
}
|
|
116
|
+
if (missing.length) {
|
|
117
|
+
return {
|
|
118
|
+
id: 'brain.hot-files',
|
|
119
|
+
status: 'fail',
|
|
120
|
+
message: `Missing ${missing.length} required second-brain file${missing.length === 1 ? '' : 's'}.`,
|
|
121
|
+
path: brainPath,
|
|
122
|
+
details: missing,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
id: 'brain.hot-files',
|
|
127
|
+
status: 'pass',
|
|
128
|
+
message: 'Required second-brain hot files are present.',
|
|
129
|
+
path: brainPath,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export async function checkBrainFolders(brainPath, expectedFolders = FOLDERS.map((f) => f.dir)) {
|
|
133
|
+
const expected = normalizeExpectedFolders(expectedFolders);
|
|
134
|
+
if (expected.invalid.length) {
|
|
135
|
+
return {
|
|
136
|
+
id: 'brain.folders',
|
|
137
|
+
status: 'fail',
|
|
138
|
+
message: `Invalid expected second-brain folder reference${expected.invalid.length === 1 ? '' : 's'}.`,
|
|
139
|
+
path: brainPath,
|
|
140
|
+
details: expected.invalid,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const missing = [];
|
|
144
|
+
for (const dir of expected.folders) {
|
|
145
|
+
if (!(await directoryExists(join(brainPath, dir))))
|
|
146
|
+
missing.push(dir);
|
|
147
|
+
}
|
|
148
|
+
if (missing.length) {
|
|
149
|
+
return {
|
|
150
|
+
id: 'brain.folders',
|
|
151
|
+
status: 'fail',
|
|
152
|
+
message: `Missing ${missing.length} expected second-brain folder${missing.length === 1 ? '' : 's'}.`,
|
|
153
|
+
path: brainPath,
|
|
154
|
+
details: missing,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
id: 'brain.folders',
|
|
159
|
+
status: 'pass',
|
|
160
|
+
message: 'Expected second-brain folders are present.',
|
|
161
|
+
path: brainPath,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export async function checkVaultStructureMap(brainPath, expectedFolders = FOLDERS.map((f) => f.dir)) {
|
|
165
|
+
const mapPath = join(brainPath, 'Vault Structure Map.md');
|
|
166
|
+
let map;
|
|
167
|
+
try {
|
|
168
|
+
map = await readFile(mapPath, 'utf8');
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return {
|
|
172
|
+
id: 'brain.structure-map',
|
|
173
|
+
status: 'fail',
|
|
174
|
+
message: 'Vault Structure Map.md is missing or unreadable.',
|
|
175
|
+
path: mapPath,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const expected = normalizeExpectedFolders(expectedFolders);
|
|
179
|
+
if (expected.invalid.length) {
|
|
180
|
+
return {
|
|
181
|
+
id: 'brain.structure-map',
|
|
182
|
+
status: 'fail',
|
|
183
|
+
message: `Invalid expected second-brain folder reference${expected.invalid.length === 1 ? '' : 's'}.`,
|
|
184
|
+
path: mapPath,
|
|
185
|
+
details: expected.invalid,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const folderReferences = extractFolderReferences(map);
|
|
189
|
+
const missing = expected.folders.filter((dir) => !folderReferences.has(dir));
|
|
190
|
+
if (missing.length) {
|
|
191
|
+
return {
|
|
192
|
+
id: 'brain.structure-map',
|
|
193
|
+
status: 'fail',
|
|
194
|
+
message: `Vault Structure Map.md is missing ${missing.length} folder reference${missing.length === 1 ? '' : 's'}.`,
|
|
195
|
+
path: mapPath,
|
|
196
|
+
details: missing,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
id: 'brain.structure-map',
|
|
201
|
+
status: 'pass',
|
|
202
|
+
message: 'Vault Structure Map.md covers the expected folder manifest.',
|
|
203
|
+
path: mapPath,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export async function checkSearchIndexFreshness(brainPath, indexPath = INDEX_PATH, toleranceMs = DEFAULT_INDEX_FRESHNESS_TOLERANCE_MS) {
|
|
207
|
+
const latestVaultMtimeMs = await latestMarkdownMtimeMs(brainPath);
|
|
208
|
+
if (latestVaultMtimeMs === 0) {
|
|
209
|
+
return {
|
|
210
|
+
id: 'brain.search-index',
|
|
211
|
+
status: 'warn',
|
|
212
|
+
message: 'No markdown files were found in the configured second-brain vault.',
|
|
213
|
+
path: brainPath,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
let indexMtimeMs = 0;
|
|
217
|
+
try {
|
|
218
|
+
indexMtimeMs = (await stat(indexPath)).mtimeMs;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return {
|
|
222
|
+
id: 'brain.search-index',
|
|
223
|
+
status: 'warn',
|
|
224
|
+
message: 'Search index is missing; run `sanook index` to build it.',
|
|
225
|
+
path: indexPath,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (indexMtimeMs + toleranceMs < latestVaultMtimeMs) {
|
|
229
|
+
return {
|
|
230
|
+
id: 'brain.search-index',
|
|
231
|
+
status: 'warn',
|
|
232
|
+
message: 'Search index is older than the second-brain markdown files.',
|
|
233
|
+
path: indexPath,
|
|
234
|
+
details: [`index_mtime_ms=${Math.round(indexMtimeMs)}`, `vault_latest_mtime_ms=${Math.round(latestVaultMtimeMs)}`],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
id: 'brain.search-index',
|
|
239
|
+
status: 'pass',
|
|
240
|
+
message: 'Search index is present and fresh enough for the vault.',
|
|
241
|
+
path: indexPath,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
export async function checkBrainMcpWiring(brainPath, mcpConfigPath = appHomePath('mcp.json')) {
|
|
245
|
+
let raw;
|
|
246
|
+
try {
|
|
247
|
+
raw = JSON.parse(await readFile(mcpConfigPath, 'utf8'));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return {
|
|
251
|
+
id: 'brain.mcp',
|
|
252
|
+
status: 'warn',
|
|
253
|
+
message: 'MCP config is missing or unreadable; `sanook brain init` can wire the vault.',
|
|
254
|
+
path: mcpConfigPath,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const servers = isRecord(raw) && isRecord(raw.mcpServers) ? raw.mcpServers : undefined;
|
|
258
|
+
const server = servers && isRecord(servers['second-brain']) ? servers['second-brain'] : undefined;
|
|
259
|
+
const args = server && Array.isArray(server.args) ? server.args : [];
|
|
260
|
+
if (!args.includes(brainPath)) {
|
|
261
|
+
return {
|
|
262
|
+
id: 'brain.mcp',
|
|
263
|
+
status: 'warn',
|
|
264
|
+
message: 'MCP server `second-brain` is not wired to the configured vault path.',
|
|
265
|
+
path: mcpConfigPath,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
id: 'brain.mcp',
|
|
270
|
+
status: 'pass',
|
|
271
|
+
message: 'MCP server `second-brain` points at the configured vault path.',
|
|
272
|
+
path: mcpConfigPath,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export async function checkBrain(options = {}) {
|
|
276
|
+
const checks = [];
|
|
277
|
+
const brainPath = options.brainPath;
|
|
278
|
+
if (!brainPath) {
|
|
279
|
+
checks.push({
|
|
280
|
+
id: 'brain.configured',
|
|
281
|
+
status: 'fail',
|
|
282
|
+
message: 'No second-brain path is configured.',
|
|
283
|
+
});
|
|
284
|
+
return { ok: false, checks };
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
if (!(await stat(brainPath)).isDirectory()) {
|
|
288
|
+
checks.push({
|
|
289
|
+
id: 'brain.path',
|
|
290
|
+
status: 'fail',
|
|
291
|
+
message: 'Configured second-brain path is not a directory.',
|
|
292
|
+
path: brainPath,
|
|
293
|
+
});
|
|
294
|
+
return { ok: false, checks };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
checks.push({
|
|
299
|
+
id: 'brain.path',
|
|
300
|
+
status: 'fail',
|
|
301
|
+
message: 'Configured second-brain path does not exist.',
|
|
302
|
+
path: brainPath,
|
|
303
|
+
});
|
|
304
|
+
return { ok: false, checks };
|
|
305
|
+
}
|
|
306
|
+
checks.push({
|
|
307
|
+
id: 'brain.path',
|
|
308
|
+
status: 'pass',
|
|
309
|
+
message: 'Configured second-brain path exists.',
|
|
310
|
+
path: brainPath,
|
|
311
|
+
});
|
|
312
|
+
checks.push(await checkBrainFolders(brainPath, options.expectedFolders));
|
|
313
|
+
checks.push(await checkBrainHotFiles(brainPath));
|
|
314
|
+
checks.push(await checkVaultStructureMap(brainPath, options.expectedFolders));
|
|
315
|
+
checks.push(await checkSearchIndexFreshness(brainPath, options.indexPath, options.indexFreshnessToleranceMs ?? DEFAULT_INDEX_FRESHNESS_TOLERANCE_MS));
|
|
316
|
+
checks.push(await checkBrainMcpWiring(brainPath, options.mcpConfigPath));
|
|
317
|
+
return { ok: !checks.some((check) => check.status === 'fail'), checks };
|
|
318
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { FOLDERS } from './brain.js';
|
|
4
|
+
import { validateFinalGateContent } from './brain-final.js';
|
|
5
|
+
import { checkBrainFolders, checkSearchIndexFreshness, checkVaultStructureMap } from './brain-doctor.js';
|
|
6
|
+
import { inspectBrainContext } from './brain-context.js';
|
|
7
|
+
import { search } from './search/engine.js';
|
|
8
|
+
import { INDEX_PATH } from './search/store.js';
|
|
9
|
+
const STATIC_CASES = ['SB-01', 'SB-02', 'SB-03', 'SB-04', 'SB-05', 'SB-06', 'SB-07', 'SB-08', 'SB-09', 'SB-10'];
|
|
10
|
+
const RETRIEVAL_CASES = [
|
|
11
|
+
{ id: 'RET-01', query: 'memory write protocol merge dont append', expectedPath: 'Shared/Rules/memory-write-protocol.md' },
|
|
12
|
+
{ id: 'RET-02', query: 'context assembly policy small context', expectedPath: 'Shared/Rules/context-assembly-policy.md' },
|
|
13
|
+
{ id: 'RET-03', query: 'quality ledger retrieval hit grounded', expectedPath: 'Evals/quality-ledger.md' },
|
|
14
|
+
];
|
|
15
|
+
async function fileExists(path) {
|
|
16
|
+
try {
|
|
17
|
+
return (await stat(path)).isFile();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function readText(path) {
|
|
24
|
+
try {
|
|
25
|
+
return await readFile(path, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function caseResult(id, title, passed, evidence = [], details = [], partial = false) {
|
|
32
|
+
const status = passed ? 'pass' : partial ? 'partial' : 'fail';
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
title,
|
|
36
|
+
status,
|
|
37
|
+
score: status === 'pass' ? 1 : status === 'partial' ? 0.5 : 0,
|
|
38
|
+
maxScore: 1,
|
|
39
|
+
evidence,
|
|
40
|
+
details,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function summarize(cases, brainPath) {
|
|
44
|
+
const score = cases.reduce((sum, item) => sum + item.score, 0);
|
|
45
|
+
const maxScore = cases.reduce((sum, item) => sum + item.maxScore, 0);
|
|
46
|
+
const percent = maxScore ? (score / maxScore) * 100 : 0;
|
|
47
|
+
return { ok: cases.every((item) => item.status === 'pass'), brainPath, score, maxScore, percent, cases };
|
|
48
|
+
}
|
|
49
|
+
async function benchmarkFileCase(brainPath) {
|
|
50
|
+
const path = join(brainPath, 'Evals', 'second-brain-benchmarks.md');
|
|
51
|
+
const content = await readText(path);
|
|
52
|
+
if (!content)
|
|
53
|
+
return caseResult('SB-00', 'Benchmark file exists', false, [path]);
|
|
54
|
+
const missing = STATIC_CASES.filter((id) => !content.includes(id));
|
|
55
|
+
return caseResult('SB-00', 'Benchmark file exists and names static cases', missing.length === 0, [path], missing.length ? [`missing case ids: ${missing.join(', ')}`] : [], missing.length > 0 && missing.length < STATIC_CASES.length);
|
|
56
|
+
}
|
|
57
|
+
async function routingCase(brainPath) {
|
|
58
|
+
const folderCheck = await checkBrainFolders(brainPath);
|
|
59
|
+
const mapCheck = await checkVaultStructureMap(brainPath);
|
|
60
|
+
const missingIndexes = [];
|
|
61
|
+
for (const folder of FOLDERS) {
|
|
62
|
+
if (!(await fileExists(join(brainPath, folder.dir, '_Index.md'))))
|
|
63
|
+
missingIndexes.push(`${folder.dir}/_Index.md`);
|
|
64
|
+
}
|
|
65
|
+
const pass = folderCheck.status === 'pass' && mapCheck.status === 'pass' && missingIndexes.length === 0;
|
|
66
|
+
return caseResult('SB-02', 'Routing map and destination indexes are complete', pass, [join(brainPath, 'Vault Structure Map.md')], [...(folderCheck.details ?? []), ...(mapCheck.details ?? []), ...missingIndexes], folderCheck.status === 'pass' || mapCheck.status === 'pass');
|
|
67
|
+
}
|
|
68
|
+
async function requiredFilesCase(brainPath, id, title, relPaths, tokens = []) {
|
|
69
|
+
const missing = [];
|
|
70
|
+
const evidence = [];
|
|
71
|
+
const tokenMisses = [];
|
|
72
|
+
for (const rel of relPaths) {
|
|
73
|
+
const path = join(brainPath, rel);
|
|
74
|
+
const content = await readText(path);
|
|
75
|
+
if (!content) {
|
|
76
|
+
missing.push(rel);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
evidence.push(path);
|
|
80
|
+
for (const token of tokens) {
|
|
81
|
+
if (!content.toLowerCase().includes(token.toLowerCase()))
|
|
82
|
+
tokenMisses.push(`${rel}: missing "${token}"`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const details = [...missing.map((m) => `missing ${m}`), ...tokenMisses];
|
|
86
|
+
return caseResult(id, title, details.length === 0, evidence, details, evidence.length > 0 && missing.length < relPaths.length);
|
|
87
|
+
}
|
|
88
|
+
async function finalGateCase(brainPath) {
|
|
89
|
+
const relPaths = ['Templates/final.md', 'Templates/final-lite.md', 'Shared/Tech-Standards/verification-standard.md'];
|
|
90
|
+
const evidence = [];
|
|
91
|
+
const details = [];
|
|
92
|
+
for (const rel of relPaths) {
|
|
93
|
+
const path = join(brainPath, rel);
|
|
94
|
+
const content = await readText(path);
|
|
95
|
+
if (!content) {
|
|
96
|
+
details.push(`missing ${rel}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
evidence.push(path);
|
|
100
|
+
if (!content.includes('If a row has no evidence'))
|
|
101
|
+
details.push(`${rel}: missing evidence rule`);
|
|
102
|
+
}
|
|
103
|
+
for (const rel of ['Templates/final.md', 'Templates/final-lite.md']) {
|
|
104
|
+
const content = await readText(join(brainPath, rel));
|
|
105
|
+
if (!content)
|
|
106
|
+
continue;
|
|
107
|
+
const validation = validateFinalGateContent(content.replace('note_type: template', 'note_type: final-gate'));
|
|
108
|
+
const structuralWarnings = validation.warnings.filter((warning) => warning.startsWith('Missing'));
|
|
109
|
+
details.push(...structuralWarnings.map((warning) => `${rel}: ${warning}`));
|
|
110
|
+
}
|
|
111
|
+
return caseResult('SB-FINAL', 'Final gate templates and validator contract exist', details.length === 0, evidence, details, evidence.length > 0);
|
|
112
|
+
}
|
|
113
|
+
async function contextCase(brainPath, indexPath) {
|
|
114
|
+
const report = await inspectBrainContext({ brainPath, indexPath });
|
|
115
|
+
const structuralWarnings = report.warnings.filter((warning) => !warning.includes('Search index is missing') && !warning.includes('Search index is older'));
|
|
116
|
+
return caseResult('SB-09', 'Hot context assembles without missing or oversized sources', report.ok && report.contextChars > 0 && structuralWarnings.length === 0, report.sources.map((source) => source.path), structuralWarnings, report.contextChars > 0);
|
|
117
|
+
}
|
|
118
|
+
async function indexCase(brainPath, indexPath) {
|
|
119
|
+
const check = await checkSearchIndexFreshness(brainPath, indexPath);
|
|
120
|
+
return caseResult('SB-IDX', 'Search index exists and is fresh enough', check.status === 'pass', [indexPath], check.status === 'pass' ? [] : [check.message, ...(check.details ?? [])], check.status === 'warn');
|
|
121
|
+
}
|
|
122
|
+
async function retrievalCase(brainPath, item, searchImpl) {
|
|
123
|
+
const res = await searchImpl(item.query, { mode: 'fts', limit: 5, sources: ['vault'] });
|
|
124
|
+
const hit = res.hits.find((candidate) => candidate.path === item.expectedPath);
|
|
125
|
+
return caseResult(item.id, `Retrieval finds ${item.expectedPath}`, !!hit, hit ? [join(brainPath, hit.path ?? item.expectedPath)] : [], hit ? [] : [`query="${item.query}" did not return ${item.expectedPath}`], res.hits.length > 0);
|
|
126
|
+
}
|
|
127
|
+
export async function runBrainEval(options = {}) {
|
|
128
|
+
const brainPath = options.brainPath;
|
|
129
|
+
if (!brainPath) {
|
|
130
|
+
return summarize([
|
|
131
|
+
caseResult('SB-CONFIG', 'Second-brain path is configured', false, [], ['Run `sanook brain init [path]` first.']),
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if (!(await stat(brainPath)).isDirectory()) {
|
|
136
|
+
return summarize([
|
|
137
|
+
caseResult('SB-CONFIG', 'Second-brain path is a directory', false, [brainPath], ['Configured brainPath is not a directory.']),
|
|
138
|
+
], brainPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return summarize([
|
|
143
|
+
caseResult('SB-CONFIG', 'Second-brain path exists', false, [brainPath], ['Configured brainPath does not exist.']),
|
|
144
|
+
], brainPath);
|
|
145
|
+
}
|
|
146
|
+
const indexPath = options.indexPath ?? INDEX_PATH;
|
|
147
|
+
const cases = [
|
|
148
|
+
await benchmarkFileCase(brainPath),
|
|
149
|
+
await requiredFilesCase(brainPath, 'SB-01', 'AI context entrypoint exists', ['Shared/AI-Context-Index.md'], ['Vault Structure Map']),
|
|
150
|
+
await routingCase(brainPath),
|
|
151
|
+
await requiredFilesCase(brainPath, 'SB-03', 'Memory write protocol is documented', ['Shared/Rules/memory-write-protocol.md'], ['ADD', 'UPDATE', 'DELETE', 'NOOP']),
|
|
152
|
+
await requiredFilesCase(brainPath, 'SB-04', 'External ingest quarantine/provenance path exists', ['Runbooks/ingest-quarantine.md', 'Shared/Provenance/ingest-log.md']),
|
|
153
|
+
await requiredFilesCase(brainPath, 'SB-05', 'Coding verification standard exists', ['Shared/Tech-Standards/verification-standard.md']),
|
|
154
|
+
await requiredFilesCase(brainPath, 'SB-06', 'Owner-facing response examples exist', ['Shared/User-Memory/response-examples.md']),
|
|
155
|
+
await requiredFilesCase(brainPath, 'SB-07', 'Framework improvement evidence paths exist', ['Evals/quality-ledger.md', 'Research/_Index.md', 'Sessions/_Index.md']),
|
|
156
|
+
await requiredFilesCase(brainPath, 'SB-08', 'Multi-agent coordination paths exist', ['Shared/Coordination/task-board.md', 'Shared/Coordination/task-board/_Index.md']),
|
|
157
|
+
await contextCase(brainPath, indexPath),
|
|
158
|
+
await requiredFilesCase(brainPath, 'SB-10', 'Learning loop ledger and session index exist', ['Evals/quality-ledger.md', 'Sessions/_Index.md']),
|
|
159
|
+
await finalGateCase(brainPath),
|
|
160
|
+
await indexCase(brainPath, indexPath),
|
|
161
|
+
];
|
|
162
|
+
if (options.runRetrieval !== false) {
|
|
163
|
+
const searchImpl = options.searchImpl ?? search;
|
|
164
|
+
for (const item of RETRIEVAL_CASES)
|
|
165
|
+
cases.push(await retrievalCase(brainPath, item, searchImpl));
|
|
166
|
+
}
|
|
167
|
+
return summarize(cases, brainPath);
|
|
168
|
+
}
|
|
169
|
+
function statusLabel(status) {
|
|
170
|
+
return status.toUpperCase().padEnd(7);
|
|
171
|
+
}
|
|
172
|
+
export function formatBrainEvalReport(report) {
|
|
173
|
+
const lines = [
|
|
174
|
+
'Sanook brain eval',
|
|
175
|
+
`vault: ${report.brainPath ?? '(not configured)'}`,
|
|
176
|
+
`score: ${report.score.toFixed(1)}/${report.maxScore} (${report.percent.toFixed(1)}%)`,
|
|
177
|
+
];
|
|
178
|
+
for (const item of report.cases) {
|
|
179
|
+
lines.push(`[${statusLabel(item.status)}] ${item.id} — ${item.title}`);
|
|
180
|
+
for (const evidence of item.evidence ?? [])
|
|
181
|
+
lines.push(` ${evidence}`);
|
|
182
|
+
for (const detail of item.details ?? [])
|
|
183
|
+
lines.push(` - ${detail}`);
|
|
184
|
+
}
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|