sanook-cli 0.5.1 → 0.5.2
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 +57 -8
- package/README.md +240 -23
- package/README.th.md +87 -6
- package/dist/approval.js +6 -0
- package/dist/bin.js +3026 -196
- package/dist/brain-context.js +223 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +371 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +12 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +152 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/commands.js +172 -13
- package/dist/compaction.js +96 -11
- package/dist/config.js +118 -28
- package/dist/context-compression.js +191 -0
- package/dist/cost.js +49 -15
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +37 -8
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +357 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/email.js +472 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +18 -0
- 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 +343 -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/insights-args.js +35 -0
- package/dist/insights.js +86 -0
- package/dist/loop.js +123 -24
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-registry.js +350 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +44 -6
- package/dist/memory.js +100 -33
- package/dist/orchestrate.js +49 -19
- package/dist/personality.js +58 -0
- package/dist/providers/codex.js +70 -36
- package/dist/providers/keys.js +1 -1
- package/dist/providers/models.js +1 -1
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +75 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session.js +93 -7
- package/dist/skill-install.js +29 -12
- package/dist/support-dump.js +175 -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 +5 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +923 -9
- package/dist/tools/read.js +16 -4
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +217 -13
- package/dist/tools/task.js +18 -7
- package/dist/tools/timeout.js +21 -3
- package/dist/trust.js +11 -1
- package/dist/ui/app.js +48 -8
- package/dist/ui/history.js +37 -5
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/setup.js +17 -4
- package/dist/update.js +24 -11
- package/dist/worktree.js +175 -4
- package/package.json +4 -4
- 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 +3 -1
- package/second-brain/Projects/sanook-cli/_Index.md +26 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -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-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/_Index.md +6 -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 +22 -3
- 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 +4 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -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/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/orchestrate.js
CHANGED
|
@@ -1,21 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
export function formatSubagentError(e) {
|
|
3
|
+
if (e instanceof Error)
|
|
4
|
+
return e.message || e.name;
|
|
5
|
+
if (typeof e === 'string')
|
|
6
|
+
return e;
|
|
7
|
+
if (e == null)
|
|
8
|
+
return String(e);
|
|
9
|
+
try {
|
|
10
|
+
const json = JSON.stringify(e);
|
|
11
|
+
if (json)
|
|
12
|
+
return json;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return inspect(e, { breakLength: Infinity, depth: 2 });
|
|
16
|
+
}
|
|
17
|
+
return String(e);
|
|
18
|
+
}
|
|
18
19
|
const DEFAULT_CONCURRENCY = 5;
|
|
20
|
+
const DEFAULT_GLOBAL_SUBAGENT_CONCURRENCY = 16;
|
|
21
|
+
let globalInFlight = 0;
|
|
22
|
+
const globalWaiters = [];
|
|
23
|
+
function globalSubagentLimit() {
|
|
24
|
+
const raw = process.env.SANOOK_SUBAGENT_CONCURRENCY?.trim();
|
|
25
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
26
|
+
return DEFAULT_GLOBAL_SUBAGENT_CONCURRENCY;
|
|
27
|
+
const parsed = Number(raw);
|
|
28
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? Math.min(parsed, 64) : DEFAULT_GLOBAL_SUBAGENT_CONCURRENCY;
|
|
29
|
+
}
|
|
30
|
+
export function globalSubagentRunningCount() {
|
|
31
|
+
return globalInFlight;
|
|
32
|
+
}
|
|
33
|
+
export async function withGlobalSubagentSlot(fn) {
|
|
34
|
+
while (globalInFlight >= globalSubagentLimit()) {
|
|
35
|
+
await new Promise((resolve) => globalWaiters.push(resolve));
|
|
36
|
+
}
|
|
37
|
+
globalInFlight++;
|
|
38
|
+
try {
|
|
39
|
+
return await fn();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
globalInFlight--;
|
|
43
|
+
globalWaiters.shift()?.();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
19
46
|
/**
|
|
20
47
|
* Run thunks concurrently, capped at `concurrency`, results in input order.
|
|
21
48
|
* The generic concurrency primitive both runParallel and worktree isolation use.
|
|
@@ -41,7 +68,7 @@ async function runOne(spec, runner, signal) {
|
|
|
41
68
|
return { ok: true, description: spec.description, text };
|
|
42
69
|
}
|
|
43
70
|
catch (e) {
|
|
44
|
-
return { ok: false, description: spec.description, text: '', error: e
|
|
71
|
+
return { ok: false, description: spec.description, text: '', error: formatSubagentError(e) };
|
|
45
72
|
}
|
|
46
73
|
}
|
|
47
74
|
/**
|
|
@@ -89,7 +116,7 @@ export class TaskRegistry {
|
|
|
89
116
|
catch (e) {
|
|
90
117
|
const cur = this.tasks.get(id);
|
|
91
118
|
if (cur.state !== 'canceled')
|
|
92
|
-
Object.assign(cur, { state: 'error', error: e
|
|
119
|
+
Object.assign(cur, { state: 'error', error: formatSubagentError(e), endedMs: this.now() });
|
|
93
120
|
return cur;
|
|
94
121
|
}
|
|
95
122
|
finally {
|
|
@@ -116,6 +143,9 @@ export class TaskRegistry {
|
|
|
116
143
|
const settle = this.settles.get(id);
|
|
117
144
|
if (!settle)
|
|
118
145
|
return undefined;
|
|
146
|
+
const current = this.tasks.get(id);
|
|
147
|
+
if (current && current.state !== 'running')
|
|
148
|
+
return current;
|
|
119
149
|
if (timeoutMs == null)
|
|
120
150
|
return settle;
|
|
121
151
|
let timer;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const PERSONALITIES = {
|
|
2
|
+
concise: {
|
|
3
|
+
label: 'Concise',
|
|
4
|
+
prompt: 'Keep responses short, direct, and practical. Prefer the smallest useful answer.',
|
|
5
|
+
},
|
|
6
|
+
friendly: {
|
|
7
|
+
label: 'Friendly',
|
|
8
|
+
prompt: 'Use a warm, encouraging tone while staying precise and useful.',
|
|
9
|
+
},
|
|
10
|
+
formal: {
|
|
11
|
+
label: 'Formal',
|
|
12
|
+
prompt: 'Use a polished, professional tone. Avoid slang and keep recommendations structured.',
|
|
13
|
+
},
|
|
14
|
+
direct: {
|
|
15
|
+
label: 'Direct',
|
|
16
|
+
prompt: 'Be blunt, decisive, and action-oriented. Lead with the answer and avoid hedging.',
|
|
17
|
+
},
|
|
18
|
+
teacher: {
|
|
19
|
+
label: 'Teacher',
|
|
20
|
+
prompt: 'Explain the reasoning clearly and teach as you go, without becoming long-winded.',
|
|
21
|
+
},
|
|
22
|
+
researcher: {
|
|
23
|
+
label: 'Researcher',
|
|
24
|
+
prompt: 'Be evidence-minded. Distinguish facts, assumptions, and uncertainty clearly.',
|
|
25
|
+
},
|
|
26
|
+
creative: {
|
|
27
|
+
label: 'Creative',
|
|
28
|
+
prompt: 'Offer imaginative options and phrasing while keeping the implementation grounded.',
|
|
29
|
+
},
|
|
30
|
+
thai: {
|
|
31
|
+
label: 'Thai-first',
|
|
32
|
+
prompt: 'Prefer Thai for user-facing prose unless the user asks for another language.',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export function normalizePersonalityName(raw) {
|
|
36
|
+
const name = raw?.trim().toLowerCase();
|
|
37
|
+
if (!name)
|
|
38
|
+
return null;
|
|
39
|
+
if (['none', 'default', 'neutral', 'off', 'clear'].includes(name))
|
|
40
|
+
return 'none';
|
|
41
|
+
return PERSONALITIES[name] ? name : null;
|
|
42
|
+
}
|
|
43
|
+
export function personalityPrompt(name) {
|
|
44
|
+
const normalized = normalizePersonalityName(name);
|
|
45
|
+
if (!normalized || normalized === 'none')
|
|
46
|
+
return '';
|
|
47
|
+
const def = PERSONALITIES[normalized];
|
|
48
|
+
return def ? `Personality overlay (${def.label}): ${def.prompt}` : '';
|
|
49
|
+
}
|
|
50
|
+
export function personalityListText() {
|
|
51
|
+
return [
|
|
52
|
+
'personality ที่เลือกได้:',
|
|
53
|
+
' none — ปิด personality overlay',
|
|
54
|
+
...Object.entries(PERSONALITIES).map(([name, def]) => ` ${name} — ${def.label}`),
|
|
55
|
+
'',
|
|
56
|
+
'ใช้: /personality <name>',
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
package/dist/providers/codex.js
CHANGED
|
@@ -2,6 +2,9 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
export function codexHome() {
|
|
6
|
+
return process.env.CODEX_HOME?.trim() || join(homedir(), '.codex');
|
|
7
|
+
}
|
|
5
8
|
/** เช็กว่า codex CLI ติดตั้ง + login ChatGPT แล้ว */
|
|
6
9
|
export async function detectCodex() {
|
|
7
10
|
const hasBinary = await new Promise((resolve) => {
|
|
@@ -24,7 +27,7 @@ export async function detectCodex() {
|
|
|
24
27
|
return { installed: false, loggedIn: false, reason: 'ไม่พบ codex CLI — ติดตั้ง: npm i -g @openai/codex' };
|
|
25
28
|
}
|
|
26
29
|
try {
|
|
27
|
-
const auth = JSON.parse(await readFile(join(
|
|
30
|
+
const auth = JSON.parse(await readFile(join(codexHome(), 'auth.json'), 'utf8'));
|
|
28
31
|
const loggedIn = auth?.auth_mode === 'chatgpt' || Boolean(auth?.tokens?.access_token);
|
|
29
32
|
return { installed: true, loggedIn, reason: loggedIn ? undefined : 'ยังไม่ได้ login — รัน: codex login' };
|
|
30
33
|
}
|
|
@@ -37,8 +40,8 @@ export async function detectCodex() {
|
|
|
37
40
|
* tolerant ต่อ malformed JSONL (codex bug #15451: --json ถูก ignore เมื่อมี tools active)
|
|
38
41
|
*/
|
|
39
42
|
export async function runCodex(opts) {
|
|
40
|
-
//
|
|
41
|
-
const args = ['exec', '--skip-git-repo-check', '--sandbox', opts.sandbox ?? 'read-only', '--
|
|
43
|
+
// `codex exec` is already non-interactive; sandbox controls whether generated commands may write.
|
|
44
|
+
const args = ['exec', '--skip-git-repo-check', '--sandbox', opts.sandbox ?? 'read-only', '--json'];
|
|
42
45
|
if (opts.model)
|
|
43
46
|
args.push('-m', opts.model);
|
|
44
47
|
if (opts.resumeThreadId)
|
|
@@ -49,52 +52,83 @@ export async function runCodex(opts) {
|
|
|
49
52
|
// (codex bug #2733/#3286: ตั้ง OPENAI_API_KEY ค้าง env ทำให้ ChatGPT-plan auth วน loop sign-in)
|
|
50
53
|
const env = { ...process.env };
|
|
51
54
|
delete env.OPENAI_API_KEY;
|
|
52
|
-
const p = spawn('codex', args, { env, shell: process.platform === 'win32' }); // Windows: codex = JS shim ผ่าน .cmd → ต้อง shell
|
|
55
|
+
const p = spawn('codex', args, { env, cwd: opts.cwd, shell: process.platform === 'win32' }); // Windows: codex = JS shim ผ่าน .cmd → ต้อง shell
|
|
53
56
|
let finalText = '';
|
|
54
57
|
let threadId;
|
|
55
58
|
let buf = '';
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
let stderr = '';
|
|
60
|
+
let aborted = false;
|
|
61
|
+
const handleStdoutLine = (line) => {
|
|
62
|
+
const t = line.trim();
|
|
63
|
+
if (!t)
|
|
64
|
+
return;
|
|
65
|
+
if (!t.startsWith('{')) {
|
|
66
|
+
// plain stdout fallback (JSONL ถูก ignore) — เก็บเป็น final text
|
|
67
|
+
finalText += (finalText ? '\n' : '') + t;
|
|
68
|
+
opts.onEvent?.({ type: 'text', text: finalText });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const ev = JSON.parse(t);
|
|
73
|
+
if (ev.type === 'thread.started' && ev.thread_id) {
|
|
74
|
+
threadId = ev.thread_id;
|
|
75
|
+
opts.onEvent?.({ type: 'thread', threadId });
|
|
76
|
+
}
|
|
77
|
+
else if (ev.type === 'item.completed' && ev.item?.type === 'agent_message') {
|
|
78
|
+
finalText = ev.item.text ?? finalText;
|
|
79
|
+
opts.onEvent?.({ type: 'text', text: finalText });
|
|
80
|
+
}
|
|
81
|
+
else if (ev.type === 'turn.completed') {
|
|
82
|
+
opts.onEvent?.({ type: 'usage', usage: ev.usage });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// malformed JSON line — ข้าม
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const abortHandler = () => {
|
|
90
|
+
aborted = true;
|
|
91
|
+
p.kill();
|
|
92
|
+
};
|
|
93
|
+
const cleanupAbortHandler = () => opts.signal?.removeEventListener('abort', abortHandler);
|
|
94
|
+
// codex อาจตายระหว่างรับ prompt → write ลง stdin ที่ปิดแล้ว = EPIPE; ถ้าไม่ดัก = crash ทั้ง CLI
|
|
95
|
+
// (close handler ด้านล่าง reject error ที่อ่านรู้เรื่องแทน)
|
|
96
|
+
p.stdin.on('error', () => { });
|
|
97
|
+
if (opts.signal?.aborted)
|
|
98
|
+
abortHandler();
|
|
99
|
+
else
|
|
100
|
+
opts.signal?.addEventListener('abort', abortHandler, { once: true });
|
|
101
|
+
if (!aborted) {
|
|
102
|
+
p.stdin.write(opts.prompt);
|
|
103
|
+
p.stdin.end();
|
|
104
|
+
}
|
|
59
105
|
p.stdout.on('data', (chunk) => {
|
|
60
106
|
buf += chunk.toString();
|
|
61
107
|
const lines = buf.split('\n');
|
|
62
108
|
buf = lines.pop() ?? '';
|
|
63
109
|
for (const line of lines) {
|
|
64
|
-
|
|
65
|
-
if (!t)
|
|
66
|
-
continue;
|
|
67
|
-
if (!t.startsWith('{')) {
|
|
68
|
-
// plain stdout fallback (JSONL ถูก ignore) — เก็บเป็น final text
|
|
69
|
-
finalText += (finalText ? '\n' : '') + t;
|
|
70
|
-
opts.onEvent?.({ type: 'text', text: finalText });
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
const ev = JSON.parse(t);
|
|
75
|
-
if (ev.type === 'thread.started' && ev.thread_id) {
|
|
76
|
-
threadId = ev.thread_id;
|
|
77
|
-
opts.onEvent?.({ type: 'thread', threadId });
|
|
78
|
-
}
|
|
79
|
-
else if (ev.type === 'item.completed' && ev.item?.type === 'agent_message') {
|
|
80
|
-
finalText = ev.item.text ?? finalText;
|
|
81
|
-
opts.onEvent?.({ type: 'text', text: finalText });
|
|
82
|
-
}
|
|
83
|
-
else if (ev.type === 'turn.completed') {
|
|
84
|
-
opts.onEvent?.({ type: 'usage', usage: ev.usage });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// malformed JSON line — ข้าม
|
|
89
|
-
}
|
|
110
|
+
handleStdoutLine(line);
|
|
90
111
|
}
|
|
91
112
|
});
|
|
92
|
-
p.on('
|
|
113
|
+
p.stderr.on('data', (chunk) => {
|
|
114
|
+
stderr += chunk.toString();
|
|
115
|
+
if (stderr.length > 4000)
|
|
116
|
+
stderr = stderr.slice(-4000);
|
|
117
|
+
});
|
|
118
|
+
p.on('error', (err) => {
|
|
119
|
+
cleanupAbortHandler();
|
|
120
|
+
reject(new Error(`เรียก codex ไม่ได้: ${err.message}`));
|
|
121
|
+
});
|
|
93
122
|
p.on('close', (code) => {
|
|
94
|
-
|
|
123
|
+
cleanupAbortHandler();
|
|
124
|
+
handleStdoutLine(buf);
|
|
125
|
+
buf = '';
|
|
126
|
+
if (aborted)
|
|
127
|
+
reject(new Error(`codex exec ถูกยกเลิก${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
|
|
128
|
+
else if (code === 0)
|
|
95
129
|
resolve({ text: finalText.trim(), threadId });
|
|
96
130
|
else
|
|
97
|
-
reject(new Error(`codex exec จบด้วย exit code ${code}`));
|
|
131
|
+
reject(new Error(`codex exec จบด้วย exit code ${code}${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
|
|
98
132
|
});
|
|
99
133
|
});
|
|
100
134
|
}
|
package/dist/providers/keys.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/** อ่าน API key จาก env (หลัก + fallbacks) — keychain เป็น enhancement ทีหลัง */
|
|
9
9
|
export function resolveKeyFromEnv(envVar, fallbacks = []) {
|
|
10
10
|
for (const name of [envVar, ...fallbacks]) {
|
|
11
|
-
const v = process.env[name];
|
|
11
|
+
const v = process.env[name]?.trim();
|
|
12
12
|
if (v)
|
|
13
13
|
return v;
|
|
14
14
|
}
|
package/dist/providers/models.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function listRemoteModels(cfg, key, timeoutMs = 6000) {
|
|
|
49
49
|
*/
|
|
50
50
|
export function mergeModelOptions(cfg, remote = []) {
|
|
51
51
|
// group alias ทั้งหมดตาม id (รวม 'default' ด้วย — กัน id ที่มีแต่ alias 'default' เช่น lmstudio:local-model,
|
|
52
|
-
// ollama:
|
|
52
|
+
// ollama:llama3.3 หายไปจนเลือกไม่ได้/Select ว่าง). ตอนทำ label ค่อยซ่อนคำ "default" ถ้ามีชื่ออื่นอยู่แล้ว
|
|
53
53
|
const aliasesById = new Map();
|
|
54
54
|
const order = []; // คง first-seen order ของ id
|
|
55
55
|
for (const [alias, id] of Object.entries(cfg.models)) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
2
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
3
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
|
-
import { createDeepSeek } from '@ai-sdk/deepseek';
|
|
5
4
|
import { createXai } from '@ai-sdk/xai';
|
|
6
5
|
import { createMistral } from '@ai-sdk/mistral';
|
|
7
6
|
import { createGroq } from '@ai-sdk/groq';
|
|
@@ -71,16 +70,6 @@ export const PROVIDERS = {
|
|
|
71
70
|
create: (key, baseURL) => createOpenAI({ apiKey: key, baseURL }),
|
|
72
71
|
note: 'Bearer key. org/project ผ่าน env. ห้าม reuse ChatGPT/Codex OAuth',
|
|
73
72
|
},
|
|
74
|
-
deepseek: {
|
|
75
|
-
id: 'deepseek',
|
|
76
|
-
label: 'DeepSeek',
|
|
77
|
-
envVar: 'DEEPSEEK_API_KEY',
|
|
78
|
-
requiresKey: true,
|
|
79
|
-
keyFormat: null, // opaque sk- → ข้าม format check
|
|
80
|
-
// V4 ids (doc audit มิ.ย. 2026): deepseek-chat/deepseek-reasoner เลิกใช้ 2026-07-24 → redirect มา V4 (dual thinking-mode)
|
|
81
|
-
models: { default: 'deepseek-v4-flash', smart: 'deepseek-v4-pro', fast: 'deepseek-v4-flash' },
|
|
82
|
-
create: (key) => createDeepSeek({ apiKey: key }),
|
|
83
|
-
},
|
|
84
73
|
xai: {
|
|
85
74
|
id: 'xai',
|
|
86
75
|
label: 'xAI Grok',
|
|
@@ -120,7 +109,7 @@ export const PROVIDERS = {
|
|
|
120
109
|
requiresKey: false,
|
|
121
110
|
localPlaceholderKey: 'ollama',
|
|
122
111
|
keyFormat: null,
|
|
123
|
-
models: { default: '
|
|
112
|
+
models: { default: 'llama3.3', llama: 'llama3.3', mistral: 'mistral' },
|
|
124
113
|
create: (key, baseURL) => createOpenAICompatible({ name: 'ollama', apiKey: key, baseURL: baseURL ?? 'http://localhost:11434/v1' }),
|
|
125
114
|
note: 'OpenAI-compat /v1 endpoint. ไม่ต้อง key',
|
|
126
115
|
},
|
|
@@ -136,32 +125,6 @@ export const PROVIDERS = {
|
|
|
136
125
|
create: (key, baseURL) => createOpenAICompatible({ name: 'lmstudio', apiKey: key, baseURL: baseURL ?? 'http://localhost:1234/v1' }),
|
|
137
126
|
note: 'ต้อง Start Server ในแอปก่อน; โหลด model เดียว ใส่ id อะไรก็ serve ตัวนั้น',
|
|
138
127
|
},
|
|
139
|
-
// ── Cloud BYOK (OpenAI-compatible, จีน — data residency, ไม่มี OAuth landmine) ──
|
|
140
|
-
minimax: {
|
|
141
|
-
id: 'minimax',
|
|
142
|
-
label: 'MiniMax',
|
|
143
|
-
envVar: 'MINIMAX_API_KEY',
|
|
144
|
-
baseURL: 'https://api.minimax.io/v1',
|
|
145
|
-
requiresKey: true,
|
|
146
|
-
keyFormat: null, // opaque
|
|
147
|
-
models: { default: 'MiniMax-M2.7', smart: 'MiniMax-M3', fast: 'MiniMax-M2.7' },
|
|
148
|
-
create: (key, baseURL) => createOpenAICompatible({ name: 'minimax', apiKey: key, baseURL: baseURL ?? 'https://api.minimax.io/v1' }),
|
|
149
|
-
note: 'OpenAI-compat /v1. data จีน. MINIMAX_BASE_URL override (intl ↔ api.minimaxi.com/v1)',
|
|
150
|
-
},
|
|
151
|
-
glm: {
|
|
152
|
-
id: 'glm',
|
|
153
|
-
label: 'GLM (z.ai / Zhipu Coding Plan)',
|
|
154
|
-
envVar: 'ZHIPU_API_KEY',
|
|
155
|
-
envFallbacks: ['ZAI_API_KEY', 'GLM_API_KEY'],
|
|
156
|
-
// Coding Plan (subscription) ใช้ Anthropic Messages API — เหมือนที่ต่อกับ Claude Code.
|
|
157
|
-
// pay-as-you-go /paas/v4 (OpenAI-compat) มีแค่ glm-4.5-flash ฟรี ที่เหลือ 429 ถ้าไม่มี balance
|
|
158
|
-
baseURL: 'https://api.z.ai/api/anthropic/v1',
|
|
159
|
-
requiresKey: true,
|
|
160
|
-
keyFormat: null, // opaque ({id}.{secret})
|
|
161
|
-
models: { default: 'glm-4.6', smart: 'glm-5.1', air: 'glm-4.5-air', glm: 'glm-4.6' },
|
|
162
|
-
create: (key, baseURL) => createAnthropic({ apiKey: key, baseURL: baseURL ?? 'https://api.z.ai/api/anthropic/v1' }),
|
|
163
|
-
note: 'z.ai Coding Plan ผ่าน Anthropic Messages API. GLM_BASE_URL override → open.bigmodel.cn/api/anthropic/v1 (จีน)',
|
|
164
|
-
},
|
|
165
128
|
// ── Delegate: OpenAI Codex ผ่าน ChatGPT plan quota (wrap official codex CLI, ToS-safe) ──
|
|
166
129
|
codex: {
|
|
167
130
|
id: 'codex',
|
|
@@ -191,13 +154,10 @@ const GLOBAL_ALIAS = {
|
|
|
191
154
|
gemini: { provider: 'google', alias: 'gemini' },
|
|
192
155
|
flash: { provider: 'google', alias: 'flash' },
|
|
193
156
|
grok: { provider: 'xai', alias: 'grok' },
|
|
194
|
-
deepseek: { provider: 'deepseek', alias: 'default' },
|
|
195
157
|
mistral: { provider: 'mistral', alias: 'default' },
|
|
196
158
|
groq: { provider: 'groq', alias: 'default' },
|
|
197
159
|
ollama: { provider: 'ollama', alias: 'default' },
|
|
198
160
|
lmstudio: { provider: 'lmstudio', alias: 'default' },
|
|
199
|
-
glm: { provider: 'glm', alias: 'default' },
|
|
200
|
-
minimax: { provider: 'minimax', alias: 'default' },
|
|
201
161
|
};
|
|
202
162
|
/** parse "provider:model" | "provider:alias" | alias | "model" (default anthropic) */
|
|
203
163
|
export function parseSpec(spec) {
|
|
@@ -221,17 +181,18 @@ export function specKey(spec) {
|
|
|
221
181
|
const { provider, model } = parseSpec(spec);
|
|
222
182
|
return `${provider}:${model}`;
|
|
223
183
|
}
|
|
184
|
+
/** canonical display/state spec: aliases become "provider:model-id" before reaching the REPL state. */
|
|
185
|
+
export function canonicalSpec(spec) {
|
|
186
|
+
return specKey(spec.trim());
|
|
187
|
+
}
|
|
224
188
|
/** หน้า console ที่ใช้สร้าง API key ต่อ provider — โชว์ในข้อความ error/wizard ("ไปเอา key ที่ไหน") */
|
|
225
189
|
const CONSOLE_URLS = {
|
|
226
190
|
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
227
191
|
google: 'https://aistudio.google.com/apikey',
|
|
228
192
|
openai: 'https://platform.openai.com/api-keys',
|
|
229
|
-
deepseek: 'https://platform.deepseek.com/api_keys',
|
|
230
193
|
xai: 'https://console.x.ai',
|
|
231
194
|
mistral: 'https://console.mistral.ai/api-keys',
|
|
232
195
|
groq: 'https://console.groq.com/keys',
|
|
233
|
-
minimax: 'https://platform.minimax.io',
|
|
234
|
-
glm: 'https://z.ai/manage-apikey/apikey-list',
|
|
235
196
|
};
|
|
236
197
|
export function consoleUrl(provider) {
|
|
237
198
|
return CONSOLE_URLS[provider];
|
|
@@ -245,6 +206,8 @@ export function hasUsableEnvKey(provider) {
|
|
|
245
206
|
const cfg = PROVIDERS[provider];
|
|
246
207
|
if (!cfg)
|
|
247
208
|
return false;
|
|
209
|
+
if (cfg.kind === 'delegate')
|
|
210
|
+
return false; // ต้องเช็ก readiness แยก (เช่น codex CLI installed + logged in)
|
|
248
211
|
if (!cfg.requiresKey)
|
|
249
212
|
return true; // local — ไม่ต้อง key
|
|
250
213
|
const k = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
@@ -260,7 +223,7 @@ export function hasUsableEnvKey(provider) {
|
|
|
260
223
|
}
|
|
261
224
|
/** หา provider ที่ "มี key ใช้ได้จริงใน env" (cloud, ตามลำดับนิยม) — ใช้ทำ first-run smart skip + แนะ headless */
|
|
262
225
|
export function detectEnvProvider() {
|
|
263
|
-
for (const id of ['anthropic', 'openai', 'google', '
|
|
226
|
+
for (const id of ['anthropic', 'openai', 'google', 'xai', 'mistral', 'groq']) {
|
|
264
227
|
const cfg = PROVIDERS[id];
|
|
265
228
|
if (cfg?.requiresKey && hasUsableEnvKey(id)) {
|
|
266
229
|
return { provider: id, label: cfg.label, envVar: cfg.envVar, model: cfg.models.default };
|
|
@@ -292,10 +255,14 @@ export function resolveModel(spec) {
|
|
|
292
255
|
const found = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
293
256
|
if (!found) {
|
|
294
257
|
const url = consoleUrl(provider);
|
|
258
|
+
const codexHint = provider === 'openai'
|
|
259
|
+
? `\n • ถ้าต้องการใช้ ChatGPT plan ไม่ใช้ API key: เลือก \`/model codex\` แล้วรัน \`codex login\``
|
|
260
|
+
: '';
|
|
295
261
|
throw new Error(`ยังไม่มี API key ของ ${cfg.label} (${cfg.envVar})\n` +
|
|
296
262
|
(url ? ` • เอา key ที่: ${url}\n` : '') +
|
|
297
263
|
` • ตั้ง: export ${cfg.envVar}="..." ` +
|
|
298
|
-
`หรือรัน \`${BRAND.cliName}\` (ไม่ใส่ task) เพื่อ setup wizard`
|
|
264
|
+
`หรือรัน \`${BRAND.cliName}\` (ไม่ใส่ task) เพื่อ setup wizard` +
|
|
265
|
+
codexHint);
|
|
299
266
|
}
|
|
300
267
|
assertDirectApiKey(cfg, found); // reject OAuth/subscription token + format ผิด
|
|
301
268
|
key = found;
|
|
@@ -303,7 +270,7 @@ export function resolveModel(spec) {
|
|
|
303
270
|
else {
|
|
304
271
|
key = resolveKeyFromEnv(cfg.envVar) ?? cfg.localPlaceholderKey ?? 'local';
|
|
305
272
|
}
|
|
306
|
-
// <PROVIDER>_BASE_URL env → override (สลับ region
|
|
273
|
+
// <PROVIDER>_BASE_URL env → override (สลับ region); ไม่งั้น local อ่าน env, cloud ใช้ default
|
|
307
274
|
const baseURL = process.env[`${cfg.id.toUpperCase()}_BASE_URL`] ??
|
|
308
275
|
(cfg.requiresKey ? cfg.baseURL : process.env[cfg.envVar] ?? cfg.baseURL);
|
|
309
276
|
return cfg.create(key, baseURL)(model);
|
package/dist/search/chunk.js
CHANGED
|
@@ -12,15 +12,11 @@
|
|
|
12
12
|
// or a stray [[ inside a code fence degrade to "no frontmatter / no links"
|
|
13
13
|
// rather than throwing. We must never block indexing a real, messy vault file.
|
|
14
14
|
// ============================================================================
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
15
16
|
const MIN_CHARS = 120; // sections shorter than this fold into the next chunk
|
|
16
|
-
/** deterministic
|
|
17
|
+
/** deterministic path hash — SHA-256 prefix keeps chunk ids short without 32-bit collision risk. */
|
|
17
18
|
export function pathHash(path) {
|
|
18
|
-
|
|
19
|
-
for (let i = 0; i < path.length; i++) {
|
|
20
|
-
h ^= path.charCodeAt(i);
|
|
21
|
-
h = Math.imul(h, 0x01000193);
|
|
22
|
-
}
|
|
23
|
-
return (h >>> 0).toString(36);
|
|
19
|
+
return createHash('sha256').update(path).digest('hex').slice(0, 16);
|
|
24
20
|
}
|
|
25
21
|
/** split a leading `---\n…\n---` frontmatter block from the body. Defensive: no block ⇒ {} + full md. */
|
|
26
22
|
export function parseFrontmatter(md) {
|
|
@@ -31,7 +27,10 @@ export function parseFrontmatter(md) {
|
|
|
31
27
|
if (end === -1)
|
|
32
28
|
return { data: empty, body: md };
|
|
33
29
|
const block = md.slice(3, end).trim();
|
|
34
|
-
|
|
30
|
+
// หา newline หลัง closing fence; ถ้าไม่มี (frontmatter-only ไม่มี trailing newline) body = '' ไม่ใช่ทั้งไฟล์
|
|
31
|
+
// (indexOf คืน -1 → slice(0) = ทั้งไฟล์ → frontmatter รั่วเข้า body ทำ index/search เพี้ยน)
|
|
32
|
+
const afterFence = md.indexOf('\n', end + 1);
|
|
33
|
+
const body = (afterFence === -1 ? '' : md.slice(afterFence + 1)).replace(/^\n+/, '');
|
|
35
34
|
const data = { tags: [] };
|
|
36
35
|
const lines = block.split('\n');
|
|
37
36
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { inlineValue, takeValue } from '../cli-option-values.js';
|
|
2
|
+
import { SEARCH_SOURCES } from './index-core.js';
|
|
3
|
+
const SEARCH_MODES = ['auto', 'fts', 'semantic', 'hybrid'];
|
|
4
|
+
function isSearchMode(v) {
|
|
5
|
+
return SEARCH_MODES.includes(v);
|
|
6
|
+
}
|
|
7
|
+
function isSearchSource(v) {
|
|
8
|
+
return SEARCH_SOURCES.includes(v);
|
|
9
|
+
}
|
|
10
|
+
function parsePositiveInteger(raw) {
|
|
11
|
+
if (!raw || !/^[1-9]\d*$/.test(raw))
|
|
12
|
+
return undefined;
|
|
13
|
+
const n = Number(raw);
|
|
14
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
15
|
+
}
|
|
16
|
+
function inlineSourceValue(value) {
|
|
17
|
+
return inlineValue('--source', value) ?? inlineValue('--sources', value);
|
|
18
|
+
}
|
|
19
|
+
export function parseSearchArgs(args) {
|
|
20
|
+
const queryParts = [];
|
|
21
|
+
let mode = 'auto';
|
|
22
|
+
let limit = 8;
|
|
23
|
+
let sources;
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
const a = args[i];
|
|
26
|
+
if (a === '--') {
|
|
27
|
+
queryParts.push(...args.slice(i + 1));
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
else if (a === '--mode' || a.startsWith('--mode=')) {
|
|
31
|
+
const next = a === '--mode' ? takeValue(args, i) : undefined;
|
|
32
|
+
const v = next ? next.value : inlineValue('--mode', a);
|
|
33
|
+
if (next)
|
|
34
|
+
i = next.nextIndex;
|
|
35
|
+
if (!v)
|
|
36
|
+
return { ok: false, message: `--mode ต้องระบุค่าเป็น ${SEARCH_MODES.join('|')}` };
|
|
37
|
+
if (!isSearchMode(v))
|
|
38
|
+
return { ok: false, message: `--mode ต้องเป็น ${SEARCH_MODES.join('|')}` };
|
|
39
|
+
mode = v;
|
|
40
|
+
}
|
|
41
|
+
else if (a === '--limit' || a.startsWith('--limit=')) {
|
|
42
|
+
const next = a === '--limit' ? takeValue(args, i) : undefined;
|
|
43
|
+
const raw = next ? next.value : inlineValue('--limit', a);
|
|
44
|
+
if (next)
|
|
45
|
+
i = next.nextIndex;
|
|
46
|
+
if (!raw)
|
|
47
|
+
return { ok: false, message: '--limit ต้องระบุค่าเป็น integer บวก เช่น 8' };
|
|
48
|
+
const n = parsePositiveInteger(raw);
|
|
49
|
+
if (n === undefined)
|
|
50
|
+
return { ok: false, message: '--limit ต้องเป็น integer บวก เช่น 8' };
|
|
51
|
+
limit = n;
|
|
52
|
+
}
|
|
53
|
+
else if (a === '--source' || a === '--sources' || a.startsWith('--source=') || a.startsWith('--sources=')) {
|
|
54
|
+
const next = a === '--source' || a === '--sources' ? takeValue(args, i) : undefined;
|
|
55
|
+
const raw = next ? next.value : inlineSourceValue(a);
|
|
56
|
+
if (next)
|
|
57
|
+
i = next.nextIndex;
|
|
58
|
+
const requested = (raw ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
59
|
+
const bad = requested.filter((s) => !isSearchSource(s));
|
|
60
|
+
if (!requested.length) {
|
|
61
|
+
return { ok: false, message: `--source ต้องระบุค่าเป็น ${SEARCH_SOURCES.join(',')} (คั่นหลายค่าได้ด้วย comma)` };
|
|
62
|
+
}
|
|
63
|
+
if (bad.length)
|
|
64
|
+
return { ok: false, message: `--source ต้องเป็น ${SEARCH_SOURCES.join(',')} (คั่นหลายค่าได้ด้วย comma)` };
|
|
65
|
+
sources = [...new Set(requested)];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
queryParts.push(a);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const query = queryParts.join(' ').trim();
|
|
72
|
+
if (!query)
|
|
73
|
+
return { ok: false, message: 'ต้องใส่ query สำหรับค้นหา' };
|
|
74
|
+
return { ok: true, value: { query, mode, limit, sources } };
|
|
75
|
+
}
|
|
@@ -139,6 +139,9 @@ export async function saveVectors(vi) {
|
|
|
139
139
|
throw e;
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
export function invalidateVectors(tag = '') {
|
|
143
|
+
return saveVectors(emptyVectors(tag));
|
|
144
|
+
}
|
|
142
145
|
export async function vectorsMtimeMs() {
|
|
143
146
|
try {
|
|
144
147
|
return (await stat(VECTORS_PATH)).mtimeMs;
|
package/dist/search/indexer.js
CHANGED
|
@@ -25,6 +25,7 @@ import { activeFacts, effImportance, loadStore } from '../memory-store.js';
|
|
|
25
25
|
import { chunkMarkdown } from './chunk.js';
|
|
26
26
|
import { addDoc, removeDoc, removeSource } from './index-core.js';
|
|
27
27
|
import { loadIndex, saveIndex } from './store.js';
|
|
28
|
+
import { buildVectorIndex, embedTexts, getEmbedder, invalidateVectors, saveVectors } from './embed-store.js';
|
|
28
29
|
/** strip a .md path to a human title fallback when a chunk has no heading. */
|
|
29
30
|
function fileTitle(rel) {
|
|
30
31
|
return (rel.split('/').pop() ?? rel).replace(/\.md$/i, '');
|
|
@@ -130,6 +131,21 @@ export function foldSkills(index, skills) {
|
|
|
130
131
|
}
|
|
131
132
|
return skills.length;
|
|
132
133
|
}
|
|
134
|
+
function docEmbeddingText(doc) {
|
|
135
|
+
return [doc.title?.trim(), doc.text.trim()].filter(Boolean).join('\n').slice(0, 4000);
|
|
136
|
+
}
|
|
137
|
+
export async function vectorizeIndex(index, tag, embed) {
|
|
138
|
+
const docs = [...index.docs.values()]
|
|
139
|
+
.filter((d) => d.text.trim())
|
|
140
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
141
|
+
if (!docs.length)
|
|
142
|
+
return buildVectorIndex(tag, []);
|
|
143
|
+
const vectors = await embed(docs.map(docEmbeddingText));
|
|
144
|
+
if (vectors.length !== docs.length) {
|
|
145
|
+
throw new Error(`embedding count mismatch: expected ${docs.length}, got ${vectors.length}`);
|
|
146
|
+
}
|
|
147
|
+
return buildVectorIndex(tag, docs.map((d, i) => ({ id: d.id, vec: vectors[i] })));
|
|
148
|
+
}
|
|
133
149
|
// ---- real-filesystem wiring ------------------------------------------------
|
|
134
150
|
const IGNORE_DIRS = new Set([
|
|
135
151
|
'node_modules', 'dist', 'build', 'coverage', '.next', '.cache', '.git',
|
|
@@ -176,6 +192,15 @@ export function nodeVaultFS(root) {
|
|
|
176
192
|
};
|
|
177
193
|
}
|
|
178
194
|
const SESSIONS_DIR = appHomePath('sessions');
|
|
195
|
+
async function configEmbeddingModel() {
|
|
196
|
+
try {
|
|
197
|
+
const cfg = JSON.parse(await readFile(appHomePath('config.json'), 'utf8'));
|
|
198
|
+
return cfg.embeddingModel;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
179
204
|
/** load first-user-message of the most recent sessions (bounded) for the session corpus. */
|
|
180
205
|
export async function loadRecentSessions(limit = 60) {
|
|
181
206
|
const out = [];
|
|
@@ -237,5 +262,23 @@ export async function reindex(now = Date.now()) {
|
|
|
237
262
|
text: `${s.description} ${s.whenToUse ?? ''}`.trim(),
|
|
238
263
|
})));
|
|
239
264
|
await saveIndex(index, nextManifest);
|
|
240
|
-
|
|
265
|
+
let vectors = 0;
|
|
266
|
+
const embedder = getEmbedder(process.env.SANOOK_EMBEDDING_MODEL ?? (await configEmbeddingModel()));
|
|
267
|
+
if (!embedder) {
|
|
268
|
+
await invalidateVectors().catch(() => { });
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
try {
|
|
272
|
+
const vi = await vectorizeIndex(index, embedder.tag, (texts) => embedTexts(embedder, texts));
|
|
273
|
+
await saveVectors(vi);
|
|
274
|
+
vectors = vi.ids.length;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Semantic search is optional. A provider/network failure must never break
|
|
278
|
+
// the BM25 floor. Clear stale vectors so hybrid/semantic cannot rank the
|
|
279
|
+
// freshly-saved BM25 index with embeddings from an older corpus.
|
|
280
|
+
await invalidateVectors(embedder.tag).catch(() => { });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { ...diff, memory, sessions, skills, vectors, vaultPath: brain ?? null };
|
|
241
284
|
}
|