sanook-cli 0.5.0 → 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 +83 -5
- package/README.md +240 -23
- package/README.th.md +87 -6
- package/dist/approval.js +6 -0
- package/dist/bin.js +3045 -210
- 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 +86 -38
- package/dist/providers/keys.js +1 -1
- package/dist/providers/models.js +22 -6
- package/dist/providers/registry.js +38 -49
- 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 +57 -11
- package/dist/ui/brain-wizard.js +2 -2
- package/dist/ui/history.js +37 -5
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/render.js +55 -15
- package/dist/ui/setup.js +107 -10
- 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,18 +2,32 @@ 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) => {
|
|
8
11
|
const p = spawn('codex', ['--version'], { shell: process.platform === 'win32' });
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
// timeout: binary ค้าง (shim รอ stdin / Gatekeeper stall ตอนรันครั้งแรกบน macOS) → ไม่ให้ wizard ตัน
|
|
13
|
+
const timer = setTimeout(() => {
|
|
14
|
+
p.kill();
|
|
15
|
+
resolve(false);
|
|
16
|
+
}, 5000);
|
|
17
|
+
p.on('error', () => {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
resolve(false);
|
|
20
|
+
});
|
|
21
|
+
p.on('close', (code) => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
resolve(code === 0);
|
|
24
|
+
});
|
|
11
25
|
});
|
|
12
26
|
if (!hasBinary) {
|
|
13
27
|
return { installed: false, loggedIn: false, reason: 'ไม่พบ codex CLI — ติดตั้ง: npm i -g @openai/codex' };
|
|
14
28
|
}
|
|
15
29
|
try {
|
|
16
|
-
const auth = JSON.parse(await readFile(join(
|
|
30
|
+
const auth = JSON.parse(await readFile(join(codexHome(), 'auth.json'), 'utf8'));
|
|
17
31
|
const loggedIn = auth?.auth_mode === 'chatgpt' || Boolean(auth?.tokens?.access_token);
|
|
18
32
|
return { installed: true, loggedIn, reason: loggedIn ? undefined : 'ยังไม่ได้ login — รัน: codex login' };
|
|
19
33
|
}
|
|
@@ -26,6 +40,7 @@ export async function detectCodex() {
|
|
|
26
40
|
* tolerant ต่อ malformed JSONL (codex bug #15451: --json ถูก ignore เมื่อมี tools active)
|
|
27
41
|
*/
|
|
28
42
|
export async function runCodex(opts) {
|
|
43
|
+
// `codex exec` is already non-interactive; sandbox controls whether generated commands may write.
|
|
29
44
|
const args = ['exec', '--skip-git-repo-check', '--sandbox', opts.sandbox ?? 'read-only', '--json'];
|
|
30
45
|
if (opts.model)
|
|
31
46
|
args.push('-m', opts.model);
|
|
@@ -33,54 +48,87 @@ export async function runCodex(opts) {
|
|
|
33
48
|
args.push('resume', opts.resumeThreadId);
|
|
34
49
|
args.push('-'); // prompt via stdin
|
|
35
50
|
return new Promise((resolve, reject) => {
|
|
36
|
-
// OPENAI_API_KEY
|
|
37
|
-
|
|
38
|
-
const
|
|
51
|
+
// ลบ OPENAI_API_KEY ออกจาก env ของ child — กัน BYOK key ของ Sanook ไป override/ชนกับ ChatGPT login
|
|
52
|
+
// (codex bug #2733/#3286: ตั้ง OPENAI_API_KEY ค้าง env ทำให้ ChatGPT-plan auth วน loop sign-in)
|
|
53
|
+
const env = { ...process.env };
|
|
54
|
+
delete env.OPENAI_API_KEY;
|
|
55
|
+
const p = spawn('codex', args, { env, cwd: opts.cwd, shell: process.platform === 'win32' }); // Windows: codex = JS shim ผ่าน .cmd → ต้อง shell
|
|
39
56
|
let finalText = '';
|
|
40
57
|
let threadId;
|
|
41
58
|
let buf = '';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|
|
45
105
|
p.stdout.on('data', (chunk) => {
|
|
46
106
|
buf += chunk.toString();
|
|
47
107
|
const lines = buf.split('\n');
|
|
48
108
|
buf = lines.pop() ?? '';
|
|
49
109
|
for (const line of lines) {
|
|
50
|
-
|
|
51
|
-
if (!t)
|
|
52
|
-
continue;
|
|
53
|
-
if (!t.startsWith('{')) {
|
|
54
|
-
// plain stdout fallback (JSONL ถูก ignore) — เก็บเป็น final text
|
|
55
|
-
finalText += (finalText ? '\n' : '') + t;
|
|
56
|
-
opts.onEvent?.({ type: 'text', text: finalText });
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
const ev = JSON.parse(t);
|
|
61
|
-
if (ev.type === 'thread.started' && ev.thread_id) {
|
|
62
|
-
threadId = ev.thread_id;
|
|
63
|
-
opts.onEvent?.({ type: 'thread', threadId });
|
|
64
|
-
}
|
|
65
|
-
else if (ev.type === 'item.completed' && ev.item?.type === 'agent_message') {
|
|
66
|
-
finalText = ev.item.text ?? finalText;
|
|
67
|
-
opts.onEvent?.({ type: 'text', text: finalText });
|
|
68
|
-
}
|
|
69
|
-
else if (ev.type === 'turn.completed') {
|
|
70
|
-
opts.onEvent?.({ type: 'usage', usage: ev.usage });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// malformed JSON line — ข้าม
|
|
75
|
-
}
|
|
110
|
+
handleStdoutLine(line);
|
|
76
111
|
}
|
|
77
112
|
});
|
|
78
|
-
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
|
+
});
|
|
79
122
|
p.on('close', (code) => {
|
|
80
|
-
|
|
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)
|
|
81
129
|
resolve({ text: finalText.trim(), threadId });
|
|
82
130
|
else
|
|
83
|
-
reject(new Error(`codex exec จบด้วย exit code ${code}`));
|
|
131
|
+
reject(new Error(`codex exec จบด้วย exit code ${code}${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
|
|
84
132
|
});
|
|
85
133
|
});
|
|
86
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
|
@@ -43,13 +43,29 @@ export async function listRemoteModels(cfg, key, timeoutMs = 6000) {
|
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* merge: curated alias (registry — มี label สื่อความหมาย) นำหน้า + remote id ที่เหลือต่อท้าย
|
|
46
|
-
* dedup ด้วย model id
|
|
46
|
+
* dedup ด้วย model id — alias หลายตัวที่ชี้ id เดียวกัน (เช่น haiku/fast → claude-haiku-4-5,
|
|
47
|
+
* smart/gpt → gpt-5.5) ต้องรวมเป็น "haiku / fast — id" บรรทัดเดียว ไม่งั้น value ซ้ำ → React key ชน
|
|
48
|
+
* → ตัวเลือกโผล่ซ้ำ/หาย (bug "มีตัวเลือกสองตัวเลือกเป็น model เดียวกัน"). ใช้ทั้ง setup wizard และ /model picker
|
|
47
49
|
*/
|
|
48
50
|
export function mergeModelOptions(cfg, remote = []) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
51
|
+
// group alias ทั้งหมดตาม id (รวม 'default' ด้วย — กัน id ที่มีแต่ alias 'default' เช่น lmstudio:local-model,
|
|
52
|
+
// ollama:llama3.3 หายไปจนเลือกไม่ได้/Select ว่าง). ตอนทำ label ค่อยซ่อนคำ "default" ถ้ามีชื่ออื่นอยู่แล้ว
|
|
53
|
+
const aliasesById = new Map();
|
|
54
|
+
const order = []; // คง first-seen order ของ id
|
|
55
|
+
for (const [alias, id] of Object.entries(cfg.models)) {
|
|
56
|
+
if (!aliasesById.has(id)) {
|
|
57
|
+
aliasesById.set(id, []);
|
|
58
|
+
order.push(id);
|
|
59
|
+
}
|
|
60
|
+
aliasesById.get(id)?.push(alias);
|
|
61
|
+
}
|
|
62
|
+
const curated = order.map((id) => {
|
|
63
|
+
const aliases = aliasesById.get(id) ?? [];
|
|
64
|
+
const named = aliases.filter((a) => a !== 'default');
|
|
65
|
+
const shown = named.length ? named : aliases; // มีแต่ 'default' → โชว์ 'default' (ดีกว่าซ่อน id หายไป)
|
|
66
|
+
return { id, label: `${shown.join(' / ')} — ${id}` };
|
|
67
|
+
});
|
|
68
|
+
const seen = new Set(order);
|
|
69
|
+
const extra = [...new Set(remote)].filter((id) => id && !seen.has(id)).map((id) => ({ id, label: id }));
|
|
54
70
|
return [...curated, ...extra].map((o) => ({ label: o.label, value: o.id }));
|
|
55
71
|
}
|
|
@@ -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,26 +181,51 @@ 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];
|
|
238
199
|
}
|
|
239
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* provider นี้มี key ใน env ที่ "ใช้ได้จริง" ไหม — มี key + ผ่าน policy (ไม่ใช่ OAuth/subscription token
|
|
202
|
+
* หรือ format ผิด). ใช้ทั้ง first-run smart-skip และ -m flag เพื่อไม่ให้ข้าม wizard ทั้งที่ key ใช้ไม่ได้
|
|
203
|
+
* (เช่น export ANTHROPIC_API_KEY=sk-ant-oat… → ถูกแบน → ต้องเข้า wizard ไม่ใช่ขึ้น "พร้อมใช้")
|
|
204
|
+
*/
|
|
205
|
+
export function hasUsableEnvKey(provider) {
|
|
206
|
+
const cfg = PROVIDERS[provider];
|
|
207
|
+
if (!cfg)
|
|
208
|
+
return false;
|
|
209
|
+
if (cfg.kind === 'delegate')
|
|
210
|
+
return false; // ต้องเช็ก readiness แยก (เช่น codex CLI installed + logged in)
|
|
211
|
+
if (!cfg.requiresKey)
|
|
212
|
+
return true; // local — ไม่ต้อง key
|
|
213
|
+
const k = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
214
|
+
if (!k)
|
|
215
|
+
return false;
|
|
216
|
+
try {
|
|
217
|
+
assertDirectApiKey(cfg, k); // reject OAuth prefix / format ผิด
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** หา provider ที่ "มี key ใช้ได้จริงใน env" (cloud, ตามลำดับนิยม) — ใช้ทำ first-run smart skip + แนะ headless */
|
|
240
225
|
export function detectEnvProvider() {
|
|
241
|
-
for (const id of ['anthropic', 'openai', 'google', '
|
|
226
|
+
for (const id of ['anthropic', 'openai', 'google', 'xai', 'mistral', 'groq']) {
|
|
242
227
|
const cfg = PROVIDERS[id];
|
|
243
|
-
if (cfg?.requiresKey &&
|
|
228
|
+
if (cfg?.requiresKey && hasUsableEnvKey(id)) {
|
|
244
229
|
return { provider: id, label: cfg.label, envVar: cfg.envVar, model: cfg.models.default };
|
|
245
230
|
}
|
|
246
231
|
}
|
|
@@ -270,10 +255,14 @@ export function resolveModel(spec) {
|
|
|
270
255
|
const found = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
271
256
|
if (!found) {
|
|
272
257
|
const url = consoleUrl(provider);
|
|
258
|
+
const codexHint = provider === 'openai'
|
|
259
|
+
? `\n • ถ้าต้องการใช้ ChatGPT plan ไม่ใช้ API key: เลือก \`/model codex\` แล้วรัน \`codex login\``
|
|
260
|
+
: '';
|
|
273
261
|
throw new Error(`ยังไม่มี API key ของ ${cfg.label} (${cfg.envVar})\n` +
|
|
274
262
|
(url ? ` • เอา key ที่: ${url}\n` : '') +
|
|
275
263
|
` • ตั้ง: export ${cfg.envVar}="..." ` +
|
|
276
|
-
`หรือรัน \`${BRAND.cliName}\` (ไม่ใส่ task) เพื่อ setup wizard`
|
|
264
|
+
`หรือรัน \`${BRAND.cliName}\` (ไม่ใส่ task) เพื่อ setup wizard` +
|
|
265
|
+
codexHint);
|
|
277
266
|
}
|
|
278
267
|
assertDirectApiKey(cfg, found); // reject OAuth/subscription token + format ผิด
|
|
279
268
|
key = found;
|
|
@@ -281,7 +270,7 @@ export function resolveModel(spec) {
|
|
|
281
270
|
else {
|
|
282
271
|
key = resolveKeyFromEnv(cfg.envVar) ?? cfg.localPlaceholderKey ?? 'local';
|
|
283
272
|
}
|
|
284
|
-
// <PROVIDER>_BASE_URL env → override (สลับ region
|
|
273
|
+
// <PROVIDER>_BASE_URL env → override (สลับ region); ไม่งั้น local อ่าน env, cloud ใช้ default
|
|
285
274
|
const baseURL = process.env[`${cfg.id.toUpperCase()}_BASE_URL`] ??
|
|
286
275
|
(cfg.requiresKey ? cfg.baseURL : process.env[cfg.envVar] ?? cfg.baseURL);
|
|
287
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;
|