sanook-cli 0.5.2 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -2
- package/README.md +15 -3
- package/README.th.md +8 -1
- package/dist/approval.js +7 -0
- package/dist/bin.js +637 -56
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +42 -3
- package/dist/brain-final.js +15 -9
- package/dist/brain-link.js +73 -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.js +3 -0
- package/dist/brand.js +4 -0
- package/dist/cli-args.js +47 -9
- package/dist/cli-option-values.js +1 -1
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +98 -15
- package/dist/config.js +66 -34
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +20 -0
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/gateway/auth.js +14 -3
- package/dist/gateway/deliver.js +45 -3
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +30 -1
- package/dist/gateway/ledger.js +20 -1
- package/dist/gateway/session.js +34 -11
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +24 -4
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +65 -9
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +153 -9
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp.js +77 -5
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +51 -7
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +7 -5
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +20 -8
- package/dist/providers/keys.js +21 -0
- package/dist/providers/models.js +1 -1
- package/dist/providers/registry.js +11 -1
- package/dist/search/cli.js +9 -1
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +10 -10
- package/dist/session-brain.js +103 -0
- package/dist/session-distill.js +84 -0
- package/dist/session.js +1 -11
- package/dist/skill-install.js +24 -1
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +31 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/permission.js +82 -16
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/search.js +9 -2
- package/dist/tools/task.js +22 -2
- package/dist/tools/timeout.js +7 -5
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +874 -35
- package/dist/ui/banner.js +78 -4
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +30 -2
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +163 -50
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +32 -6
- package/dist/usage-cli.js +160 -0
- package/dist/usage-ledger.js +169 -0
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/package.json +4 -3
- package/scripts/postinstall.mjs +4 -4
- package/second-brain/Projects/_Index.md +17 -4
- package/second-brain/Projects/sanook-cli/_Index.md +7 -3
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +52 -11
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +2 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -23
- package/second-brain/Shared/Tech-Standards/_Index.md +2 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const OSC52_MAX_CHARS = 100_000;
|
|
3
|
+
function powershellSetClipboardScript(text) {
|
|
4
|
+
const b64 = Buffer.from(text, 'utf8').toString('base64');
|
|
5
|
+
return `Set-Clipboard -Value ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64}')))`;
|
|
6
|
+
}
|
|
7
|
+
function clipboardWriteCommands(platform, env) {
|
|
8
|
+
if (platform === 'darwin')
|
|
9
|
+
return [{ args: [], command: 'pbcopy', stdin: true }];
|
|
10
|
+
if (platform === 'win32')
|
|
11
|
+
return [{ args: ['-NoProfile', '-NonInteractive'], command: 'powershell', stdin: false }];
|
|
12
|
+
const commands = [];
|
|
13
|
+
if (env.WSL_INTEROP || env.WSL_DISTRO_NAME) {
|
|
14
|
+
commands.push({ args: ['-NoProfile', '-NonInteractive'], command: 'powershell.exe', stdin: false });
|
|
15
|
+
}
|
|
16
|
+
if (env.WAYLAND_DISPLAY)
|
|
17
|
+
commands.push({ args: ['--type', 'text/plain'], command: 'wl-copy', stdin: true });
|
|
18
|
+
commands.push({ args: ['-selection', 'clipboard', '-in'], command: 'xclip', stdin: true });
|
|
19
|
+
commands.push({ args: ['--clipboard', '--input'], command: 'xsel', stdin: true });
|
|
20
|
+
return commands;
|
|
21
|
+
}
|
|
22
|
+
function runClipboardCommand(command, text, start) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const args = command.stdin ? command.args : [...command.args, '-Command', powershellSetClipboardScript(text)];
|
|
25
|
+
let child;
|
|
26
|
+
try {
|
|
27
|
+
child = start(command.command, args, { stdio: command.stdin ? ['pipe', 'ignore', 'ignore'] : ['ignore', 'ignore', 'ignore'], windowsHide: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
resolve(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
child.once('error', () => resolve(false));
|
|
34
|
+
child.once('close', (code) => resolve(code === 0));
|
|
35
|
+
if (command.stdin)
|
|
36
|
+
child.stdin?.end(text);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function writeSystemClipboard(text, options = {}) {
|
|
40
|
+
const env = options.env ?? process.env;
|
|
41
|
+
const platform = options.platform ?? process.platform;
|
|
42
|
+
const start = options.spawn ?? spawn;
|
|
43
|
+
for (const command of clipboardWriteCommands(platform, env)) {
|
|
44
|
+
if (await runClipboardCommand(command, text, start))
|
|
45
|
+
return command.command;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
export function osc52Sequence(text) {
|
|
50
|
+
const safe = text.length > OSC52_MAX_CHARS ? text.slice(0, OSC52_MAX_CHARS) : text;
|
|
51
|
+
return `\u001b]52;c;${Buffer.from(safe, 'utf8').toString('base64')}\u0007`;
|
|
52
|
+
}
|
|
53
|
+
export async function copyTextToClipboard(text, options = {}) {
|
|
54
|
+
const payload = text.trimEnd();
|
|
55
|
+
if (!payload.trim())
|
|
56
|
+
throw new Error('ไม่มีข้อความให้ copy');
|
|
57
|
+
const backend = await writeSystemClipboard(payload, options);
|
|
58
|
+
if (backend)
|
|
59
|
+
return { detail: backend, method: 'system' };
|
|
60
|
+
if (options.writeOsc52) {
|
|
61
|
+
options.writeOsc52(osc52Sequence(payload));
|
|
62
|
+
return { detail: 'OSC52', method: 'osc52' };
|
|
63
|
+
}
|
|
64
|
+
throw new Error('ไม่พบ clipboard backend และไม่มี OSC52 output');
|
|
65
|
+
}
|
package/dist/commands.js
CHANGED
|
@@ -6,16 +6,25 @@ import { parseFrontmatter } from './skills.js';
|
|
|
6
6
|
import { projectConfigPathIfTrusted } from './trust.js';
|
|
7
7
|
import { normalizePersonalityName, personalityListText } from './personality.js';
|
|
8
8
|
import { parseInsightsArgs } from './insights-args.js';
|
|
9
|
-
|
|
9
|
+
import { formatHotkeys } from './hotkeys.js';
|
|
10
|
+
import { formatToolCatalog } from './tool-catalog.js';
|
|
11
|
+
export const HELP_TEXT = `คำสั่ง:
|
|
10
12
|
/help แสดงคำสั่งทั้งหมด
|
|
11
13
|
/new, /reset เริ่มบทสนทนาใหม่
|
|
12
14
|
/status ดูสถานะ session ปัจจุบัน
|
|
13
|
-
/model [spec] ดู/เปลี่ยน model
|
|
15
|
+
/model [spec] ดู/เปลี่ยน model — /model เปิด picker 2 ขั้น (provider → model)
|
|
16
|
+
/setup ดูขั้นตอน setup wizard (model · agent · tools · gateway · brain)
|
|
17
|
+
/dashboard เปิด Sanook Dashboard (local web UI)
|
|
14
18
|
/personality [name]
|
|
15
19
|
ดู/ตั้ง personality overlay
|
|
20
|
+
/details [thinking|tools] [hidden|collapsed|expanded]
|
|
21
|
+
คุมแผง thinking/tool trail แบบ Hermes-style
|
|
16
22
|
/platforms ดู providers + messaging platforms ที่รองรับ
|
|
17
23
|
/tools ดู tools ที่ agent ใช้ได้
|
|
18
|
-
/
|
|
24
|
+
/mcp เปิด MCP Hub overlay
|
|
25
|
+
/skills เปิด Skills Hub overlay (จัดการ: ${BRAND.cliName} skill list)
|
|
26
|
+
/sessions เปิด Session Switcher overlay · /trail พับ/ขยาย tool trail
|
|
27
|
+
/tasks ดู background sub-agents (task_spawn)
|
|
19
28
|
/diff ดู git diff (สิ่งที่ agent แก้ในรอบนี้)
|
|
20
29
|
/retry รัน prompt ล่าสุดอีกครั้ง
|
|
21
30
|
/stop หยุด turn ที่กำลังรัน
|
|
@@ -24,6 +33,8 @@ const HELP_TEXT = `คำสั่ง:
|
|
|
24
33
|
/cost, /usage ดู token + cost รอบล่าสุด
|
|
25
34
|
/insights [--days N] [--all]
|
|
26
35
|
ดู usage/session insights ในเครื่อง
|
|
36
|
+
/hotkeys เปิด overlay คีย์ลัดใน REPL
|
|
37
|
+
/copy [last] copy คำตอบ assistant ล่าสุดไป clipboard/OSC52
|
|
27
38
|
↑/↓ ประวัติ · @ไฟล์ แนบ context/รูป · \\ ลงท้าย = บรรทัดใหม่
|
|
28
39
|
/clear ล้าง conversation (เริ่มใหม่)
|
|
29
40
|
/compact, /compress
|
|
@@ -37,14 +48,6 @@ const HELP_TEXT = `คำสั่ง:
|
|
|
37
48
|
custom commands:
|
|
38
49
|
~/.sanook/commands/<name>.md และ .sanook/commands/<name>.md (project ต้อง trust ก่อน)
|
|
39
50
|
args: ใช้ $ARGUMENTS หรือ {{ args }}; ถ้าไม่มี placeholder จะ append args ต่อท้าย`;
|
|
40
|
-
const TOOLS_LIST = [
|
|
41
|
-
'read_file (offset/limit) write_file edit_file (replace_all) list_dir glob grep run_bash',
|
|
42
|
-
'git_status git_diff git_log git_commit',
|
|
43
|
-
'remember recall · skill find_skills create_skill',
|
|
44
|
-
'schedule_task list_scheduled cancel_scheduled',
|
|
45
|
-
'task task_parallel task_spawn task_collect task_cancel task_status ← sub-agent (ขนาน/background)',
|
|
46
|
-
'diagnostics ← type error/lint จาก language server (LSP)',
|
|
47
|
-
].join('\n ');
|
|
48
51
|
const MESSAGING_PLATFORMS = [
|
|
49
52
|
'telegram',
|
|
50
53
|
'discord',
|
|
@@ -167,16 +170,42 @@ export function parseCommand(input, ctx) {
|
|
|
167
170
|
return { handled: true, action: 'clear', message: 'ล้าง conversation แล้ว' };
|
|
168
171
|
case 'status':
|
|
169
172
|
return { handled: true, message: statusMenu(ctx) };
|
|
173
|
+
case 'hotkeys':
|
|
174
|
+
return { handled: true, action: 'hotkeys', message: formatHotkeys() };
|
|
170
175
|
case 'compact':
|
|
171
176
|
case 'compress':
|
|
172
177
|
return { handled: true, action: 'compact', message: 'บีบ context แล้ว' };
|
|
178
|
+
case 'copy': {
|
|
179
|
+
const target = args[0]?.toLowerCase();
|
|
180
|
+
if (!target || target === 'last' || target === 'assistant')
|
|
181
|
+
return { handled: true, action: 'copyLast' };
|
|
182
|
+
return { handled: true, message: 'ใช้ /copy หรือ /copy last' };
|
|
183
|
+
}
|
|
173
184
|
case 'quit':
|
|
174
185
|
case 'exit':
|
|
175
186
|
return { handled: true, action: 'quit' };
|
|
176
187
|
case 'model':
|
|
177
188
|
if (!args[0])
|
|
178
|
-
return { handled: true, message: modelMenu(ctx.model) };
|
|
189
|
+
return { handled: true, action: 'modelPicker', message: modelMenu(ctx.model) };
|
|
179
190
|
return modelChange(args[0]);
|
|
191
|
+
case 'setup':
|
|
192
|
+
return {
|
|
193
|
+
handled: true,
|
|
194
|
+
message: [
|
|
195
|
+
`${BRAND.productName} setup (Hermes-style sections):`,
|
|
196
|
+
` 1. ${BRAND.cliName} setup model — provider + model wizard`,
|
|
197
|
+
` 2. ${BRAND.cliName} setup agent — permissionMode, budget, personality`,
|
|
198
|
+
` 3. ${BRAND.cliName} setup tools — built-in tools + MCP`,
|
|
199
|
+
` 4. ${BRAND.cliName} setup gateway — Telegram/Discord/Slack/…`,
|
|
200
|
+
` 5. ${BRAND.cliName} setup brain — second-brain vault`,
|
|
201
|
+
` หรือรัน ${BRAND.cliName} ครั้งแรก → wizard 10 ขั้น (ภาษา → … → gateway → brain)`,
|
|
202
|
+
].join('\n'),
|
|
203
|
+
};
|
|
204
|
+
case 'dashboard':
|
|
205
|
+
return {
|
|
206
|
+
handled: true,
|
|
207
|
+
message: `Sanook Dashboard — รัน: ${BRAND.cliName} dashboard\n แล้วเปิด http://127.0.0.1:9119 (Chat · Files · Logs · Cron · Channels)`,
|
|
208
|
+
};
|
|
180
209
|
case 'personality': {
|
|
181
210
|
const raw = args.join(' ').trim();
|
|
182
211
|
if (!raw)
|
|
@@ -192,11 +221,55 @@ export function parseCommand(input, ctx) {
|
|
|
192
221
|
};
|
|
193
222
|
}
|
|
194
223
|
case 'tools':
|
|
195
|
-
return { handled: true, message: `tools ที่ agent ใช้ได้ (+ MCP ที่ตั้งไว้):\n ${
|
|
224
|
+
return { handled: true, action: 'toolsHub', message: `tools ที่ agent ใช้ได้ (+ MCP ที่ตั้งไว้):\n ${formatToolCatalog()}` };
|
|
225
|
+
case 'trail': {
|
|
226
|
+
const rawMode = args[0]?.toLowerCase();
|
|
227
|
+
if (!rawMode)
|
|
228
|
+
return { handled: true, action: 'toolTrail', message: 'toggle tool trail view' };
|
|
229
|
+
if (['compact', 'collapse', 'collapsed', 'hide', 'summary'].includes(rawMode)) {
|
|
230
|
+
return { handled: true, action: 'toolTrail', message: 'tool trail → compact', toolTrailMode: 'compact' };
|
|
231
|
+
}
|
|
232
|
+
if (['expanded', 'expand', 'full', 'show'].includes(rawMode)) {
|
|
233
|
+
return { handled: true, action: 'toolTrail', message: 'tool trail → expanded', toolTrailMode: 'expanded' };
|
|
234
|
+
}
|
|
235
|
+
return { handled: true, message: 'ใช้ /trail, /trail compact, หรือ /trail expanded' };
|
|
236
|
+
}
|
|
237
|
+
case 'details': {
|
|
238
|
+
const section = args[0]?.toLowerCase();
|
|
239
|
+
const mode = args[1]?.toLowerCase();
|
|
240
|
+
const usage = 'ใช้ /details thinking|tools hidden|collapsed|expanded';
|
|
241
|
+
if (!section && !mode)
|
|
242
|
+
return { handled: true, message: usage };
|
|
243
|
+
if (section !== 'thinking' && section !== 'tools')
|
|
244
|
+
return { handled: true, message: usage };
|
|
245
|
+
if (mode !== 'hidden' && mode !== 'collapsed' && mode !== 'expanded')
|
|
246
|
+
return { handled: true, message: usage };
|
|
247
|
+
return {
|
|
248
|
+
handled: true,
|
|
249
|
+
action: 'details',
|
|
250
|
+
detailMode: mode,
|
|
251
|
+
detailSection: section,
|
|
252
|
+
message: `details ${section} → ${mode}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
196
255
|
case 'platforms':
|
|
197
256
|
return { handled: true, message: platformMenu() };
|
|
257
|
+
case 'mcp':
|
|
258
|
+
return {
|
|
259
|
+
handled: true,
|
|
260
|
+
action: 'mcpHub',
|
|
261
|
+
message: `MCP servers — จัดการด้วย "${BRAND.cliName} mcp list/search/install/doctor"`,
|
|
262
|
+
};
|
|
198
263
|
case 'skills':
|
|
199
|
-
return {
|
|
264
|
+
return {
|
|
265
|
+
handled: true,
|
|
266
|
+
action: 'skillsHub',
|
|
267
|
+
message: `skills โหลดจาก built-in + ${appHomePath('skills')} — จัดการด้วย "${BRAND.cliName} skill list/add/remove"`,
|
|
268
|
+
};
|
|
269
|
+
case 'sessions':
|
|
270
|
+
return { handled: true, action: 'sessionsHub', message: `saved sessions — จัดการด้วย "${BRAND.cliName} sessions"` };
|
|
271
|
+
case 'tasks':
|
|
272
|
+
return { handled: true, action: 'tasksHub', message: 'background tasks — จาก task_spawn (Enter ดูรายละเอียด)' };
|
|
200
273
|
case 'diff':
|
|
201
274
|
return { handled: true, action: 'diff' };
|
|
202
275
|
case 'retry':
|
|
@@ -209,7 +282,10 @@ export function parseCommand(input, ctx) {
|
|
|
209
282
|
return { handled: true, action: 'rewind' };
|
|
210
283
|
case 'cost':
|
|
211
284
|
case 'usage':
|
|
212
|
-
return {
|
|
285
|
+
return {
|
|
286
|
+
handled: true,
|
|
287
|
+
message: `${ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)'}\n→ ${BRAND.cliName} usage daily`,
|
|
288
|
+
};
|
|
213
289
|
case 'insights': {
|
|
214
290
|
const parsed = parseInsightsArgs(args);
|
|
215
291
|
if (parsed === null)
|
|
@@ -230,15 +306,22 @@ export const BUILTIN_COMMANDS = new Set([
|
|
|
230
306
|
'new',
|
|
231
307
|
'reset',
|
|
232
308
|
'status',
|
|
309
|
+
'hotkeys',
|
|
233
310
|
'compact',
|
|
234
311
|
'compress',
|
|
312
|
+
'copy',
|
|
235
313
|
'quit',
|
|
236
314
|
'exit',
|
|
237
315
|
'model',
|
|
238
316
|
'personality',
|
|
317
|
+
'details',
|
|
239
318
|
'platforms',
|
|
319
|
+
'trail',
|
|
240
320
|
'tools',
|
|
321
|
+
'mcp',
|
|
241
322
|
'skills',
|
|
323
|
+
'sessions',
|
|
324
|
+
'tasks',
|
|
242
325
|
'diff',
|
|
243
326
|
'retry',
|
|
244
327
|
'stop',
|
package/dist/config.js
CHANGED
|
@@ -4,9 +4,12 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { appHomePath, appProjectPath, BRAND } from './brand.js';
|
|
5
5
|
import { projectRoot, projectTrustStatus } from './trust.js';
|
|
6
6
|
import { registerPricing } from './cost.js';
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export function configHomeDir() {
|
|
8
|
+
return appHomePath();
|
|
9
|
+
}
|
|
10
|
+
function authPath() {
|
|
11
|
+
return join(configHomeDir(), 'auth.json');
|
|
12
|
+
}
|
|
10
13
|
const AUTH_ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
14
|
const RESERVED_AUTH_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
12
15
|
const PricingKeySchema = z.string().regex(/^[^:\s]+:\S+$/, 'key ต้องเป็น provider:model');
|
|
@@ -46,6 +49,8 @@ export const ConfigSchema = z.object({
|
|
|
46
49
|
embeddingModel: z.string().optional().catch(undefined),
|
|
47
50
|
// Hermes-style /personality overlay (stored as a small named prompt)
|
|
48
51
|
personality: z.string().optional().catch(undefined),
|
|
52
|
+
/** UI + setup wizard language */
|
|
53
|
+
locale: z.enum(['en', 'th']).catch('th').default('th'),
|
|
49
54
|
});
|
|
50
55
|
const DEFAULT_THINKING_BUDGET = 4096;
|
|
51
56
|
function normalizeThinkingBudget(value) {
|
|
@@ -99,10 +104,36 @@ export async function agentTuning() {
|
|
|
99
104
|
const summaryModel = trimmedString(process.env.SANOOK_SUMMARY_MODEL) ?? trimmedString(raw.summaryModel);
|
|
100
105
|
return { cacheTtl, thinkingBudget, compaction, contextCompression, summaryModel };
|
|
101
106
|
}
|
|
107
|
+
const warnedBadConfigKeys = new Set();
|
|
108
|
+
function globalConfigPath() {
|
|
109
|
+
return join(configHomeDir(), 'config.json');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Validate the merged config, but degrade gracefully: a malformed strict field (bad model/maxSteps/
|
|
113
|
+
* permissionMode/budgetUsd/pricing in a hand-edited config.json) is dropped to its default with a
|
|
114
|
+
* one-time stderr warning instead of throwing and crashing boot. Security-sensitive fields drop to
|
|
115
|
+
* the SAFE default (budgetUsd→no cap is still surfaced by the warning; pricing→none).
|
|
116
|
+
*/
|
|
117
|
+
function parseConfigGraceful(merged) {
|
|
118
|
+
const first = ConfigSchema.safeParse(merged);
|
|
119
|
+
if (first.success)
|
|
120
|
+
return first.data;
|
|
121
|
+
const badKeys = [...new Set(first.error.issues.map((i) => String(i.path[0])).filter(Boolean))];
|
|
122
|
+
const cleaned = { ...merged };
|
|
123
|
+
for (const k of badKeys)
|
|
124
|
+
delete cleaned[k];
|
|
125
|
+
const fresh = badKeys.filter((k) => !warnedBadConfigKeys.has(k));
|
|
126
|
+
if (fresh.length) {
|
|
127
|
+
fresh.forEach((k) => warnedBadConfigKeys.add(k));
|
|
128
|
+
process.stderr.write(`${BRAND.cliName}: ⚠ ละเลย config ที่ค่าผิด (ใช้ค่า default แทน): ${fresh.join(', ')}\n`);
|
|
129
|
+
}
|
|
130
|
+
const second = ConfigSchema.safeParse(cleaned);
|
|
131
|
+
return second.success ? second.data : ConfigSchema.parse({});
|
|
132
|
+
}
|
|
102
133
|
async function readJson(path) {
|
|
103
134
|
try {
|
|
104
135
|
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
105
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
136
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
106
137
|
}
|
|
107
138
|
catch {
|
|
108
139
|
return {}; // ไม่มีไฟล์ / parse ไม่ได้ = ใช้ default
|
|
@@ -128,21 +159,22 @@ function sanitizeUntrustedProjectConfig(cfg) {
|
|
|
128
159
|
* (config flat — shallow merge พอ; strip undefined ใน overrides กัน override ทับ default)
|
|
129
160
|
*/
|
|
130
161
|
export async function loadConfig(overrides = {}, cwd = process.cwd()) {
|
|
131
|
-
const global = await readJson(
|
|
162
|
+
const global = await readJson(globalConfigPath());
|
|
132
163
|
const root = await projectRoot(cwd);
|
|
133
164
|
const projectRaw = await readJson(appProjectPath(root, 'config.json'));
|
|
134
165
|
const trust = await projectTrustStatus(root);
|
|
135
166
|
const project = trust.trusted ? projectRaw : sanitizeUntrustedProjectConfig(projectRaw);
|
|
136
167
|
const envConfig = {};
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
const envModel = trimmedString(process.env[BRAND.modelEnvVar]);
|
|
169
|
+
if (envModel)
|
|
170
|
+
envConfig.model = envModel;
|
|
139
171
|
const cleanOverrides = {};
|
|
140
172
|
for (const [k, v] of Object.entries(overrides)) {
|
|
141
173
|
if (v !== undefined)
|
|
142
174
|
cleanOverrides[k] = v;
|
|
143
175
|
}
|
|
144
176
|
const merged = { ...global, ...project, ...envConfig, ...cleanOverrides };
|
|
145
|
-
const config =
|
|
177
|
+
const config = parseConfigGraceful(merged);
|
|
146
178
|
// pricing override: config.pricing + env SANOOK_PRICING (JSON) → ลงทะเบียนเข้า cost table
|
|
147
179
|
registerPricing(config.pricing);
|
|
148
180
|
registerPricing(parseEnvPricing());
|
|
@@ -181,41 +213,41 @@ export function parsePricingOverride(raw) {
|
|
|
181
213
|
/** ครั้งแรกที่รัน (ยังไม่มี global config) → ต้องทำ setup wizard */
|
|
182
214
|
export async function isFirstRun() {
|
|
183
215
|
try {
|
|
184
|
-
await readFile(
|
|
216
|
+
await readFile(globalConfigPath(), 'utf8');
|
|
185
217
|
return false;
|
|
186
218
|
}
|
|
187
219
|
catch {
|
|
188
220
|
return true;
|
|
189
221
|
}
|
|
190
222
|
}
|
|
191
|
-
/** บันทึก global config (model/provider ที่เลือกตอน setup) */
|
|
223
|
+
/** บันทึก global config (model/provider/locale ที่เลือกตอน setup) */
|
|
192
224
|
export async function saveGlobalConfig(cfg) {
|
|
193
|
-
await mkdir(
|
|
194
|
-
const existing = await readJson(
|
|
195
|
-
await writeFile(
|
|
196
|
-
await chmod(
|
|
225
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
226
|
+
const existing = await readJson(globalConfigPath());
|
|
227
|
+
await writeFile(globalConfigPath(), `${JSON.stringify({ ...existing, ...cfg }, null, 2)}\n`, { mode: 0o600 });
|
|
228
|
+
await chmod(globalConfigPath(), 0o600).catch(() => { });
|
|
197
229
|
}
|
|
198
230
|
/** บันทึก path ของ second-brain workspace ลง global config (merge — ไม่ทับ field อื่น) */
|
|
199
231
|
export async function saveBrainPath(path) {
|
|
200
|
-
await mkdir(
|
|
201
|
-
const existing = await readJson(
|
|
202
|
-
await writeFile(
|
|
203
|
-
await chmod(
|
|
232
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
233
|
+
const existing = await readJson(globalConfigPath());
|
|
234
|
+
await writeFile(globalConfigPath(), `${JSON.stringify({ ...existing, brainPath: path }, null, 2)}\n`, { mode: 0o600 });
|
|
235
|
+
await chmod(globalConfigPath(), 0o600).catch(() => { });
|
|
204
236
|
}
|
|
205
237
|
/** อ่าน config.json ดิบ (ไม่ apply default/schema) — สำหรับ `sanook config` */
|
|
206
238
|
export async function readGlobalConfigRaw() {
|
|
207
|
-
return readJson(
|
|
239
|
+
return readJson(globalConfigPath());
|
|
208
240
|
}
|
|
209
241
|
/** path ของ auth.json (ใช้โชว์ใน CLI เท่านั้น; ห้าม print raw secret) */
|
|
210
242
|
export function authConfigPath() {
|
|
211
|
-
return
|
|
243
|
+
return authPath();
|
|
212
244
|
}
|
|
213
245
|
function isSafeAuthEnvVarName(name) {
|
|
214
246
|
return AUTH_ENV_VAR_RE.test(name) && !RESERVED_AUTH_KEYS.has(name);
|
|
215
247
|
}
|
|
216
248
|
/** อ่าน auth.json ดิบแบบกรองเฉพาะ string values — caller ต้อง redact ก่อนโชว์ */
|
|
217
249
|
export async function readStoredAuthRaw() {
|
|
218
|
-
const raw = await readJson(
|
|
250
|
+
const raw = await readJson(authPath());
|
|
219
251
|
const auth = {};
|
|
220
252
|
for (const [k, v] of Object.entries(raw)) {
|
|
221
253
|
if (isSafeAuthEnvVarName(k) && typeof v === 'string')
|
|
@@ -225,44 +257,44 @@ export async function readStoredAuthRaw() {
|
|
|
225
257
|
}
|
|
226
258
|
/** merge patch ลง config.json (สำหรับ `sanook config set`) */
|
|
227
259
|
export async function patchGlobalConfig(patch) {
|
|
228
|
-
await mkdir(
|
|
229
|
-
const existing = await readJson(
|
|
230
|
-
await writeFile(
|
|
231
|
-
await chmod(
|
|
260
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
261
|
+
const existing = await readJson(globalConfigPath());
|
|
262
|
+
await writeFile(globalConfigPath(), `${JSON.stringify({ ...existing, ...patch }, null, 2)}\n`, { mode: 0o600 });
|
|
263
|
+
await chmod(globalConfigPath(), 0o600).catch(() => { });
|
|
232
264
|
}
|
|
233
265
|
/** บันทึก API key ลง ~/.sanook/auth.json (chmod 0600) + set env ทันทีสำหรับ session นี้ */
|
|
234
266
|
export async function saveKey(envVar, key) {
|
|
235
267
|
if (!isSafeAuthEnvVarName(envVar))
|
|
236
268
|
throw new Error(`env var ไม่ถูกต้อง: ${envVar}`);
|
|
237
|
-
await mkdir(
|
|
269
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
238
270
|
const auth = await readStoredAuthRaw();
|
|
239
271
|
auth[envVar] = key;
|
|
240
|
-
await writeFile(
|
|
241
|
-
await chmod(
|
|
272
|
+
await writeFile(authPath(), `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
273
|
+
await chmod(authPath(), 0o600); // เจ้าของอ่าน/เขียนเท่านั้น
|
|
242
274
|
process.env[envVar] = key;
|
|
243
275
|
}
|
|
244
276
|
/** ลบ key ที่ Sanook เก็บไว้ใน auth.json (ไม่แตะ env จริงของ shell ภายนอก) */
|
|
245
277
|
export async function removeStoredKey(envVar) {
|
|
246
278
|
if (!isSafeAuthEnvVarName(envVar))
|
|
247
279
|
return false;
|
|
248
|
-
await mkdir(
|
|
280
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
249
281
|
const auth = await readStoredAuthRaw();
|
|
250
282
|
if (!Object.prototype.hasOwnProperty.call(auth, envVar))
|
|
251
283
|
return false;
|
|
252
284
|
delete auth[envVar];
|
|
253
|
-
await writeFile(
|
|
254
|
-
await chmod(
|
|
285
|
+
await writeFile(authPath(), `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
286
|
+
await chmod(authPath(), 0o600).catch(() => { });
|
|
255
287
|
delete process.env[envVar];
|
|
256
288
|
return true;
|
|
257
289
|
}
|
|
258
290
|
/** ล้าง auth.json ที่ Sanook เก็บไว้ทั้งหมด */
|
|
259
291
|
export async function clearStoredAuth() {
|
|
260
|
-
await mkdir(
|
|
292
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
261
293
|
const auth = await readStoredAuthRaw();
|
|
262
294
|
for (const envVar of Object.keys(auth))
|
|
263
295
|
delete process.env[envVar];
|
|
264
|
-
await writeFile(
|
|
265
|
-
await chmod(
|
|
296
|
+
await writeFile(authPath(), '{}\n', { mode: 0o600 });
|
|
297
|
+
await chmod(authPath(), 0o600).catch(() => { });
|
|
266
298
|
}
|
|
267
299
|
/** โหลด key จาก auth.json เข้า env ตอน boot (ไม่ override env ที่ตั้งไว้แล้ว) */
|
|
268
300
|
export async function loadKeysIntoEnv() {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { termList } from './search/index-core.js';
|
|
4
|
+
const PACK_DIR = 'Shared/Context-Packs';
|
|
5
|
+
const MIN_SCORE = 0.35;
|
|
6
|
+
const DEFAULT_MAX_CHARS = 1200;
|
|
7
|
+
/** Known packs + retrieval signals (aligned with Shared/Context-Packs/_Index.md). */
|
|
8
|
+
const PACK_CATALOG = [
|
|
9
|
+
{
|
|
10
|
+
slug: 'second-brain-maintenance',
|
|
11
|
+
title: 'Second-Brain Maintenance',
|
|
12
|
+
description: 'vault structure, routing rules, memory policy, indexes, runbooks, agent adapters',
|
|
13
|
+
signalTerms: [
|
|
14
|
+
'vault',
|
|
15
|
+
'structure',
|
|
16
|
+
'routing',
|
|
17
|
+
'memory',
|
|
18
|
+
'policy',
|
|
19
|
+
'index',
|
|
20
|
+
'runbook',
|
|
21
|
+
'agent',
|
|
22
|
+
'adapter',
|
|
23
|
+
'framework',
|
|
24
|
+
'obsidian',
|
|
25
|
+
'maintenance',
|
|
26
|
+
'brain',
|
|
27
|
+
'scaffold',
|
|
28
|
+
'frontmatter',
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
slug: 'coding-release',
|
|
33
|
+
title: 'Coding & Release',
|
|
34
|
+
description: 'source code, tests, build/release, CLI commands, runtime scripts',
|
|
35
|
+
signalTerms: [
|
|
36
|
+
'code',
|
|
37
|
+
'coding',
|
|
38
|
+
'test',
|
|
39
|
+
'tests',
|
|
40
|
+
'build',
|
|
41
|
+
'release',
|
|
42
|
+
'cli',
|
|
43
|
+
'script',
|
|
44
|
+
'implement',
|
|
45
|
+
'fix',
|
|
46
|
+
'bug',
|
|
47
|
+
'typecheck',
|
|
48
|
+
'npm',
|
|
49
|
+
'ship',
|
|
50
|
+
'deploy',
|
|
51
|
+
'refactor',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
slug: 'research-to-framework',
|
|
56
|
+
title: 'Research To Framework',
|
|
57
|
+
description: 'research, experiment, comparison, promote findings into framework',
|
|
58
|
+
signalTerms: [
|
|
59
|
+
'research',
|
|
60
|
+
'experiment',
|
|
61
|
+
'framework',
|
|
62
|
+
'benchmark',
|
|
63
|
+
'eval',
|
|
64
|
+
'hypothesis',
|
|
65
|
+
'promote',
|
|
66
|
+
'distillation',
|
|
67
|
+
'comparison',
|
|
68
|
+
'method',
|
|
69
|
+
'sota',
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
function catalogEntry(slug) {
|
|
74
|
+
const base = PACK_CATALOG.find((item) => item.slug === slug);
|
|
75
|
+
if (!base)
|
|
76
|
+
throw new Error(`unknown context pack slug: ${slug}`);
|
|
77
|
+
return { ...base, relPath: `${PACK_DIR}/${slug}.md` };
|
|
78
|
+
}
|
|
79
|
+
function packTerms(pack) {
|
|
80
|
+
return new Set([...termList(pack.slug), ...termList(pack.title), ...pack.signalTerms.map((t) => t.toLowerCase())]);
|
|
81
|
+
}
|
|
82
|
+
/** Score query against a pack via token overlap (deterministic, no network). */
|
|
83
|
+
export function scoreContextPack(query, pack) {
|
|
84
|
+
const queryTerms = termList(query);
|
|
85
|
+
if (!queryTerms.length)
|
|
86
|
+
return { score: 0, matchedTerms: [] };
|
|
87
|
+
const signals = packTerms(pack);
|
|
88
|
+
const matchedTerms = queryTerms.filter((term) => signals.has(term));
|
|
89
|
+
if (!matchedTerms.length)
|
|
90
|
+
return { score: 0, matchedTerms: [] };
|
|
91
|
+
const recall = matchedTerms.length / queryTerms.length;
|
|
92
|
+
const precision = matchedTerms.length / signals.size;
|
|
93
|
+
return { score: recall * 0.7 + precision * 0.3, matchedTerms };
|
|
94
|
+
}
|
|
95
|
+
export async function listContextPacks(brainPath) {
|
|
96
|
+
const dir = join(brainPath, PACK_DIR);
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const slugs = new Set(entries.filter((e) => e.isFile() && e.name.endsWith('.md') && e.name !== '_Index.md').map((e) => e.name.replace(/\.md$/i, '')));
|
|
105
|
+
return PACK_CATALOG.filter((item) => slugs.has(item.slug)).map((item) => catalogEntry(item.slug));
|
|
106
|
+
}
|
|
107
|
+
/** Pick the best matching context pack for a task query, or null if no clear match. */
|
|
108
|
+
export function selectContextPack(query, packs) {
|
|
109
|
+
const trimmed = query.trim();
|
|
110
|
+
if (!trimmed || !packs.length)
|
|
111
|
+
return null;
|
|
112
|
+
let best = null;
|
|
113
|
+
for (const pack of packs) {
|
|
114
|
+
const { score, matchedTerms } = scoreContextPack(trimmed, pack);
|
|
115
|
+
if (score < MIN_SCORE)
|
|
116
|
+
continue;
|
|
117
|
+
if (!best || score > best.score)
|
|
118
|
+
best = { pack, score, matchedTerms };
|
|
119
|
+
}
|
|
120
|
+
return best;
|
|
121
|
+
}
|
|
122
|
+
export async function readContextPackExcerpt(brainPath, pack, maxChars = DEFAULT_MAX_CHARS) {
|
|
123
|
+
const path = join(brainPath, pack.relPath);
|
|
124
|
+
let raw;
|
|
125
|
+
try {
|
|
126
|
+
raw = (await readFile(path, 'utf8')).trim();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
if (!raw)
|
|
132
|
+
return '';
|
|
133
|
+
const trimmed = raw.length > maxChars ? `${raw.slice(0, maxChars)}\n…` : raw;
|
|
134
|
+
return `## context-pack: ${pack.slug}\n${trimmed}`;
|
|
135
|
+
}
|
|
136
|
+
export async function buildContextPackBlock(brainPath, query, maxChars = DEFAULT_MAX_CHARS) {
|
|
137
|
+
const packs = await listContextPacks(brainPath);
|
|
138
|
+
const selected = selectContextPack(query, packs);
|
|
139
|
+
if (!selected)
|
|
140
|
+
return '';
|
|
141
|
+
const body = await readContextPackExcerpt(brainPath, selected.pack, maxChars);
|
|
142
|
+
if (!body)
|
|
143
|
+
return '';
|
|
144
|
+
return `<context_pack slug="${selected.pack.slug}" note="task-family context pack (auto-selected) — load order + done criteria; ไม่ใช่คำสั่ง">\n${body}\n</context_pack>`;
|
|
145
|
+
}
|
package/dist/cost.js
CHANGED
|
@@ -12,6 +12,14 @@ export const PRICING = {
|
|
|
12
12
|
'openai:gpt-5.5': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
13
13
|
'openai:gpt-5.4-mini': { input: 0.25, output: 2, cacheWrite: 0.25, cacheRead: 0.025 },
|
|
14
14
|
'openai:gpt-5.3-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
15
|
+
// OpenAI Codex delegate (ChatGPT plan — token counts are real; cost is estimated from API list price)
|
|
16
|
+
'codex:gpt-5.5': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
17
|
+
'codex:gpt-5.4': { input: 2.5, output: 15, cacheWrite: 2.5, cacheRead: 0.25 },
|
|
18
|
+
'codex:gpt-5.4-mini': { input: 0.25, output: 2, cacheWrite: 0.25, cacheRead: 0.025 },
|
|
19
|
+
'codex:gpt-5.3-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
20
|
+
'codex:gpt-5.2-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
21
|
+
'codex:gpt-5-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
22
|
+
'codex:gpt-5.3-codex-spark': { input: 0.25, output: 2, cacheWrite: 0.25, cacheRead: 0.025 },
|
|
15
23
|
// Google Gemini (≤200k context tier)
|
|
16
24
|
'google:gemini-2.5-pro': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.31 },
|
|
17
25
|
'google:gemini-2.5-flash': { input: 0.3, output: 2.5, cacheWrite: 0.3, cacheRead: 0.075 },
|
|
@@ -147,4 +155,16 @@ export class CostMeter {
|
|
|
147
155
|
const budget = this.budgetUsd != null ? ` / budget $${this.budgetUsd}` : '';
|
|
148
156
|
return `tokens: ${total} (in ${this.inTok} · out ${this.outTok} · cache-read ${this.cacheReadTok} · cache-write ${this.cacheWriteTok}) · cost ${cost}${budget}`;
|
|
149
157
|
}
|
|
158
|
+
snapshot() {
|
|
159
|
+
return {
|
|
160
|
+
specKey: this.specKey,
|
|
161
|
+
inputTokens: this.inTok,
|
|
162
|
+
outputTokens: this.outTok,
|
|
163
|
+
cacheReadTokens: this.cacheReadTok,
|
|
164
|
+
cacheWriteTokens: this.cacheWriteTok,
|
|
165
|
+
totalTokens: this.inTok + this.outTok + this.cacheReadTok + this.cacheWriteTok,
|
|
166
|
+
costUsd: this.spent,
|
|
167
|
+
hasPricing: this.hasPricing,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
150
170
|
}
|