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
package/dist/mcp.js
CHANGED
|
@@ -14,6 +14,7 @@ const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.u
|
|
|
14
14
|
export const PROTOCOL_VERSION = '2024-11-05'; // shared by the MCP client (here) and server (mcp-server.ts)
|
|
15
15
|
const MAX_BUF = 16 * 1024 * 1024; // กัน server ส่ง byte ยาวไม่มี newline → memory โต unbounded
|
|
16
16
|
const REQUEST_TIMEOUT = 20_000;
|
|
17
|
+
export const MAX_MCP_TOOL_OUTPUT_CHARS = 200_000;
|
|
17
18
|
// env ปลอดภัยที่ส่งให้ MCP child (ไม่มี secret) — server ที่ต้อง token ให้ตั้งใน cfg.env เอง
|
|
18
19
|
const SAFE_ENV_KEYS = ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'LANG', 'LC_ALL', 'USER', 'SHELL', 'TERM', 'NODE_PATH', 'NVM_DIR', 'APPDATA'];
|
|
19
20
|
function safeEnv() {
|
|
@@ -25,10 +26,37 @@ function safeEnv() {
|
|
|
25
26
|
}
|
|
26
27
|
return out;
|
|
27
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
|
+
}
|
|
28
50
|
export function isValidMcpServerName(name) {
|
|
29
51
|
return (/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name) &&
|
|
30
52
|
!['__proto__', 'prototype', 'constructor'].includes(name));
|
|
31
53
|
}
|
|
54
|
+
export function capMcpToolOutput(text, max = MAX_MCP_TOOL_OUTPUT_CHARS) {
|
|
55
|
+
if (text.length <= max)
|
|
56
|
+
return text;
|
|
57
|
+
const omitted = text.length - max;
|
|
58
|
+
return `${text.slice(0, max)}\n\n[MCP output truncated: ${omitted} chars omitted]`;
|
|
59
|
+
}
|
|
32
60
|
/** stdio transport — JSON-RPC 2.0, newline-delimited ผ่าน child process stdin/stdout */
|
|
33
61
|
class StdioTransport {
|
|
34
62
|
proc;
|
|
@@ -36,6 +64,7 @@ class StdioTransport {
|
|
|
36
64
|
nextId = 1;
|
|
37
65
|
pending = new Map();
|
|
38
66
|
dead = false;
|
|
67
|
+
stderrTail = '';
|
|
39
68
|
constructor(cfg) {
|
|
40
69
|
this.proc = spawn(cfg.command, cfg.args ?? [], {
|
|
41
70
|
// minimal env เท่านั้น (PATH/HOME/locale) + cfg.env ที่ user ตั้งเอง — ไม่ส่ง secret
|
|
@@ -44,11 +73,19 @@ class StdioTransport {
|
|
|
44
73
|
// Windows: `npx`/`npm`/JS bins เป็น .cmd shim → spawn ตรงๆ = ENOENT. shell=true ให้ผ่าน PATHEXT.
|
|
45
74
|
// (config นี้ user เป็นเจ้าของ/trust แล้ว — bare-name resolution เท่านั้น)
|
|
46
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',
|
|
47
79
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
80
|
});
|
|
49
81
|
this.proc.stdout?.on('data', (d) => this.onData(d.toString()));
|
|
82
|
+
// ต้อง drain stderr — piped ไว้แต่ไม่อ่าน = OS pipe buffer (~64KB) เต็ม → server บล็อกตอนเขียน log = แฮงค์
|
|
83
|
+
// เก็บหางไว้ ~2KB ช่วย debug ว่า server ตายเพราะอะไร
|
|
84
|
+
this.proc.stderr?.on('data', (d) => {
|
|
85
|
+
this.stderrTail = (this.stderrTail + d.toString()).slice(-2000);
|
|
86
|
+
});
|
|
50
87
|
this.proc.on('error', () => this.fail('spawn error'));
|
|
51
|
-
this.proc.on('exit', () => this.fail('server exited'));
|
|
88
|
+
this.proc.on('exit', () => this.fail(this.stderrTail.trim() ? `server exited — ${this.stderrTail.trim().split('\n').pop()}` : 'server exited'));
|
|
52
89
|
this.proc.stdin?.on('error', () => { }); // กัน EPIPE
|
|
53
90
|
}
|
|
54
91
|
fail(reason) {
|
|
@@ -112,11 +149,21 @@ class StdioTransport {
|
|
|
112
149
|
this.proc.stdin?.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
|
|
113
150
|
}
|
|
114
151
|
close() {
|
|
152
|
+
const pid = this.proc.pid;
|
|
115
153
|
try {
|
|
116
|
-
|
|
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();
|
|
117
159
|
}
|
|
118
160
|
catch {
|
|
119
|
-
|
|
161
|
+
try {
|
|
162
|
+
this.proc.kill();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* already dead */
|
|
166
|
+
}
|
|
120
167
|
}
|
|
121
168
|
}
|
|
122
169
|
}
|
|
@@ -175,8 +222,11 @@ class HttpTransport {
|
|
|
175
222
|
const sid = res.headers.get('mcp-session-id');
|
|
176
223
|
if (sid)
|
|
177
224
|
this.sessionId = sid;
|
|
178
|
-
if (!res.ok)
|
|
179
|
-
|
|
225
|
+
if (!res.ok) {
|
|
226
|
+
const err = new Error(`mcp http ${res.status} ${res.statusText}`);
|
|
227
|
+
err.status = res.status;
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
180
230
|
const ctype = res.headers.get('content-type') ?? '';
|
|
181
231
|
if (ctype.includes('text/event-stream'))
|
|
182
232
|
return this.parseSse(await res.text(), id);
|
|
@@ -206,16 +256,16 @@ class McpClient {
|
|
|
206
256
|
constructor(cfg) {
|
|
207
257
|
this.transport = cfg.url ? new HttpTransport(cfg.url, cfg.headers) : new StdioTransport(cfg);
|
|
208
258
|
}
|
|
209
|
-
async initialize() {
|
|
259
|
+
async initialize(timeoutMs = REQUEST_TIMEOUT) {
|
|
210
260
|
await this.transport.request('initialize', {
|
|
211
261
|
protocolVersion: PROTOCOL_VERSION,
|
|
212
262
|
capabilities: {},
|
|
213
263
|
clientInfo: { name: BRAND.mcpClientName, version: VERSION },
|
|
214
|
-
});
|
|
264
|
+
}, timeoutMs);
|
|
215
265
|
this.transport.notify('notifications/initialized');
|
|
216
266
|
}
|
|
217
|
-
async listTools() {
|
|
218
|
-
const r = (await this.transport.request('tools/list'));
|
|
267
|
+
async listTools(timeoutMs = REQUEST_TIMEOUT) {
|
|
268
|
+
const r = (await this.transport.request('tools/list', undefined, timeoutMs));
|
|
219
269
|
return r?.tools ?? [];
|
|
220
270
|
}
|
|
221
271
|
async callTool(name, args) {
|
|
@@ -224,12 +274,39 @@ class McpClient {
|
|
|
224
274
|
.filter((c) => c.type === 'text')
|
|
225
275
|
.map((c) => c.text ?? '')
|
|
226
276
|
.join('\n');
|
|
227
|
-
|
|
277
|
+
const capped = capMcpToolOutput(text);
|
|
278
|
+
return r?.isError ? `MCP error: ${capped}` : capped || '(no output)';
|
|
228
279
|
}
|
|
229
280
|
close() {
|
|
230
281
|
this.transport.close();
|
|
231
282
|
}
|
|
232
283
|
}
|
|
284
|
+
export async function probeMcpServer(cfg, timeoutMs = REQUEST_TIMEOUT) {
|
|
285
|
+
const transport = cfg.url ? 'http' : 'stdio';
|
|
286
|
+
if (!cfg.url && !cfg.command)
|
|
287
|
+
return { ok: false, transport, tools: [], error: 'ต้องมี command หรือ url' };
|
|
288
|
+
const client = new McpClient(cfg);
|
|
289
|
+
const deadline = Date.now() + Math.max(1, timeoutMs);
|
|
290
|
+
const remaining = (method) => {
|
|
291
|
+
const ms = deadline - Date.now();
|
|
292
|
+
if (ms <= 0)
|
|
293
|
+
throw new Error(`mcp timeout: ${method}`);
|
|
294
|
+
return ms;
|
|
295
|
+
};
|
|
296
|
+
try {
|
|
297
|
+
await client.initialize(remaining('initialize'));
|
|
298
|
+
const tools = await client.listTools(remaining('tools/list'));
|
|
299
|
+
return { ok: true, transport, tools };
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
const error = e.message;
|
|
303
|
+
const authHints = mcpAuthHints(cfg, error);
|
|
304
|
+
return { ok: false, transport, tools: [], error, ...(authHints.length ? { authHints } : {}) };
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
client.close();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
233
310
|
function stringRecord(value) {
|
|
234
311
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
235
312
|
return undefined;
|
|
@@ -257,6 +334,10 @@ function sanitizeMcpServerConfig(raw) {
|
|
|
257
334
|
const headers = stringRecord(r.headers);
|
|
258
335
|
if (headers)
|
|
259
336
|
cfg.headers = headers;
|
|
337
|
+
if (r.enabled === false)
|
|
338
|
+
cfg.enabled = false;
|
|
339
|
+
else if (r.enabled === true)
|
|
340
|
+
cfg.enabled = true;
|
|
260
341
|
return cfg.command || cfg.url ? cfg : null;
|
|
261
342
|
}
|
|
262
343
|
async function readMcpFile(path, merged) {
|
|
@@ -294,6 +375,31 @@ export async function loadMcpConfig(onLog, cwd = process.cwd()) {
|
|
|
294
375
|
}
|
|
295
376
|
return merged;
|
|
296
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
|
+
}
|
|
297
403
|
let cachePromise = null;
|
|
298
404
|
let activeClients = []; // sync ref สำหรับ closeMcp ใน exit handler
|
|
299
405
|
/** โหลด tools จาก MCP servers — in-flight promise singleton (concurrent call ไม่ spawn ซ้ำ/leak child) */
|
|
@@ -309,6 +415,10 @@ async function buildMcpTools(onLog) {
|
|
|
309
415
|
const clients = [];
|
|
310
416
|
activeClients = clients; // ref เดียวกัน → closeMcp kill client ที่ spawn ระหว่าง build ได้ด้วย
|
|
311
417
|
for (const [serverName, cfg] of Object.entries(config)) {
|
|
418
|
+
if (!isMcpServerEnabled(cfg)) {
|
|
419
|
+
onLog?.(`MCP "${serverName}" disabled — ข้าม`);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
312
422
|
if (!cfg.url && !cfg.command) {
|
|
313
423
|
onLog?.(`MCP "${serverName}" ข้าม: ต้องมี "command" (stdio) หรือ "url" (remote)`);
|
|
314
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,58 +79,167 @@ 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) : '';
|
|
83
|
-
}
|
|
84
|
-
/** ประกอบ
|
|
85
|
-
export async function
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
return brainPath ? buildBrainContext(brainPath, { cwd }) : '';
|
|
85
|
+
}
|
|
86
|
+
/** ประกอบ source parts ชุดเดียวกับที่ inject เข้า prompt จริง — ให้ CLI inspect ได้โดยไม่ drift */
|
|
87
|
+
export async function buildBrainContextParts(brainPath, options = {}) {
|
|
88
|
+
const idx = await readTrimmedPart({
|
|
89
|
+
id: 'ai-context-index',
|
|
90
|
+
label: 'AI Context Index',
|
|
91
|
+
brainPath,
|
|
92
|
+
relPath: 'Shared/AI-Context-Index.md',
|
|
93
|
+
maxChars: 3000,
|
|
94
|
+
});
|
|
95
|
+
const currentState = await readTrimmedPart({
|
|
96
|
+
id: 'current-state',
|
|
97
|
+
label: 'Current State',
|
|
98
|
+
brainPath,
|
|
99
|
+
relPath: 'Shared/Operating-State/current-state.md',
|
|
100
|
+
maxChars: 1500,
|
|
101
|
+
wrap: (content) => `## current-state\n${content}`,
|
|
102
|
+
});
|
|
103
|
+
const inbox = await readInboxPart(brainPath, 'Shared/Memory-Inbox/memory-inbox.md', 1200);
|
|
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;
|
|
145
|
+
}
|
|
146
|
+
export function renderBrainContext(brainPath, parts) {
|
|
147
|
+
const content = parts.map((part) => part.content).filter(Boolean);
|
|
148
|
+
if (!content.length)
|
|
100
149
|
return '';
|
|
101
|
-
return `<brain_vault path="${brainPath}" note="second-brain ของ user — สิ่งที่จำไว้/state ปัจจุบันอยู่ใน block นี้; route โน้ตตาม Vault Structure Map; อ่าน/เขียนไฟล์ใน vault ด้วย absolute path ได้">\n${
|
|
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>`;
|
|
102
151
|
}
|
|
103
|
-
/**
|
|
104
|
-
async function
|
|
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));
|
|
155
|
+
}
|
|
156
|
+
/** Build a standalone context-pack block for per-turn injection (turn-retrieval path). */
|
|
157
|
+
export { buildContextPackBlock };
|
|
158
|
+
async function readTrimmedPart(input) {
|
|
159
|
+
const p = join(input.brainPath, input.relPath);
|
|
105
160
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
161
|
+
const raw = (await readFile(p, 'utf8')).trim();
|
|
162
|
+
const trimmed = raw.length > input.maxChars ? `${raw.slice(0, input.maxChars)}\n…` : raw;
|
|
163
|
+
const content = trimmed ? input.wrap?.(trimmed) ?? trimmed : '';
|
|
164
|
+
return {
|
|
165
|
+
id: input.id,
|
|
166
|
+
label: input.label,
|
|
167
|
+
relPath: input.relPath,
|
|
168
|
+
path: p,
|
|
169
|
+
content,
|
|
170
|
+
chars: content.length,
|
|
171
|
+
maxChars: input.maxChars,
|
|
172
|
+
status: content ? 'present' : 'empty',
|
|
173
|
+
};
|
|
110
174
|
}
|
|
111
175
|
catch {
|
|
112
|
-
return
|
|
176
|
+
return {
|
|
177
|
+
id: input.id,
|
|
178
|
+
label: input.label,
|
|
179
|
+
relPath: input.relPath,
|
|
180
|
+
path: p,
|
|
181
|
+
content: '',
|
|
182
|
+
chars: 0,
|
|
183
|
+
maxChars: input.maxChars,
|
|
184
|
+
status: 'missing',
|
|
185
|
+
};
|
|
113
186
|
}
|
|
114
187
|
}
|
|
115
188
|
/** ดึงรายการ "- ..." ใต้ "## New Candidates" จาก memory-inbox (fact ที่ remember ไว้) */
|
|
116
189
|
async function inboxCandidates(p, max) {
|
|
117
190
|
try {
|
|
118
|
-
|
|
119
|
-
if (!after)
|
|
120
|
-
return '';
|
|
121
|
-
const lines = after
|
|
122
|
-
.split('\n')
|
|
123
|
-
.filter((l) => l.trim().startsWith('- ') && !l.includes('_('))
|
|
124
|
-
.map((l) => l.trim());
|
|
125
|
-
const text = lines.join('\n').trim();
|
|
126
|
-
return text.length > max ? `${text.slice(0, max)}\n…` : text;
|
|
191
|
+
return inboxCandidatesFromText(await readFile(p, 'utf8'), max);
|
|
127
192
|
}
|
|
128
193
|
catch {
|
|
129
194
|
return '';
|
|
130
195
|
}
|
|
131
196
|
}
|
|
197
|
+
function inboxCandidatesFromText(content, max) {
|
|
198
|
+
const lines = content.split('\n');
|
|
199
|
+
const markerIndex = lines.findIndex((line) => line.trim() === '## New Candidates');
|
|
200
|
+
if (markerIndex === -1)
|
|
201
|
+
return '';
|
|
202
|
+
const sectionLines = [];
|
|
203
|
+
for (const line of lines.slice(markerIndex + 1)) {
|
|
204
|
+
if (/^#{1,6}\s+/.test(line.trim()))
|
|
205
|
+
break;
|
|
206
|
+
sectionLines.push(line);
|
|
207
|
+
}
|
|
208
|
+
const candidates = sectionLines
|
|
209
|
+
.filter((l) => l.trim().startsWith('- ') && !l.includes('_('))
|
|
210
|
+
.map((l) => l.trim());
|
|
211
|
+
const text = candidates.join('\n').trim();
|
|
212
|
+
return text.length > max ? `${text.slice(0, max)}\n…` : text;
|
|
213
|
+
}
|
|
214
|
+
async function readInboxPart(brainPath, relPath, maxChars) {
|
|
215
|
+
const p = join(brainPath, relPath);
|
|
216
|
+
try {
|
|
217
|
+
const content = inboxCandidatesFromText(await readFile(p, 'utf8'), maxChars);
|
|
218
|
+
const wrapped = content ? `## remembered (Memory-Inbox)\n${content}` : '';
|
|
219
|
+
return {
|
|
220
|
+
id: 'memory-inbox',
|
|
221
|
+
label: 'Memory Inbox',
|
|
222
|
+
relPath,
|
|
223
|
+
path: p,
|
|
224
|
+
content: wrapped,
|
|
225
|
+
chars: wrapped.length,
|
|
226
|
+
maxChars,
|
|
227
|
+
status: wrapped ? 'present' : 'empty',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return {
|
|
232
|
+
id: 'memory-inbox',
|
|
233
|
+
label: 'Memory Inbox',
|
|
234
|
+
relPath,
|
|
235
|
+
path: p,
|
|
236
|
+
content: '',
|
|
237
|
+
chars: 0,
|
|
238
|
+
maxChars,
|
|
239
|
+
status: 'missing',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
132
243
|
/** path ของ second-brain vault จาก config (undefined = ไม่ได้ตั้ง) */
|
|
133
244
|
export async function getBrainPath() {
|
|
134
245
|
try {
|
|
@@ -165,7 +276,7 @@ export async function appendToVaultInbox(brainPath, fact) {
|
|
|
165
276
|
}
|
|
166
277
|
/** บันทึก worklog ย่อเข้า vault Sessions/ (รายวัน) — "second brain จำว่าวันนี้ทำอะไร" */
|
|
167
278
|
export async function appendBrainWorklog(brainPath, entry) {
|
|
168
|
-
if (!worklogEnabled())
|
|
279
|
+
if (!persistenceEnabled() || !worklogEnabled())
|
|
169
280
|
return false;
|
|
170
281
|
const dir = join(brainPath, 'Sessions');
|
|
171
282
|
if (!(await exists(dir)))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { canonicalSpec, hasUsableEnvKey, PROVIDERS, parseSpec } from './providers/registry.js';
|
|
2
|
+
function statusFor(provider) {
|
|
3
|
+
const cfg = PROVIDERS[provider];
|
|
4
|
+
if (cfg.kind === 'delegate')
|
|
5
|
+
return 'delegate';
|
|
6
|
+
if (!cfg.requiresKey)
|
|
7
|
+
return 'local';
|
|
8
|
+
return hasUsableEnvKey(provider) ? 'ready' : 'needs-key';
|
|
9
|
+
}
|
|
10
|
+
function statusLabel(status) {
|
|
11
|
+
if (status === 'needs-key')
|
|
12
|
+
return 'needs key';
|
|
13
|
+
return status;
|
|
14
|
+
}
|
|
15
|
+
export function modelPickerOptions(current) {
|
|
16
|
+
const currentSpec = canonicalSpec(current);
|
|
17
|
+
return Object.entries(PROVIDERS).flatMap(([provider, cfg]) => {
|
|
18
|
+
const grouped = new Map();
|
|
19
|
+
for (const [alias, model] of Object.entries(cfg.models)) {
|
|
20
|
+
const aliases = grouped.get(model) ?? [];
|
|
21
|
+
aliases.push(alias);
|
|
22
|
+
grouped.set(model, aliases);
|
|
23
|
+
}
|
|
24
|
+
const status = statusFor(provider);
|
|
25
|
+
return [...grouped.entries()].map(([model, aliases]) => {
|
|
26
|
+
const nonDefaultAliases = aliases.filter((alias) => alias !== 'default');
|
|
27
|
+
const displayAliases = nonDefaultAliases.length ? nonDefaultAliases.join('/') : 'default';
|
|
28
|
+
const spec = `${provider}:${model}`;
|
|
29
|
+
return {
|
|
30
|
+
aliases: displayAliases,
|
|
31
|
+
current: spec === currentSpec,
|
|
32
|
+
label: `${provider}:${displayAliases}`,
|
|
33
|
+
meta: `${cfg.label} · ${statusLabel(status)}`,
|
|
34
|
+
model,
|
|
35
|
+
provider,
|
|
36
|
+
spec,
|
|
37
|
+
status,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function initialModelPickerIndex(options) {
|
|
43
|
+
const current = options.findIndex((option) => option.current);
|
|
44
|
+
return current === -1 ? 0 : current;
|
|
45
|
+
}
|
|
46
|
+
export function modelProviderEntries() {
|
|
47
|
+
return Object.entries(PROVIDERS).map(([id, cfg]) => ({
|
|
48
|
+
id,
|
|
49
|
+
label: cfg.label,
|
|
50
|
+
status: statusFor(id),
|
|
51
|
+
modelCount: new Set(Object.values(cfg.models)).size,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
export function filterModelPickerOptions(options, providerId) {
|
|
55
|
+
if (!providerId)
|
|
56
|
+
return options;
|
|
57
|
+
return options.filter((option) => option.provider === providerId);
|
|
58
|
+
}
|