sanook-cli 0.5.2 → 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/CHANGELOG.md +91 -2
- package/README.md +15 -3
- package/README.th.md +8 -1
- package/dist/approval.js +7 -0
- package/dist/bin.js +623 -56
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +42 -3
- package/dist/brain-final.js +15 -9
- 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.js +3 -0
- package/dist/cli-args.js +47 -9
- package/dist/cli-option-values.js +1 -1
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +94 -14
- package/dist/config.js +31 -5
- package/dist/context-pack.js +145 -0
- 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/gateway/auth.js +14 -3
- package/dist/gateway/deliver.js +45 -3
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +30 -1
- package/dist/gateway/ledger.js +20 -1
- package/dist/gateway/session.js +30 -11
- 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 +24 -4
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +34 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +153 -9
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp.js +77 -5
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +51 -7
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +7 -5
- 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 +20 -8
- package/dist/providers/keys.js +21 -0
- package/dist/providers/models.js +1 -1
- package/dist/search/cli.js +9 -1
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +10 -10
- package/dist/session-distill.js +84 -0
- package/dist/session.js +1 -11
- package/dist/skill-install.js +24 -1
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +31 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/permission.js +82 -16
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/search.js +9 -2
- package/dist/tools/task.js +22 -2
- package/dist/tools/timeout.js +7 -5
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +835 -29
- package/dist/ui/banner.js +78 -4
- package/dist/ui/markdown.js +122 -0
- 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 +163 -50
- 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 +32 -6
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/package.json +2 -2
- package/second-brain/Projects/_Index.md +17 -4
- package/second-brain/Projects/sanook-cli/_Index.md +7 -3
- 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 +52 -11
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -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 +2 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -23
- package/second-brain/Shared/Tech-Standards/_Index.md +2 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -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/dist/mcp-registry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inferRegistryServerRisk, formatMcpRiskLabel } from './mcp-risk.js';
|
|
1
2
|
import { inlineValue, takeValue } from './cli-option-values.js';
|
|
2
3
|
export const MCP_REGISTRY_BASE_URL = 'https://registry.modelcontextprotocol.io/v0';
|
|
3
4
|
export const MCP_PRESETS = [
|
|
@@ -22,19 +23,45 @@ export const MCP_PRESETS = [
|
|
|
22
23
|
servers: ['capital.hove/read-only-local-postgres-mcp-server', 'com.mcparmory/sentry', 'io.github.CSOAI-ORG/docker-helper-ai-mcp'],
|
|
23
24
|
},
|
|
24
25
|
];
|
|
26
|
+
const REGISTRY_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
27
|
+
const registryCache = new Map();
|
|
28
|
+
export function clearMcpRegistryCache() {
|
|
29
|
+
registryCache.clear();
|
|
30
|
+
}
|
|
31
|
+
function cacheKey(url) {
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
function readRegistryCache(url) {
|
|
35
|
+
const entry = registryCache.get(cacheKey(url));
|
|
36
|
+
if (!entry)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (Date.now() >= entry.expiresAt) {
|
|
39
|
+
registryCache.delete(cacheKey(url));
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return entry.value;
|
|
43
|
+
}
|
|
44
|
+
function writeRegistryCache(url, value) {
|
|
45
|
+
registryCache.set(cacheKey(url), { expiresAt: Date.now() + REGISTRY_CACHE_TTL_MS, value });
|
|
46
|
+
}
|
|
47
|
+
export { REGISTRY_CACHE_TTL_MS };
|
|
25
48
|
export function parseKeyValueList(values) {
|
|
26
49
|
const out = {};
|
|
27
50
|
for (const value of values) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
throw new Error(`ต้องใช้รูปแบบ KEY=value: ${value}`);
|
|
31
|
-
const key = value.slice(0, idx).trim();
|
|
32
|
-
if (!key)
|
|
33
|
-
throw new Error(`ต้องใช้รูปแบบ KEY=value: ${value}`);
|
|
34
|
-
out[key] = value.slice(idx + 1);
|
|
51
|
+
const parsed = parseKeyValueEntry(value);
|
|
52
|
+
out[parsed.key] = parsed.value;
|
|
35
53
|
}
|
|
36
54
|
return out;
|
|
37
55
|
}
|
|
56
|
+
function parseKeyValueEntry(value) {
|
|
57
|
+
const idx = value.indexOf('=');
|
|
58
|
+
if (idx <= 0)
|
|
59
|
+
throw new Error(`ต้องใช้รูปแบบ KEY=value: ${value}`);
|
|
60
|
+
const key = value.slice(0, idx).trim();
|
|
61
|
+
if (!key)
|
|
62
|
+
throw new Error(`ต้องใช้รูปแบบ KEY=value: ${value}`);
|
|
63
|
+
return { key, value: value.slice(idx + 1) };
|
|
64
|
+
}
|
|
38
65
|
function parseRegistrySearchLimit(raw) {
|
|
39
66
|
if (!raw || !/^[1-9]\d*$/.test(raw))
|
|
40
67
|
return undefined;
|
|
@@ -44,6 +71,7 @@ function parseRegistrySearchLimit(raw) {
|
|
|
44
71
|
export function parseMcpRegistrySearchArgs(args) {
|
|
45
72
|
const query = [];
|
|
46
73
|
let limit = 10;
|
|
74
|
+
let limitSet = false;
|
|
47
75
|
let cursor;
|
|
48
76
|
for (let i = 0; i < args.length; i++) {
|
|
49
77
|
const a = args[i];
|
|
@@ -59,7 +87,10 @@ export function parseMcpRegistrySearchArgs(args) {
|
|
|
59
87
|
const parsed = parseRegistrySearchLimit(raw);
|
|
60
88
|
if (parsed === undefined)
|
|
61
89
|
return { ok: false, message: '--limit ต้องเป็นจำนวนเต็ม 1-50' };
|
|
90
|
+
if (limitSet)
|
|
91
|
+
return { ok: false, message: 'ใช้ --limit เพียงครั้งเดียว' };
|
|
62
92
|
limit = parsed;
|
|
93
|
+
limitSet = true;
|
|
63
94
|
}
|
|
64
95
|
else if (a === '--cursor' || a.startsWith('--cursor=')) {
|
|
65
96
|
const next = a === '--cursor' ? takeValue(args, i) : undefined;
|
|
@@ -69,6 +100,8 @@ export function parseMcpRegistrySearchArgs(args) {
|
|
|
69
100
|
const parsed = raw?.trim();
|
|
70
101
|
if (!parsed)
|
|
71
102
|
return { ok: false, message: '--cursor ต้องระบุค่า' };
|
|
103
|
+
if (cursor !== undefined)
|
|
104
|
+
return { ok: false, message: 'ใช้ --cursor เพียงครั้งเดียว' };
|
|
72
105
|
cursor = parsed;
|
|
73
106
|
}
|
|
74
107
|
else {
|
|
@@ -77,6 +110,111 @@ export function parseMcpRegistrySearchArgs(args) {
|
|
|
77
110
|
}
|
|
78
111
|
return { ok: true, value: { query: query.join(' ').trim(), limit, cursor } };
|
|
79
112
|
}
|
|
113
|
+
function parseInstallOptionValue(args, index, flag) {
|
|
114
|
+
const arg = args[index];
|
|
115
|
+
if (arg === flag)
|
|
116
|
+
return takeValue(args, index);
|
|
117
|
+
return { value: inlineValue(flag, arg), nextIndex: index };
|
|
118
|
+
}
|
|
119
|
+
export function parseMcpRegistryInstallArgs(args) {
|
|
120
|
+
const positionals = [];
|
|
121
|
+
const env = [];
|
|
122
|
+
const headers = [];
|
|
123
|
+
let alias;
|
|
124
|
+
let transport;
|
|
125
|
+
let version;
|
|
126
|
+
let project = false;
|
|
127
|
+
for (let i = 0; i < args.length; i++) {
|
|
128
|
+
const a = args[i];
|
|
129
|
+
if (a === '--') {
|
|
130
|
+
positionals.push(...args.slice(i + 1));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (a === '--project') {
|
|
134
|
+
project = true;
|
|
135
|
+
}
|
|
136
|
+
else if (a === '--name' || a.startsWith('--name=')) {
|
|
137
|
+
const next = parseInstallOptionValue(args, i, '--name');
|
|
138
|
+
if (next.nextIndex !== i)
|
|
139
|
+
i = next.nextIndex;
|
|
140
|
+
const value = next.value?.trim();
|
|
141
|
+
if (!value)
|
|
142
|
+
return { ok: false, message: '--name ต้องระบุค่า' };
|
|
143
|
+
if (alias !== undefined)
|
|
144
|
+
return { ok: false, message: 'ใช้ --name เพียงครั้งเดียว' };
|
|
145
|
+
alias = value;
|
|
146
|
+
}
|
|
147
|
+
else if (a === '--transport' || a.startsWith('--transport=')) {
|
|
148
|
+
const next = parseInstallOptionValue(args, i, '--transport');
|
|
149
|
+
if (next.nextIndex !== i)
|
|
150
|
+
i = next.nextIndex;
|
|
151
|
+
const value = next.value?.trim();
|
|
152
|
+
if (!value)
|
|
153
|
+
return { ok: false, message: '--transport ต้องระบุค่า' };
|
|
154
|
+
if (!['auto', 'remote', 'stdio'].includes(value)) {
|
|
155
|
+
return { ok: false, message: '--transport ต้องเป็น auto, remote, หรือ stdio' };
|
|
156
|
+
}
|
|
157
|
+
if (transport !== undefined)
|
|
158
|
+
return { ok: false, message: 'ใช้ --transport เพียงครั้งเดียว' };
|
|
159
|
+
transport = value;
|
|
160
|
+
}
|
|
161
|
+
else if (a === '--version' || a.startsWith('--version=')) {
|
|
162
|
+
const next = parseInstallOptionValue(args, i, '--version');
|
|
163
|
+
if (next.nextIndex !== i)
|
|
164
|
+
i = next.nextIndex;
|
|
165
|
+
const value = next.value?.trim();
|
|
166
|
+
if (!value)
|
|
167
|
+
return { ok: false, message: '--version ต้องระบุค่า' };
|
|
168
|
+
if (version !== undefined)
|
|
169
|
+
return { ok: false, message: 'ใช้ --version เพียงครั้งเดียว' };
|
|
170
|
+
version = value;
|
|
171
|
+
}
|
|
172
|
+
else if (a === '--env' || a.startsWith('--env=')) {
|
|
173
|
+
const next = parseInstallOptionValue(args, i, '--env');
|
|
174
|
+
if (next.nextIndex !== i)
|
|
175
|
+
i = next.nextIndex;
|
|
176
|
+
const value = next.value;
|
|
177
|
+
if (!value?.trim())
|
|
178
|
+
return { ok: false, message: '--env ต้องระบุ KEY=value' };
|
|
179
|
+
try {
|
|
180
|
+
parseKeyValueEntry(value);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return { ok: false, message: `--env ต้องใช้รูปแบบ KEY=value: ${value}` };
|
|
184
|
+
}
|
|
185
|
+
env.push(value);
|
|
186
|
+
}
|
|
187
|
+
else if (a === '--header' || a.startsWith('--header=')) {
|
|
188
|
+
const next = parseInstallOptionValue(args, i, '--header');
|
|
189
|
+
if (next.nextIndex !== i)
|
|
190
|
+
i = next.nextIndex;
|
|
191
|
+
const value = next.value;
|
|
192
|
+
if (!value?.trim())
|
|
193
|
+
return { ok: false, message: '--header ต้องระบุ KEY=value' };
|
|
194
|
+
try {
|
|
195
|
+
parseKeyValueEntry(value);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return { ok: false, message: `--header ต้องใช้รูปแบบ KEY=value: ${value}` };
|
|
199
|
+
}
|
|
200
|
+
headers.push(value);
|
|
201
|
+
}
|
|
202
|
+
else if (a.startsWith('-')) {
|
|
203
|
+
return { ok: false, message: `ไม่รู้จัก option: ${a}` };
|
|
204
|
+
}
|
|
205
|
+
else if (!a.startsWith('-')) {
|
|
206
|
+
positionals.push(a);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const name = positionals[0];
|
|
210
|
+
if (!name) {
|
|
211
|
+
return { ok: false, message: 'ใช้: sanook mcp install <registry-server-name> [--name alias] [--transport auto|remote|stdio] [--env KEY=value] [--header KEY=value] [--project]' };
|
|
212
|
+
}
|
|
213
|
+
if (positionals.length > 1) {
|
|
214
|
+
return { ok: false, message: `ระบุ registry server ได้เพียงชื่อเดียว: ${positionals.slice(1).join(' ')}` };
|
|
215
|
+
}
|
|
216
|
+
return { ok: true, value: { name, alias, transport, version, env, headers, project } };
|
|
217
|
+
}
|
|
80
218
|
export function aliasFromRegistryName(name) {
|
|
81
219
|
const [scope = '', rawLeaf = name] = name.split('/');
|
|
82
220
|
const leaf = rawLeaf
|
|
@@ -162,7 +300,7 @@ export function formatRegistrySearch(result) {
|
|
|
162
300
|
return `${lines[0]}\n(no matches)`;
|
|
163
301
|
for (const server of result.servers) {
|
|
164
302
|
lines.push(`${server.name}${server.version ? `@${server.version}` : ''} — ${server.description ?? '(no description)'}`);
|
|
165
|
-
lines.push(` transport: ${transportSummary(server)}${server.repositoryUrl ? ` · repo: ${server.repositoryUrl}` : ''}`);
|
|
303
|
+
lines.push(` transport: ${transportSummary(server)} · risk: ${formatMcpRiskLabel(inferRegistryServerRisk(server))}${server.repositoryUrl ? ` · repo: ${server.repositoryUrl}` : ''}`);
|
|
166
304
|
}
|
|
167
305
|
if (result.nextCursor)
|
|
168
306
|
lines.push(`next: --cursor ${result.nextCursor}`);
|
|
@@ -175,6 +313,7 @@ export function formatRegistryInfo(server) {
|
|
|
175
313
|
if (server.websiteUrl)
|
|
176
314
|
lines.push(`website: ${server.websiteUrl}`);
|
|
177
315
|
lines.push(`transport: ${transportSummary(server)}`);
|
|
316
|
+
lines.push(`risk: ${formatMcpRiskLabel(inferRegistryServerRisk(server))}`);
|
|
178
317
|
if (server.remotes.length) {
|
|
179
318
|
lines.push('remotes:');
|
|
180
319
|
for (const remote of server.remotes) {
|
|
@@ -234,10 +373,15 @@ function latestOnly(servers) {
|
|
|
234
373
|
return [...out.values()];
|
|
235
374
|
}
|
|
236
375
|
async function fetchRegistryJson(url, fetchImpl = fetch) {
|
|
376
|
+
const cached = readRegistryCache(url);
|
|
377
|
+
if (cached)
|
|
378
|
+
return cached;
|
|
237
379
|
const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
238
380
|
if (!res.ok)
|
|
239
381
|
throw new Error(`registry ${res.status} ${res.statusText}`);
|
|
240
|
-
|
|
382
|
+
const json = (await res.json());
|
|
383
|
+
writeRegistryCache(url, json);
|
|
384
|
+
return json;
|
|
241
385
|
}
|
|
242
386
|
function transportSummary(server) {
|
|
243
387
|
const transports = [
|
package/dist/mcp-risk.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const RISK_PRIORITY = {
|
|
2
|
+
'read-only': 0,
|
|
3
|
+
'network-write': 1,
|
|
4
|
+
'file-write': 2,
|
|
5
|
+
'database-write': 3,
|
|
6
|
+
'infra/admin': 4,
|
|
7
|
+
};
|
|
8
|
+
const WRITE_TOOL = /\b(write|create|update|delete|insert|drop|push|post|send|execute|deploy|apply|modify|edit|remove|destroy|mutate|run_|upload|patch|merge|commit|publish|trigger|invoke|call|set_)\b/i;
|
|
9
|
+
const READ_ONLY_TEXT = /\b(read[-_ ]?only|readonly|list|get|search|fetch|query|inspect|view|describe|lookup|recall)\b/i;
|
|
10
|
+
const FILE_WRITE_TEXT = /\b(file|filesystem|fs[-_ ]?server|write_file|edit_file|directory)\b/i;
|
|
11
|
+
const DB_WRITE_TEXT = /\b(postgres|postgresql|mysql|sqlite|mongodb|redis|database|sql|db[-_ ]?write)\b/i;
|
|
12
|
+
const NETWORK_WRITE_TEXT = /\b(github|gitlab|slack|discord|linear|jira|notion|fetch|search|browser|playwright|http|web|api|issue|pull|release|message|chat|email|gmail|drive|obsidian|tavily|brave)\b/i;
|
|
13
|
+
const INFRA_TEXT = /\b(docker|kubernetes|k8s|helm|terraform|aws|gcp|azure|infra|container|cluster|pod|deployment|kubectl)\b/i;
|
|
14
|
+
function maxRisk(...labels) {
|
|
15
|
+
return labels.reduce((best, label) => (RISK_PRIORITY[label] > RISK_PRIORITY[best] ? label : best), 'read-only');
|
|
16
|
+
}
|
|
17
|
+
function riskFromText(text) {
|
|
18
|
+
const haystack = text.toLowerCase();
|
|
19
|
+
if (INFRA_TEXT.test(haystack))
|
|
20
|
+
return 'infra/admin';
|
|
21
|
+
if (DB_WRITE_TEXT.test(haystack))
|
|
22
|
+
return READ_ONLY_TEXT.test(haystack) ? 'read-only' : 'database-write';
|
|
23
|
+
if (FILE_WRITE_TEXT.test(haystack))
|
|
24
|
+
return 'file-write';
|
|
25
|
+
if (NETWORK_WRITE_TEXT.test(haystack))
|
|
26
|
+
return READ_ONLY_TEXT.test(haystack) ? 'read-only' : 'network-write';
|
|
27
|
+
if (READ_ONLY_TEXT.test(haystack))
|
|
28
|
+
return 'read-only';
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
function riskFromTools(tools) {
|
|
32
|
+
const labels = [];
|
|
33
|
+
for (const tool of tools) {
|
|
34
|
+
const text = `${tool.name} ${tool.description ?? ''}`;
|
|
35
|
+
const base = riskFromText(text);
|
|
36
|
+
if (base)
|
|
37
|
+
labels.push(base);
|
|
38
|
+
if (WRITE_TOOL.test(text) && base !== 'read-only') {
|
|
39
|
+
if (DB_WRITE_TEXT.test(text))
|
|
40
|
+
labels.push('database-write');
|
|
41
|
+
else if (FILE_WRITE_TEXT.test(text))
|
|
42
|
+
labels.push('file-write');
|
|
43
|
+
else if (INFRA_TEXT.test(text))
|
|
44
|
+
labels.push('infra/admin');
|
|
45
|
+
else
|
|
46
|
+
labels.push('network-write');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return labels.length ? maxRisk(...labels) : undefined;
|
|
50
|
+
}
|
|
51
|
+
export function inferRegistryServerRisk(server) {
|
|
52
|
+
const parts = [
|
|
53
|
+
server.name,
|
|
54
|
+
server.title,
|
|
55
|
+
server.description,
|
|
56
|
+
...server.packages.map((pkg) => `${pkg.registryType ?? ''} ${pkg.identifier ?? ''} ${pkg.runtimeHint ?? ''}`),
|
|
57
|
+
...server.remotes.map((remote) => `${remote.type ?? ''} ${remote.url ?? ''}`),
|
|
58
|
+
].filter((part) => Boolean(part));
|
|
59
|
+
const labels = parts.map((part) => riskFromText(part)).filter((label) => !!label);
|
|
60
|
+
return labels.length ? maxRisk(...labels) : 'read-only';
|
|
61
|
+
}
|
|
62
|
+
export function inferConfiguredServerRisk(name, cfg, tools = []) {
|
|
63
|
+
const commandLine = [cfg.command, ...(cfg.args ?? []), cfg.url].filter(Boolean).join(' ');
|
|
64
|
+
const labels = [riskFromText(name), riskFromText(commandLine), riskFromTools(tools)].filter((label) => !!label);
|
|
65
|
+
if (cfg.url)
|
|
66
|
+
labels.push('network-write');
|
|
67
|
+
return labels.length ? maxRisk(...labels) : 'read-only';
|
|
68
|
+
}
|
|
69
|
+
export function formatMcpRiskLabel(label) {
|
|
70
|
+
return label;
|
|
71
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -26,6 +26,27 @@ function safeEnv() {
|
|
|
26
26
|
}
|
|
27
27
|
return out;
|
|
28
28
|
}
|
|
29
|
+
export function isMcpServerEnabled(cfg) {
|
|
30
|
+
return cfg.enabled !== false;
|
|
31
|
+
}
|
|
32
|
+
/** auth hints for hosted MCP remotes that return HTTP 401 */
|
|
33
|
+
export function mcpAuthHints(cfg, error) {
|
|
34
|
+
if (!cfg.url || !error || !/\b401\b/.test(error))
|
|
35
|
+
return [];
|
|
36
|
+
const hints = [];
|
|
37
|
+
const authHeader = cfg.headers?.Authorization ?? cfg.headers?.authorization;
|
|
38
|
+
if (!authHeader) {
|
|
39
|
+
hints.push('remote server ตอบ 401 — เพิ่ม Authorization header ใน ~/.sanook/mcp.json หรือตอน install: --header Authorization=\'Bearer <token>\'');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
hints.push('remote server ตอบ 401 แม้มี Authorization header — ตรวจว่า token หมดอายุ, scope ไม่พอ, หรือ header name/format ไม่ตรงที่ server ต้องการ');
|
|
43
|
+
}
|
|
44
|
+
if (!Object.keys(cfg.env ?? {}).length) {
|
|
45
|
+
hints.push('บาง hosted MCP ใช้ API key ผ่าน env แทน header — ดู requirements: sanook mcp info <registry-server-name>');
|
|
46
|
+
}
|
|
47
|
+
hints.push('ทดสอบหลังแก้: sanook mcp test <name>');
|
|
48
|
+
return hints;
|
|
49
|
+
}
|
|
29
50
|
export function isValidMcpServerName(name) {
|
|
30
51
|
return (/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name) &&
|
|
31
52
|
!['__proto__', 'prototype', 'constructor'].includes(name));
|
|
@@ -52,6 +73,9 @@ class StdioTransport {
|
|
|
52
73
|
// Windows: `npx`/`npm`/JS bins เป็น .cmd shim → spawn ตรงๆ = ENOENT. shell=true ให้ผ่าน PATHEXT.
|
|
53
74
|
// (config นี้ user เป็นเจ้าของ/trust แล้ว — bare-name resolution เท่านั้น)
|
|
54
75
|
shell: process.platform === 'win32',
|
|
76
|
+
// POSIX: own process group → close() can SIGTERM the whole tree (npx/uvx/docker wrappers
|
|
77
|
+
// spawn the real server as a grandchild; killing only the wrapper would orphan it).
|
|
78
|
+
detached: process.platform !== 'win32',
|
|
55
79
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
80
|
});
|
|
57
81
|
this.proc.stdout?.on('data', (d) => this.onData(d.toString()));
|
|
@@ -125,11 +149,21 @@ class StdioTransport {
|
|
|
125
149
|
this.proc.stdin?.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
|
|
126
150
|
}
|
|
127
151
|
close() {
|
|
152
|
+
const pid = this.proc.pid;
|
|
128
153
|
try {
|
|
129
|
-
|
|
154
|
+
// POSIX: negative pid = the whole process group (kills npx/uvx/docker + the real server child).
|
|
155
|
+
if (pid && process.platform !== 'win32')
|
|
156
|
+
process.kill(-pid, 'SIGTERM');
|
|
157
|
+
else
|
|
158
|
+
this.proc.kill();
|
|
130
159
|
}
|
|
131
160
|
catch {
|
|
132
|
-
|
|
161
|
+
try {
|
|
162
|
+
this.proc.kill();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* already dead */
|
|
166
|
+
}
|
|
133
167
|
}
|
|
134
168
|
}
|
|
135
169
|
}
|
|
@@ -188,8 +222,11 @@ class HttpTransport {
|
|
|
188
222
|
const sid = res.headers.get('mcp-session-id');
|
|
189
223
|
if (sid)
|
|
190
224
|
this.sessionId = sid;
|
|
191
|
-
if (!res.ok)
|
|
192
|
-
|
|
225
|
+
if (!res.ok) {
|
|
226
|
+
const err = new Error(`mcp http ${res.status} ${res.statusText}`);
|
|
227
|
+
err.status = res.status;
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
193
230
|
const ctype = res.headers.get('content-type') ?? '';
|
|
194
231
|
if (ctype.includes('text/event-stream'))
|
|
195
232
|
return this.parseSse(await res.text(), id);
|
|
@@ -262,7 +299,9 @@ export async function probeMcpServer(cfg, timeoutMs = REQUEST_TIMEOUT) {
|
|
|
262
299
|
return { ok: true, transport, tools };
|
|
263
300
|
}
|
|
264
301
|
catch (e) {
|
|
265
|
-
|
|
302
|
+
const error = e.message;
|
|
303
|
+
const authHints = mcpAuthHints(cfg, error);
|
|
304
|
+
return { ok: false, transport, tools: [], error, ...(authHints.length ? { authHints } : {}) };
|
|
266
305
|
}
|
|
267
306
|
finally {
|
|
268
307
|
client.close();
|
|
@@ -295,6 +334,10 @@ function sanitizeMcpServerConfig(raw) {
|
|
|
295
334
|
const headers = stringRecord(r.headers);
|
|
296
335
|
if (headers)
|
|
297
336
|
cfg.headers = headers;
|
|
337
|
+
if (r.enabled === false)
|
|
338
|
+
cfg.enabled = false;
|
|
339
|
+
else if (r.enabled === true)
|
|
340
|
+
cfg.enabled = true;
|
|
298
341
|
return cfg.command || cfg.url ? cfg : null;
|
|
299
342
|
}
|
|
300
343
|
async function readMcpFile(path, merged) {
|
|
@@ -332,6 +375,31 @@ export async function loadMcpConfig(onLog, cwd = process.cwd()) {
|
|
|
332
375
|
}
|
|
333
376
|
return merged;
|
|
334
377
|
}
|
|
378
|
+
/** หา path ของไฟล์ config ที่เก็บ server นี้ (global หรือ trusted project) */
|
|
379
|
+
export async function findMcpServerConfigPath(name, cwd = process.cwd()) {
|
|
380
|
+
const globalPath = appHomePath('mcp.json');
|
|
381
|
+
try {
|
|
382
|
+
const cfg = JSON.parse(await readFile(globalPath, 'utf8'));
|
|
383
|
+
if (cfg.mcpServers && isValidMcpServerName(name) && name in cfg.mcpServers)
|
|
384
|
+
return globalPath;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
/* no global config */
|
|
388
|
+
}
|
|
389
|
+
const root = await projectRoot(cwd);
|
|
390
|
+
const projectPath = await projectConfigPathIfTrusted('mcp.json', root);
|
|
391
|
+
if (!projectPath)
|
|
392
|
+
return undefined;
|
|
393
|
+
try {
|
|
394
|
+
const cfg = JSON.parse(await readFile(projectPath, 'utf8'));
|
|
395
|
+
if (cfg.mcpServers && isValidMcpServerName(name) && name in cfg.mcpServers)
|
|
396
|
+
return projectPath;
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
/* unreadable project config */
|
|
400
|
+
}
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
335
403
|
let cachePromise = null;
|
|
336
404
|
let activeClients = []; // sync ref สำหรับ closeMcp ใน exit handler
|
|
337
405
|
/** โหลด tools จาก MCP servers — in-flight promise singleton (concurrent call ไม่ spawn ซ้ำ/leak child) */
|
|
@@ -347,6 +415,10 @@ async function buildMcpTools(onLog) {
|
|
|
347
415
|
const clients = [];
|
|
348
416
|
activeClients = clients; // ref เดียวกัน → closeMcp kill client ที่ spawn ระหว่าง build ได้ด้วย
|
|
349
417
|
for (const [serverName, cfg] of Object.entries(config)) {
|
|
418
|
+
if (!isMcpServerEnabled(cfg)) {
|
|
419
|
+
onLog?.(`MCP "${serverName}" disabled — ข้าม`);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
350
422
|
if (!cfg.url && !cfg.command) {
|
|
351
423
|
onLog?.(`MCP "${serverName}" ข้าม: ต้องมี "command" (stdio) หรือ "url" (remote)`);
|
|
352
424
|
continue;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// `sanook memory log` — a read-only viewer over the BI-TEMPORAL memory store: see how a belief about
|
|
2
|
+
// the project evolved over time (what was true, when it was superseded, and by what). The store keeps
|
|
3
|
+
// superseded/archived facts with validFrom/invalidatedAt/supersededBy/supersedes edges — most coding
|
|
4
|
+
// CLIs overwrite memory, so this "decision evolution" view is genuinely differentiated. Pure +
|
|
5
|
+
// deterministic (no disk/clock of its own) → fully testable.
|
|
6
|
+
import { tokens } from './memory-store.js';
|
|
7
|
+
function relevance(query, fact) {
|
|
8
|
+
const q = [...tokens(query)];
|
|
9
|
+
if (!q.length)
|
|
10
|
+
return 0;
|
|
11
|
+
const ft = [...tokens(fact.text)];
|
|
12
|
+
// forgiving match for a human-facing viewer: exact OR a shared prefix (deploy↔deploys↔deployment)
|
|
13
|
+
const matches = (t) => ft.some((w) => w === t || (t.length >= 3 && (w.startsWith(t) || t.startsWith(w))));
|
|
14
|
+
let overlap = 0;
|
|
15
|
+
for (const t of q)
|
|
16
|
+
if (matches(t))
|
|
17
|
+
overlap++;
|
|
18
|
+
return overlap;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Facts matching `query` across ALL statuses (active + superseded + archived), each with its
|
|
22
|
+
* evolution edges resolved. Empty query → the most recently CHANGED facts (superseded/archived first)
|
|
23
|
+
* so `sanook memory log` with no args surfaces "what beliefs changed recently".
|
|
24
|
+
*/
|
|
25
|
+
export function memoryLog(store, query = '', limit = 12) {
|
|
26
|
+
const byId = new Map(store.facts.map((f) => [f.id, f]));
|
|
27
|
+
const q = query.trim();
|
|
28
|
+
const ranked = q
|
|
29
|
+
? store.facts
|
|
30
|
+
.map((f) => ({ f, score: relevance(q, f) }))
|
|
31
|
+
.filter((x) => x.score > 0)
|
|
32
|
+
.sort((a, b) => b.score - a.score || b.f.updated - a.f.updated)
|
|
33
|
+
.map((x) => x.f)
|
|
34
|
+
: [...store.facts]
|
|
35
|
+
.filter((f) => f.status !== 'active') // no query → highlight what CHANGED
|
|
36
|
+
.sort((a, b) => (b.invalidatedAt ?? b.updated) - (a.invalidatedAt ?? a.updated));
|
|
37
|
+
return ranked.slice(0, limit).map((f) => ({
|
|
38
|
+
fact: f,
|
|
39
|
+
supersededBy: f.supersededBy ? byId.get(f.supersededBy) : undefined,
|
|
40
|
+
supersedes: f.supersedes.map((id) => byId.get(id)).filter((x) => !!x),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
export function memoryStats(store) {
|
|
44
|
+
const byTier = {};
|
|
45
|
+
let active = 0, superseded = 0, archived = 0;
|
|
46
|
+
for (const f of store.facts) {
|
|
47
|
+
byTier[f.tier] = (byTier[f.tier] ?? 0) + 1;
|
|
48
|
+
if (f.status === 'active')
|
|
49
|
+
active++;
|
|
50
|
+
else if (f.status === 'superseded')
|
|
51
|
+
superseded++;
|
|
52
|
+
else if (f.status === 'archived')
|
|
53
|
+
archived++;
|
|
54
|
+
}
|
|
55
|
+
return { total: store.facts.length, active, superseded, archived, byTier };
|
|
56
|
+
}
|
|
57
|
+
function day(ms) {
|
|
58
|
+
try {
|
|
59
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return '?';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const BADGE = { active: '● active', superseded: '↻ superseded', archived: '⌁ archived' };
|
|
66
|
+
export function renderMemoryLog(entries, query = '') {
|
|
67
|
+
if (!entries.length) {
|
|
68
|
+
return query ? `ไม่เจอ fact ที่ตรงกับ "${query}" ใน memory (รวม superseded/archived)` : 'ยังไม่มี belief ที่เปลี่ยน (superseded/archived) — memory ยังนิ่ง';
|
|
69
|
+
}
|
|
70
|
+
const lines = [query ? `memory log — "${query}" (${entries.length})` : `memory log — recent changes (${entries.length})`];
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
const f = e.fact;
|
|
73
|
+
const when = f.invalidatedAt ? `${day(f.validFrom)} → ${day(f.invalidatedAt)}` : `since ${day(f.validFrom)}`;
|
|
74
|
+
lines.push('', `${BADGE[f.status] ?? f.status} [${f.noteType}/${f.tier}] ${when}`);
|
|
75
|
+
lines.push(` ${f.text}`);
|
|
76
|
+
if (e.supersededBy)
|
|
77
|
+
lines.push(` ↳ superseded by: ${e.supersededBy.text} (${day(e.supersededBy.validFrom)})`);
|
|
78
|
+
for (const s of e.supersedes)
|
|
79
|
+
lines.push(` ↳ supersedes: ${s.text}`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
export function renderMemoryStats(s) {
|
|
84
|
+
const tiers = Object.entries(s.byTier).map(([t, n]) => `${t}:${n}`).join(' · ') || '(none)';
|
|
85
|
+
return [
|
|
86
|
+
`memory: ${s.total} fact(s)`,
|
|
87
|
+
` ● active ${s.active} · ↻ superseded ${s.superseded} · ⌁ archived ${s.archived}`,
|
|
88
|
+
` tiers: ${tiers}`,
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
package/dist/memory-store.js
CHANGED
|
@@ -503,6 +503,8 @@ export async function loadStore(now = Date.now()) {
|
|
|
503
503
|
const parsed = StoreSchema.safeParse(JSON.parse(await readFile(MEMORY_JSON, 'utf8')));
|
|
504
504
|
if (parsed.success)
|
|
505
505
|
return parsed.data;
|
|
506
|
+
// parseable but schema-invalid (version bump / partial write): DON'T lose it — saveStore
|
|
507
|
+
// preserves the original to a .corrupt backup before the next overwrite (loadStore stays pure).
|
|
506
508
|
}
|
|
507
509
|
catch {
|
|
508
510
|
/* no json yet, or malformed → fall through */
|
|
@@ -536,12 +538,46 @@ async function writeSecure(path, content) {
|
|
|
536
538
|
* Both files are 0o600. On the very first json write, the legacy MEMORY.md is backed up
|
|
537
539
|
* to MEMORY.md.bak so raw legacy text is never destroyed. No-op when persistence is disabled.
|
|
538
540
|
*/
|
|
541
|
+
/**
|
|
542
|
+
* If an existing memory.json cannot be validated (schema bump / corruption / partial write),
|
|
543
|
+
* copy it verbatim to memory.json.<ts>.corrupt before it gets overwritten — so a single schema
|
|
544
|
+
* mismatch never silently destroys the entire auto-memory. Best-effort, idempotent per `now`.
|
|
545
|
+
*/
|
|
546
|
+
async function preserveUnvalidatableStore(now) {
|
|
547
|
+
let raw;
|
|
548
|
+
try {
|
|
549
|
+
raw = await readFile(MEMORY_JSON, 'utf8');
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
if (StoreSchema.safeParse(JSON.parse(raw)).success)
|
|
556
|
+
return; // valid → nothing to rescue
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
/* unparseable → preserve below */
|
|
560
|
+
}
|
|
561
|
+
const backup = `${MEMORY_JSON}.${now}.corrupt`;
|
|
562
|
+
if (await exists(backup))
|
|
563
|
+
return;
|
|
564
|
+
try {
|
|
565
|
+
await writeFile(backup, raw, { mode: 0o600 });
|
|
566
|
+
await chmod(backup, 0o600).catch(() => { });
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
/* best-effort */
|
|
570
|
+
}
|
|
571
|
+
}
|
|
539
572
|
export async function saveStore(store, now = Date.now()) {
|
|
540
573
|
if (!persistenceEnabled())
|
|
541
574
|
return;
|
|
542
575
|
await mkdir(MEMORY_DIR, { recursive: true });
|
|
543
576
|
const firstJson = !(await exists(MEMORY_JSON));
|
|
544
|
-
if (firstJson
|
|
577
|
+
if (!firstJson) {
|
|
578
|
+
await preserveUnvalidatableStore(now); // data-loss guard before overwriting an unvalidatable store
|
|
579
|
+
}
|
|
580
|
+
else if (await exists(AUTO_MEMORY_FILE)) {
|
|
545
581
|
await copyFile(AUTO_MEMORY_FILE, MEMORY_BAK).catch(() => { });
|
|
546
582
|
await chmod(MEMORY_BAK, 0o600).catch(() => { });
|
|
547
583
|
}
|
package/dist/memory.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { join, dirname, resolve } from 'node:path';
|
|
3
|
+
import { buildContextPackBlock, listContextPacks, readContextPackExcerpt, selectContextPack } from './context-pack.js';
|
|
4
|
+
import { buildProjectContextBlock, resolveVaultProject } from './project-registry.js';
|
|
3
5
|
import { appHomePath, BRAND, persistenceEnabled, worklogEnabled } from './brand.js';
|
|
4
6
|
import { redactKey } from './providers/keys.js';
|
|
5
7
|
import { loadStore, saveStore, mergeFact, maybeConsolidate, consolidate, renderPromptBlock } from './memory-store.js';
|
|
@@ -77,12 +79,12 @@ export async function loadAutoMemory() {
|
|
|
77
79
|
* "รู้จัก" vault: inject Shared/AI-Context-Index.md (ไฟล์ที่ vault บอกให้อ่านก่อน) เข้า system prompt
|
|
78
80
|
* brainPath มาจาก ~/.sanook/config.json · ไม่มี/ไฟล์หาย → คืน '' (เงียบ)
|
|
79
81
|
*/
|
|
80
|
-
export async function loadBrainContext() {
|
|
82
|
+
export async function loadBrainContext(cwd = process.cwd()) {
|
|
81
83
|
const brainPath = await getBrainPath();
|
|
82
|
-
return brainPath ? buildBrainContext(brainPath) : '';
|
|
84
|
+
return brainPath ? buildBrainContext(brainPath, { cwd }) : '';
|
|
83
85
|
}
|
|
84
86
|
/** ประกอบ source parts ชุดเดียวกับที่ inject เข้า prompt จริง — ให้ CLI inspect ได้โดยไม่ drift */
|
|
85
|
-
export async function buildBrainContextParts(brainPath) {
|
|
87
|
+
export async function buildBrainContextParts(brainPath, options = {}) {
|
|
86
88
|
const idx = await readTrimmedPart({
|
|
87
89
|
id: 'ai-context-index',
|
|
88
90
|
label: 'AI Context Index',
|
|
@@ -99,7 +101,47 @@ export async function buildBrainContextParts(brainPath) {
|
|
|
99
101
|
wrap: (content) => `## current-state\n${content}`,
|
|
100
102
|
});
|
|
101
103
|
const inbox = await readInboxPart(brainPath, 'Shared/Memory-Inbox/memory-inbox.md', 1200);
|
|
102
|
-
|
|
104
|
+
const parts = [idx, currentState, inbox];
|
|
105
|
+
const project = await resolveVaultProject({
|
|
106
|
+
brainPath,
|
|
107
|
+
cwd: options.cwd,
|
|
108
|
+
slug: options.projectSlug,
|
|
109
|
+
});
|
|
110
|
+
if (project) {
|
|
111
|
+
const block = await buildProjectContextBlock(brainPath, project);
|
|
112
|
+
parts.push({
|
|
113
|
+
id: 'project-workspace',
|
|
114
|
+
label: `Project (${project.slug})`,
|
|
115
|
+
relPath: `${project.relDir}/`,
|
|
116
|
+
path: join(brainPath, project.relDir),
|
|
117
|
+
content: block,
|
|
118
|
+
chars: block.length,
|
|
119
|
+
maxChars: 3500,
|
|
120
|
+
status: block ? 'present' : 'empty',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const taskQuery = options.taskQuery?.trim();
|
|
124
|
+
if (taskQuery) {
|
|
125
|
+
const packs = await listContextPacks(brainPath);
|
|
126
|
+
const selected = selectContextPack(taskQuery, packs);
|
|
127
|
+
if (selected) {
|
|
128
|
+
const relPath = selected.pack.relPath;
|
|
129
|
+
const path = join(brainPath, relPath);
|
|
130
|
+
const maxChars = 1200;
|
|
131
|
+
const excerpt = await readContextPackExcerpt(brainPath, selected.pack, maxChars);
|
|
132
|
+
parts.push({
|
|
133
|
+
id: 'context-pack',
|
|
134
|
+
label: `Context Pack (${selected.pack.slug})`,
|
|
135
|
+
relPath,
|
|
136
|
+
path,
|
|
137
|
+
content: excerpt,
|
|
138
|
+
chars: excerpt.length,
|
|
139
|
+
maxChars,
|
|
140
|
+
status: excerpt ? 'present' : 'empty',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return parts;
|
|
103
145
|
}
|
|
104
146
|
export function renderBrainContext(brainPath, parts) {
|
|
105
147
|
const content = parts.map((part) => part.content).filter(Boolean);
|
|
@@ -107,10 +149,12 @@ export function renderBrainContext(brainPath, parts) {
|
|
|
107
149
|
return '';
|
|
108
150
|
return `<brain_vault path="${brainPath}" note="second-brain ของ user — สิ่งที่จำไว้/state ปัจจุบันอยู่ใน block นี้; route โน้ตตาม Vault Structure Map; อ่าน/เขียนไฟล์ใน vault ด้วย absolute path ได้">\n${content.join('\n\n')}\n</brain_vault>`;
|
|
109
151
|
}
|
|
110
|
-
/** ประกอบ brain context จาก vault path (pure → testable) — entry + current-state + remembered facts */
|
|
111
|
-
export async function buildBrainContext(brainPath) {
|
|
112
|
-
return renderBrainContext(brainPath, await buildBrainContextParts(brainPath));
|
|
152
|
+
/** ประกอบ brain context จาก vault path (pure → testable) — entry + current-state + remembered facts + optional context pack */
|
|
153
|
+
export async function buildBrainContext(brainPath, options = {}) {
|
|
154
|
+
return renderBrainContext(brainPath, await buildBrainContextParts(brainPath, options));
|
|
113
155
|
}
|
|
156
|
+
/** Build a standalone context-pack block for per-turn injection (turn-retrieval path). */
|
|
157
|
+
export { buildContextPackBlock };
|
|
114
158
|
async function readTrimmedPart(input) {
|
|
115
159
|
const p = join(input.brainPath, input.relPath);
|
|
116
160
|
try {
|