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/commands.js
CHANGED
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { PROVIDERS, parseSpec } from './providers/registry.js';
|
|
3
|
+
import { canonicalSpec, consoleUrl, hasUsableEnvKey, PROVIDERS, parseSpec } from './providers/registry.js';
|
|
4
4
|
import { appHomePath, BRAND } from './brand.js';
|
|
5
5
|
import { parseFrontmatter } from './skills.js';
|
|
6
6
|
import { projectConfigPathIfTrusted } from './trust.js';
|
|
7
|
+
import { normalizePersonalityName, personalityListText } from './personality.js';
|
|
8
|
+
import { parseInsightsArgs } from './insights-args.js';
|
|
7
9
|
const HELP_TEXT = `คำสั่ง:
|
|
8
10
|
/help แสดงคำสั่งทั้งหมด
|
|
11
|
+
/new, /reset เริ่มบทสนทนาใหม่
|
|
12
|
+
/status ดูสถานะ session ปัจจุบัน
|
|
9
13
|
/model [spec] ดู/เปลี่ยน model (เช่น /model opus, /model openai:gpt-5)
|
|
14
|
+
/personality [name]
|
|
15
|
+
ดู/ตั้ง personality overlay
|
|
16
|
+
/platforms ดู providers + messaging platforms ที่รองรับ
|
|
10
17
|
/tools ดู tools ที่ agent ใช้ได้
|
|
11
18
|
/skills ดูจำนวน skills (จัดการ: ${BRAND.cliName} skill list)
|
|
12
19
|
/diff ดู git diff (สิ่งที่ agent แก้ในรอบนี้)
|
|
20
|
+
/retry รัน prompt ล่าสุดอีกครั้ง
|
|
21
|
+
/stop หยุด turn ที่กำลังรัน
|
|
13
22
|
/undo stash การแก้ไฟล์ล่าสุด (กู้คืนด้วย git stash pop)
|
|
14
23
|
/rewind ย้อนกลับ 1 turn (คืนไฟล์ git + ตัดบทสนทนา, recoverable)
|
|
15
|
-
/cost
|
|
24
|
+
/cost, /usage ดู token + cost รอบล่าสุด
|
|
25
|
+
/insights [--days N] [--all]
|
|
26
|
+
ดู usage/session insights ในเครื่อง
|
|
16
27
|
↑/↓ ประวัติ · @ไฟล์ แนบ context/รูป · \\ ลงท้าย = บรรทัดใหม่
|
|
17
28
|
/clear ล้าง conversation (เริ่มใหม่)
|
|
18
|
-
/compact
|
|
29
|
+
/compact, /compress
|
|
30
|
+
บีบ context (truncate · หรือ summarize ถ้าตั้ง compaction)
|
|
19
31
|
/quit ออก
|
|
20
32
|
|
|
21
33
|
นอก REPL (พิมพ์ใน shell):
|
|
22
|
-
${BRAND.cliName} search "<q>" · index · brain init · serve · mcp serve · config set <k> <v>
|
|
34
|
+
${BRAND.cliName} search "<q>" · index · brain init · brain context · brain eval · brain review · brain final · serve · mcp serve · config set <k> <v>
|
|
23
35
|
ดูทั้งหมด: ${BRAND.cliName} --help
|
|
24
36
|
|
|
25
37
|
custom commands:
|
|
26
|
-
~/.sanook/commands/<name>.md และ .sanook/commands/<name>.md (project ต้อง trust ก่อน)
|
|
38
|
+
~/.sanook/commands/<name>.md และ .sanook/commands/<name>.md (project ต้อง trust ก่อน)
|
|
39
|
+
args: ใช้ $ARGUMENTS หรือ {{ args }}; ถ้าไม่มี placeholder จะ append args ต่อท้าย`;
|
|
27
40
|
const TOOLS_LIST = [
|
|
28
41
|
'read_file (offset/limit) write_file edit_file (replace_all) list_dir glob grep run_bash',
|
|
29
42
|
'git_status git_diff git_log git_commit',
|
|
@@ -32,6 +45,24 @@ const TOOLS_LIST = [
|
|
|
32
45
|
'task task_parallel task_spawn task_collect task_cancel task_status ← sub-agent (ขนาน/background)',
|
|
33
46
|
'diagnostics ← type error/lint จาก language server (LSP)',
|
|
34
47
|
].join('\n ');
|
|
48
|
+
const MESSAGING_PLATFORMS = [
|
|
49
|
+
'telegram',
|
|
50
|
+
'discord',
|
|
51
|
+
'slack',
|
|
52
|
+
'mattermost',
|
|
53
|
+
'homeassistant',
|
|
54
|
+
'email',
|
|
55
|
+
'line',
|
|
56
|
+
'sms',
|
|
57
|
+
'ntfy',
|
|
58
|
+
'signal',
|
|
59
|
+
'whatsapp',
|
|
60
|
+
'matrix',
|
|
61
|
+
'googlechat',
|
|
62
|
+
'bluebubbles',
|
|
63
|
+
'teams',
|
|
64
|
+
'webhooks',
|
|
65
|
+
];
|
|
35
66
|
export function parseSlashInvocation(input) {
|
|
36
67
|
const trimmed = input.trim();
|
|
37
68
|
if (!trimmed.startsWith('/'))
|
|
@@ -39,7 +70,10 @@ export function parseSlashInvocation(input) {
|
|
|
39
70
|
const match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
|
40
71
|
if (!match)
|
|
41
72
|
return null;
|
|
42
|
-
|
|
73
|
+
const name = match[1].toLowerCase();
|
|
74
|
+
if (name !== '?' && !isValidCommandName(name))
|
|
75
|
+
return null;
|
|
76
|
+
return { name, args: match[2] ?? '' };
|
|
43
77
|
}
|
|
44
78
|
/** /model (ไม่มี arg) — โชว์ model ปัจจุบัน + ตัวเลือกของ provider นั้น (alias จาก registry) */
|
|
45
79
|
function modelMenu(current) {
|
|
@@ -60,19 +94,81 @@ function modelMenu(current) {
|
|
|
60
94
|
.filter(Boolean)
|
|
61
95
|
.join('\n');
|
|
62
96
|
}
|
|
97
|
+
function missingKeyHint(provider) {
|
|
98
|
+
const cfg = PROVIDERS[provider];
|
|
99
|
+
if (!cfg?.requiresKey || hasUsableEnvKey(provider))
|
|
100
|
+
return undefined;
|
|
101
|
+
const url = consoleUrl(provider);
|
|
102
|
+
const lines = [
|
|
103
|
+
`⚠ ยังไม่มี API key ของ ${cfg.label} (${cfg.envVar}) — model นี้จะยังรันไม่ได้จนกว่าจะตั้ง key`,
|
|
104
|
+
url ? ` • เอา key ที่: ${url}` : undefined,
|
|
105
|
+
` • ตั้ง: export ${cfg.envVar}="..." หรือรัน ${BRAND.cliName} เพื่อเข้า setup wizard`,
|
|
106
|
+
].filter(Boolean);
|
|
107
|
+
if (provider === 'openai') {
|
|
108
|
+
lines.push(' • ถ้าต้องการใช้ ChatGPT plan ไม่ใช้ API key: /model codex แล้วรัน codex login');
|
|
109
|
+
}
|
|
110
|
+
return lines.join('\n');
|
|
111
|
+
}
|
|
112
|
+
function platformMenu() {
|
|
113
|
+
return [
|
|
114
|
+
`providers: ${Object.keys(PROVIDERS).join(' · ')}`,
|
|
115
|
+
`messaging: ${MESSAGING_PLATFORMS.join(' · ')}`,
|
|
116
|
+
`setup: ${BRAND.cliName} setup หรือ ${BRAND.cliName} gateway setup <platform>`,
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
119
|
+
function statusMenu(ctx) {
|
|
120
|
+
const { provider } = parseSpec(ctx.model);
|
|
121
|
+
const cfg = PROVIDERS[provider];
|
|
122
|
+
return [
|
|
123
|
+
`session: REPL`,
|
|
124
|
+
`model: ${ctx.model}`,
|
|
125
|
+
`provider: ${cfg?.label ?? provider}`,
|
|
126
|
+
`usage: ${ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)'}`,
|
|
127
|
+
`platforms: พิมพ์ /platforms`,
|
|
128
|
+
`system status: ${BRAND.cliName} status`,
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
131
|
+
function modelChange(spec) {
|
|
132
|
+
const canonical = canonicalSpec(spec);
|
|
133
|
+
const { provider, model } = parseSpec(canonical);
|
|
134
|
+
if (!PROVIDERS[provider]) {
|
|
135
|
+
return {
|
|
136
|
+
handled: true,
|
|
137
|
+
message: `provider ไม่รองรับ: "${provider}" — รองรับ: ${Object.keys(PROVIDERS).join(' · ')}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!model) {
|
|
141
|
+
return {
|
|
142
|
+
handled: true,
|
|
143
|
+
message: `model spec ไม่ครบ: "${spec}" — ใช้ /model <alias> หรือ /model <provider:model>`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const hint = missingKeyHint(provider);
|
|
147
|
+
return {
|
|
148
|
+
handled: true,
|
|
149
|
+
modelChange: canonical,
|
|
150
|
+
message: [`เปลี่ยน model → ${canonical}`, hint].filter(Boolean).join('\n'),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
63
153
|
/** parse input — ถ้าขึ้นต้น / = slash command, ไม่งั้น handled=false (ส่งเข้า agent) */
|
|
64
154
|
export function parseCommand(input, ctx) {
|
|
65
155
|
const trimmed = input.trim();
|
|
66
156
|
if (!trimmed.startsWith('/'))
|
|
67
157
|
return { handled: false };
|
|
68
|
-
const [
|
|
158
|
+
const [rawCmd, ...args] = trimmed.slice(1).split(/\s+/);
|
|
159
|
+
const cmd = rawCmd.toLowerCase();
|
|
69
160
|
switch (cmd) {
|
|
70
161
|
case 'help':
|
|
71
162
|
case '?':
|
|
72
163
|
return { handled: true, action: 'help', message: HELP_TEXT };
|
|
73
164
|
case 'clear':
|
|
165
|
+
case 'new':
|
|
166
|
+
case 'reset':
|
|
74
167
|
return { handled: true, action: 'clear', message: 'ล้าง conversation แล้ว' };
|
|
168
|
+
case 'status':
|
|
169
|
+
return { handled: true, message: statusMenu(ctx) };
|
|
75
170
|
case 'compact':
|
|
171
|
+
case 'compress':
|
|
76
172
|
return { handled: true, action: 'compact', message: 'บีบ context แล้ว' };
|
|
77
173
|
case 'quit':
|
|
78
174
|
case 'exit':
|
|
@@ -80,17 +176,46 @@ export function parseCommand(input, ctx) {
|
|
|
80
176
|
case 'model':
|
|
81
177
|
if (!args[0])
|
|
82
178
|
return { handled: true, message: modelMenu(ctx.model) };
|
|
83
|
-
return
|
|
179
|
+
return modelChange(args[0]);
|
|
180
|
+
case 'personality': {
|
|
181
|
+
const raw = args.join(' ').trim();
|
|
182
|
+
if (!raw)
|
|
183
|
+
return { handled: true, message: personalityListText() };
|
|
184
|
+
const name = normalizePersonalityName(raw);
|
|
185
|
+
if (!name)
|
|
186
|
+
return { handled: true, message: `ไม่รู้จัก personality: ${raw}\n\n${personalityListText()}` };
|
|
187
|
+
return {
|
|
188
|
+
handled: true,
|
|
189
|
+
action: 'personality',
|
|
190
|
+
personalityChange: name === 'none' ? '' : name,
|
|
191
|
+
message: name === 'none' ? 'ปิด personality overlay แล้ว' : `ตั้ง personality → ${name}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
84
194
|
case 'tools':
|
|
85
195
|
return { handled: true, message: `tools ที่ agent ใช้ได้ (+ MCP ที่ตั้งไว้):\n ${TOOLS_LIST}` };
|
|
196
|
+
case 'platforms':
|
|
197
|
+
return { handled: true, message: platformMenu() };
|
|
86
198
|
case 'skills':
|
|
87
199
|
return { handled: true, message: `skills โหลดจาก built-in + ${appHomePath('skills')} — จัดการด้วย "${BRAND.cliName} skill list/add/remove"` };
|
|
88
200
|
case 'diff':
|
|
89
201
|
return { handled: true, action: 'diff' };
|
|
202
|
+
case 'retry':
|
|
203
|
+
return { handled: true, action: 'retry' };
|
|
204
|
+
case 'stop':
|
|
205
|
+
return { handled: true, action: 'stop', message: 'ไม่มี turn ที่กำลังทำงาน' };
|
|
90
206
|
case 'undo':
|
|
91
207
|
return { handled: true, action: 'undo' };
|
|
208
|
+
case 'rewind':
|
|
209
|
+
return { handled: true, action: 'rewind' };
|
|
92
210
|
case 'cost':
|
|
211
|
+
case 'usage':
|
|
93
212
|
return { handled: true, message: ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)' };
|
|
213
|
+
case 'insights': {
|
|
214
|
+
const parsed = parseInsightsArgs(args);
|
|
215
|
+
if (parsed === null)
|
|
216
|
+
return { handled: true, message: 'ใช้: /insights [--days N] [--all] (N ต้องเป็นจำนวนวันบวก)' };
|
|
217
|
+
return { handled: true, action: 'insights', insightsDays: parsed.days, insightsAll: parsed.all };
|
|
218
|
+
}
|
|
94
219
|
default:
|
|
95
220
|
return { handled: true, message: `ไม่รู้จักคำสั่ง /${cmd} — พิมพ์ /help` };
|
|
96
221
|
}
|
|
@@ -99,11 +224,44 @@ export function parseCommand(input, ctx) {
|
|
|
99
224
|
// ไฟล์ markdown (frontmatter optional) = prompt template ที่ส่งเข้า agent. $ARGUMENTS = ส่วนหลังชื่อคำสั่ง
|
|
100
225
|
// (เลียน Claude Code .claude/commands) — global ~/.sanook/commands + project .sanook/commands (project ทับ)
|
|
101
226
|
export const BUILTIN_COMMANDS = new Set([
|
|
102
|
-
'help',
|
|
227
|
+
'help',
|
|
228
|
+
'?',
|
|
229
|
+
'clear',
|
|
230
|
+
'new',
|
|
231
|
+
'reset',
|
|
232
|
+
'status',
|
|
233
|
+
'compact',
|
|
234
|
+
'compress',
|
|
235
|
+
'quit',
|
|
236
|
+
'exit',
|
|
237
|
+
'model',
|
|
238
|
+
'personality',
|
|
239
|
+
'platforms',
|
|
240
|
+
'tools',
|
|
241
|
+
'skills',
|
|
242
|
+
'diff',
|
|
243
|
+
'retry',
|
|
244
|
+
'stop',
|
|
245
|
+
'undo',
|
|
246
|
+
'rewind',
|
|
247
|
+
'cost',
|
|
248
|
+
'usage',
|
|
249
|
+
'insights',
|
|
103
250
|
]);
|
|
104
251
|
function isValidCommandName(name) {
|
|
105
252
|
return /^[a-z0-9][a-z0-9-]{0,40}$/.test(name);
|
|
106
253
|
}
|
|
254
|
+
function compareCommandFiles(a, b) {
|
|
255
|
+
const an = a.toLowerCase();
|
|
256
|
+
const bn = b.toLowerCase();
|
|
257
|
+
if (an !== bn)
|
|
258
|
+
return an.localeCompare(bn);
|
|
259
|
+
if (a === an && b !== bn)
|
|
260
|
+
return 1;
|
|
261
|
+
if (a !== an && b === bn)
|
|
262
|
+
return -1;
|
|
263
|
+
return a.localeCompare(b);
|
|
264
|
+
}
|
|
107
265
|
/** scan custom commands จาก global + project (project override). ข้าม built-in ชื่อซ้ำ */
|
|
108
266
|
export async function loadCustomCommands(cwd = process.cwd()) {
|
|
109
267
|
const out = new Map();
|
|
@@ -119,10 +277,11 @@ export async function loadCustomCommands(cwd = process.cwd()) {
|
|
|
119
277
|
catch {
|
|
120
278
|
continue; // ไม่มีโฟลเดอร์ = ข้าม
|
|
121
279
|
}
|
|
122
|
-
for (const f of files) {
|
|
123
|
-
|
|
280
|
+
for (const f of files.sort(compareCommandFiles)) {
|
|
281
|
+
const normalizedFile = f.toLowerCase();
|
|
282
|
+
if (!normalizedFile.endsWith('.md'))
|
|
124
283
|
continue;
|
|
125
|
-
const name =
|
|
284
|
+
const name = normalizedFile.slice(0, -3);
|
|
126
285
|
if (!isValidCommandName(name) || BUILTIN_COMMANDS.has(name))
|
|
127
286
|
continue;
|
|
128
287
|
try {
|
|
@@ -140,7 +299,7 @@ export async function loadCustomCommands(cwd = process.cwd()) {
|
|
|
140
299
|
export function expandCustomCommand(cmd, args) {
|
|
141
300
|
const a = args.trim();
|
|
142
301
|
if (/\$ARGUMENTS|\{\{\s*args\s*\}\}/.test(cmd.body)) {
|
|
143
|
-
return cmd.body.replace(/\$ARGUMENTS|\{\{\s*args\s*\}\}/g, a);
|
|
302
|
+
return cmd.body.replace(/\$ARGUMENTS|\{\{\s*args\s*\}\}/g, () => a);
|
|
144
303
|
}
|
|
145
304
|
return a ? `${cmd.body}\n\n${a}` : cmd.body;
|
|
146
305
|
}
|
package/dist/compaction.js
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
|
+
import { selectiveCompressText } from './context-compression.js';
|
|
1
2
|
const TRUNC_HEAD = 400;
|
|
2
3
|
const TRUNC_TAIL = 600;
|
|
3
4
|
const CHARS_PER_TOKEN = 4; // ประมาณคร่าวๆ (จริง ~3.5-4 ต่อ token)
|
|
5
|
+
const SELECTIVE_TOOL_TARGET_CHARS = 6_000;
|
|
6
|
+
const SELECTIVE_TOOL_MIN_CHARS = 8_000;
|
|
7
|
+
function textFromMessageContent(content) {
|
|
8
|
+
if (typeof content === 'string')
|
|
9
|
+
return content;
|
|
10
|
+
if (!Array.isArray(content))
|
|
11
|
+
return '';
|
|
12
|
+
return content
|
|
13
|
+
.map((part) => {
|
|
14
|
+
if (typeof part === 'object' && part && 'type' in part && part.type === 'text' && 'text' in part && typeof part.text === 'string')
|
|
15
|
+
return part.text;
|
|
16
|
+
return '';
|
|
17
|
+
})
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join('\n');
|
|
20
|
+
}
|
|
21
|
+
function latestUserText(messages) {
|
|
22
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
23
|
+
if (messages[i].role !== 'user')
|
|
24
|
+
continue;
|
|
25
|
+
const text = textFromMessageContent(messages[i].content).trim();
|
|
26
|
+
if (text)
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
function adaptiveStaleTarget(baseTarget, rank, count) {
|
|
32
|
+
if (count <= 1)
|
|
33
|
+
return baseTarget;
|
|
34
|
+
const recency = rank / Math.max(1, count - 1); // 0 = oldest, 1 = newest stale
|
|
35
|
+
return Math.max(1_500, Math.floor(baseTarget * (0.35 + 0.65 * recency)));
|
|
36
|
+
}
|
|
37
|
+
function adaptiveMinChars(targetChars, baseMinChars) {
|
|
38
|
+
return Math.min(baseMinChars, Math.max(targetChars + 1_000, Math.floor(targetChars * 1.45)));
|
|
39
|
+
}
|
|
4
40
|
/** ตัดข้อความยาว เก็บหัว (intent) + ท้าย (error/result) */
|
|
5
41
|
export function truncateText(s) {
|
|
6
42
|
if (s.length <= TRUNC_HEAD + TRUNC_TAIL + 40)
|
|
@@ -16,23 +52,72 @@ export function truncateText(s) {
|
|
|
16
52
|
*/
|
|
17
53
|
export function pruneToolResults(messages, keepTail = 4) {
|
|
18
54
|
const cut = Math.max(0, messages.length - keepTail);
|
|
19
|
-
|
|
55
|
+
let changed = false;
|
|
56
|
+
const out = messages.map((m, i) => {
|
|
20
57
|
if (i >= cut)
|
|
21
58
|
return m;
|
|
22
59
|
if (m.role !== 'tool' || !Array.isArray(m.content))
|
|
23
60
|
return m;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
const content = m.content.map((part) => {
|
|
62
|
+
if (part.type === 'tool-result' &&
|
|
63
|
+
part.output?.type === 'text' &&
|
|
64
|
+
typeof part.output.value === 'string') {
|
|
65
|
+
const compressed = selectiveCompressText(part.output.value, {
|
|
66
|
+
targetChars: SELECTIVE_TOOL_TARGET_CHARS,
|
|
67
|
+
minChars: SELECTIVE_TOOL_MIN_CHARS,
|
|
68
|
+
});
|
|
69
|
+
if (compressed.changed) {
|
|
70
|
+
changed = true;
|
|
71
|
+
return { ...part, output: { ...part.output, value: compressed.text } };
|
|
72
|
+
}
|
|
73
|
+
const truncated = truncateText(part.output.value);
|
|
74
|
+
if (truncated !== part.output.value) {
|
|
75
|
+
changed = true;
|
|
76
|
+
return { ...part, output: { ...part.output, value: truncated } };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return part;
|
|
80
|
+
});
|
|
81
|
+
return content === m.content ? m : { ...m, content };
|
|
82
|
+
});
|
|
83
|
+
return changed ? out : messages;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Per-step token optimizer (zero LLM cost).
|
|
87
|
+
* Compresses stale, very large tool results before each model request while keeping the latest tail full.
|
|
88
|
+
*/
|
|
89
|
+
export function selectivelyCompressStaleToolResults(messages, keepTail = 6, targetChars = SELECTIVE_TOOL_TARGET_CHARS, minChars = SELECTIVE_TOOL_MIN_CHARS, query = latestUserText(messages)) {
|
|
90
|
+
const cut = Math.max(0, messages.length - keepTail);
|
|
91
|
+
const staleToolIndexes = messages
|
|
92
|
+
.map((m, i) => ({ m, i }))
|
|
93
|
+
.filter(({ m, i }) => i < cut && m.role === 'tool' && Array.isArray(m.content))
|
|
94
|
+
.map(({ i }) => i);
|
|
95
|
+
const rankByIndex = new Map(staleToolIndexes.map((index, rank) => [index, rank]));
|
|
96
|
+
let changed = false;
|
|
97
|
+
const out = messages.map((m, i) => {
|
|
98
|
+
if (i >= cut)
|
|
99
|
+
return m;
|
|
100
|
+
if (m.role !== 'tool' || !Array.isArray(m.content))
|
|
101
|
+
return m;
|
|
102
|
+
let messageChanged = false;
|
|
103
|
+
const adaptiveTarget = adaptiveStaleTarget(targetChars, rankByIndex.get(i) ?? 0, staleToolIndexes.length);
|
|
104
|
+
const adaptiveMin = adaptiveMinChars(adaptiveTarget, minChars);
|
|
105
|
+
const content = m.content.map((part) => {
|
|
106
|
+
if (part.type === 'tool-result' &&
|
|
107
|
+
part.output?.type === 'text' &&
|
|
108
|
+
typeof part.output.value === 'string') {
|
|
109
|
+
const compressed = selectiveCompressText(part.output.value, { targetChars: adaptiveTarget, minChars: adaptiveMin, query });
|
|
110
|
+
if (compressed.changed) {
|
|
111
|
+
changed = true;
|
|
112
|
+
messageChanged = true;
|
|
113
|
+
return { ...part, output: { ...part.output, value: compressed.text } };
|
|
31
114
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
115
|
+
}
|
|
116
|
+
return part;
|
|
117
|
+
});
|
|
118
|
+
return messageChanged ? { ...m, content } : m;
|
|
35
119
|
});
|
|
120
|
+
return changed ? out : messages;
|
|
36
121
|
}
|
|
37
122
|
/** ประมาณ token ของ conversation (chars/4) — ไม่เป๊ะแต่พอใช้ตัดสิน compact */
|
|
38
123
|
export function estimateTokens(messages) {
|
package/dist/config.js
CHANGED
|
@@ -7,7 +7,10 @@ import { registerPricing } from './cost.js';
|
|
|
7
7
|
export const CONFIG_DIR = appHomePath();
|
|
8
8
|
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
9
9
|
const AUTH_PATH = join(CONFIG_DIR, 'auth.json'); // API keys (chmod 0600)
|
|
10
|
-
|
|
10
|
+
const AUTH_ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
|
+
const RESERVED_AUTH_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
12
|
+
const PricingKeySchema = z.string().regex(/^[^:\s]+:\S+$/, 'key ต้องเป็น provider:model');
|
|
13
|
+
export const PricingOverrideSchema = z.record(PricingKeySchema, z
|
|
11
14
|
.object({
|
|
12
15
|
input: z.number().finite().nonnegative().optional(),
|
|
13
16
|
output: z.number().finite().nonnegative().optional(),
|
|
@@ -33,39 +36,68 @@ export const ConfigSchema = z.object({
|
|
|
33
36
|
cacheTtl: z.enum(['5m', '1h']).catch('5m').default('5m'),
|
|
34
37
|
// วิธีบีบ context ตอนยาว: 'truncate' (default, zero-LLM) · 'summarize' (ใช้ model ถูกย่อ — จำ context ได้ดีกว่า)
|
|
35
38
|
compaction: z.enum(['truncate', 'summarize']).catch('truncate').default('truncate'),
|
|
39
|
+
// token reducer: off, local zero-LLM selective compressor, or optional Headroom proxy adapter.
|
|
40
|
+
contextCompression: z.enum(['off', 'selective', 'headroom']).catch('selective').default('selective'),
|
|
36
41
|
// extended thinking (Anthropic): false/ไม่ตั้ง = ปิด · true = budget default · number = budget tokens
|
|
37
42
|
thinking: z.union([z.boolean(), z.number().int().positive()]).optional().catch(undefined),
|
|
38
43
|
// model สำหรับย่อ (compaction=summarize) — ไม่ตั้ง = ใช้ fast-sibling ของ model หลัก (ค่ายเดียวกัน ถูกกว่า)
|
|
39
44
|
summaryModel: z.string().optional().catch(undefined),
|
|
45
|
+
// model สำหรับ semantic search embeddings (เช่น openai:text-embedding-3-small)
|
|
46
|
+
embeddingModel: z.string().optional().catch(undefined),
|
|
47
|
+
// Hermes-style /personality overlay (stored as a small named prompt)
|
|
48
|
+
personality: z.string().optional().catch(undefined),
|
|
40
49
|
});
|
|
41
50
|
const DEFAULT_THINKING_BUDGET = 4096;
|
|
51
|
+
function normalizeThinkingBudget(value) {
|
|
52
|
+
const budget = Math.floor(value);
|
|
53
|
+
return Number.isSafeInteger(budget) && budget > 0 ? budget : undefined;
|
|
54
|
+
}
|
|
42
55
|
/** parse thinking config (config field หรือ env) → budget tokens (undefined = ปิด) */
|
|
43
56
|
function parseThinking(v) {
|
|
44
|
-
if (typeof v === 'number' && v
|
|
45
|
-
return
|
|
57
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
58
|
+
return normalizeThinkingBudget(v);
|
|
46
59
|
if (v === true)
|
|
47
60
|
return DEFAULT_THINKING_BUDGET;
|
|
48
61
|
if (typeof v === 'string') {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
const clean = v.trim();
|
|
63
|
+
if (/^\d+$/.test(clean))
|
|
64
|
+
return normalizeThinkingBudget(Number(clean));
|
|
65
|
+
if (['on', 'true', '1', 'yes'].includes(clean.toLowerCase()))
|
|
52
66
|
return DEFAULT_THINKING_BUDGET;
|
|
53
67
|
}
|
|
54
68
|
return undefined;
|
|
55
69
|
}
|
|
70
|
+
function trimmedString(v) {
|
|
71
|
+
if (typeof v !== 'string')
|
|
72
|
+
return undefined;
|
|
73
|
+
const clean = v.trim();
|
|
74
|
+
return clean ? clean : undefined;
|
|
75
|
+
}
|
|
76
|
+
function parseCacheTtl(v) {
|
|
77
|
+
const clean = trimmedString(v);
|
|
78
|
+
return clean === '5m' || clean === '1h' ? clean : undefined;
|
|
79
|
+
}
|
|
80
|
+
function parseCompaction(v) {
|
|
81
|
+
const clean = trimmedString(v);
|
|
82
|
+
return clean === 'truncate' || clean === 'summarize' ? clean : undefined;
|
|
83
|
+
}
|
|
84
|
+
function parseContextCompression(v) {
|
|
85
|
+
const clean = trimmedString(v);
|
|
86
|
+
return clean === 'off' || clean === 'selective' || clean === 'headroom' ? clean : undefined;
|
|
87
|
+
}
|
|
56
88
|
/**
|
|
57
89
|
* อ่าน tuning knobs (cache TTL / thinking / compaction / summary model) จาก global config.json
|
|
58
|
-
* + env override (SANOOK_CACHE_TTL / SANOOK_THINKING / SANOOK_COMPACTION / SANOOK_SUMMARY_MODEL).
|
|
90
|
+
* + env override (SANOOK_CACHE_TTL / SANOOK_THINKING / SANOOK_COMPACTION / SANOOK_CONTEXT_COMPRESSION / SANOOK_SUMMARY_MODEL).
|
|
59
91
|
* อ่านตรงจาก config.json (เลี่ยง thread ผ่าน call stack ลึก) — เบา, เรียกครั้งเดียวต่อ turn.
|
|
60
92
|
*/
|
|
61
93
|
export async function agentTuning() {
|
|
62
94
|
const raw = await readGlobalConfigRaw();
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const summaryModel = process.env.SANOOK_SUMMARY_MODEL ?? (
|
|
68
|
-
return { cacheTtl, thinkingBudget, compaction, summaryModel };
|
|
95
|
+
const cacheTtl = parseCacheTtl(process.env.SANOOK_CACHE_TTL) ?? parseCacheTtl(raw.cacheTtl) ?? '5m';
|
|
96
|
+
const thinkingBudget = parseThinking(trimmedString(process.env.SANOOK_THINKING) ?? raw.thinking);
|
|
97
|
+
const compaction = parseCompaction(process.env.SANOOK_COMPACTION) ?? parseCompaction(raw.compaction) ?? 'truncate';
|
|
98
|
+
const contextCompression = parseContextCompression(process.env.SANOOK_CONTEXT_COMPRESSION) ?? parseContextCompression(raw.contextCompression) ?? 'selective';
|
|
99
|
+
const summaryModel = trimmedString(process.env.SANOOK_SUMMARY_MODEL) ?? trimmedString(raw.summaryModel);
|
|
100
|
+
return { cacheTtl, thinkingBudget, compaction, contextCompression, summaryModel };
|
|
69
101
|
}
|
|
70
102
|
async function readJson(path) {
|
|
71
103
|
try {
|
|
@@ -76,9 +108,18 @@ async function readJson(path) {
|
|
|
76
108
|
return {}; // ไม่มีไฟล์ / parse ไม่ได้ = ใช้ default
|
|
77
109
|
}
|
|
78
110
|
}
|
|
111
|
+
// key ที่ untrusted project ตั้งไม่ได้ (ต้อง `sanook trust` ก่อน):
|
|
112
|
+
// - permissionMode: auto = auto-approve mutation (รัน bash/แก้ไฟล์ไม่ถาม) — อันตรายสุด
|
|
113
|
+
// - budgetUsd: repo อันตรายตั้งสูงๆ = ปิด spend cap ของ user (เปลืองเงินจริง)
|
|
114
|
+
// - pricing: ตั้งราคาปลอม = ทำให้ budget cap ไม่ trigger (ซ่อน cost / bypass cap)
|
|
115
|
+
// (model/maxSteps/embeddingModel ฯลฯ ปล่อยได้ — เป็น preference ที่ user เห็น/override ได้ และตอนนี้ถูกคุมด้วย budget จริงของ user)
|
|
116
|
+
const UNTRUSTED_PROJECT_DENY = new Set(['permissionMode', 'budgetUsd', 'pricing']);
|
|
79
117
|
function sanitizeUntrustedProjectConfig(cfg) {
|
|
80
|
-
const out = {
|
|
81
|
-
|
|
118
|
+
const out = {};
|
|
119
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
120
|
+
if (!UNTRUSTED_PROJECT_DENY.has(k))
|
|
121
|
+
out[k] = v;
|
|
122
|
+
}
|
|
82
123
|
return out;
|
|
83
124
|
}
|
|
84
125
|
/**
|
|
@@ -113,16 +154,29 @@ function parseEnvPricing() {
|
|
|
113
154
|
if (!raw)
|
|
114
155
|
return undefined;
|
|
115
156
|
try {
|
|
116
|
-
|
|
117
|
-
const res = PricingOverrideSchema.safeParse(parsed);
|
|
118
|
-
return res.success ? res.data : undefined;
|
|
157
|
+
return parsePricingOverride(raw);
|
|
119
158
|
}
|
|
120
159
|
catch {
|
|
121
160
|
return undefined; // JSON ไม่ถูก = ข้าม (ไม่ทำให้ boot ล้ม)
|
|
122
161
|
}
|
|
123
162
|
}
|
|
124
163
|
export function parsePricingOverride(raw) {
|
|
125
|
-
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(raw);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new Error('pricing JSON parse ไม่สำเร็จ');
|
|
170
|
+
}
|
|
171
|
+
const res = PricingOverrideSchema.safeParse(parsed);
|
|
172
|
+
if (!res.success) {
|
|
173
|
+
const details = res.error.issues
|
|
174
|
+
.slice(0, 3)
|
|
175
|
+
.map((issue) => `${issue.path.length ? issue.path.join('.') : 'pricing'}: ${issue.message}`)
|
|
176
|
+
.join('; ');
|
|
177
|
+
throw new Error(`pricing schema ไม่ถูกต้อง${details ? ` — ${details}` : ''}`);
|
|
178
|
+
}
|
|
179
|
+
return res.data;
|
|
126
180
|
}
|
|
127
181
|
/** ครั้งแรกที่รัน (ยังไม่มี global config) → ต้องทำ setup wizard */
|
|
128
182
|
export async function isFirstRun() {
|
|
@@ -152,6 +206,23 @@ export async function saveBrainPath(path) {
|
|
|
152
206
|
export async function readGlobalConfigRaw() {
|
|
153
207
|
return readJson(CONFIG_PATH);
|
|
154
208
|
}
|
|
209
|
+
/** path ของ auth.json (ใช้โชว์ใน CLI เท่านั้น; ห้าม print raw secret) */
|
|
210
|
+
export function authConfigPath() {
|
|
211
|
+
return AUTH_PATH;
|
|
212
|
+
}
|
|
213
|
+
function isSafeAuthEnvVarName(name) {
|
|
214
|
+
return AUTH_ENV_VAR_RE.test(name) && !RESERVED_AUTH_KEYS.has(name);
|
|
215
|
+
}
|
|
216
|
+
/** อ่าน auth.json ดิบแบบกรองเฉพาะ string values — caller ต้อง redact ก่อนโชว์ */
|
|
217
|
+
export async function readStoredAuthRaw() {
|
|
218
|
+
const raw = await readJson(AUTH_PATH);
|
|
219
|
+
const auth = {};
|
|
220
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
221
|
+
if (isSafeAuthEnvVarName(k) && typeof v === 'string')
|
|
222
|
+
auth[k] = v;
|
|
223
|
+
}
|
|
224
|
+
return auth;
|
|
225
|
+
}
|
|
155
226
|
/** merge patch ลง config.json (สำหรับ `sanook config set`) */
|
|
156
227
|
export async function patchGlobalConfig(patch) {
|
|
157
228
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
@@ -161,25 +232,44 @@ export async function patchGlobalConfig(patch) {
|
|
|
161
232
|
}
|
|
162
233
|
/** บันทึก API key ลง ~/.sanook/auth.json (chmod 0600) + set env ทันทีสำหรับ session นี้ */
|
|
163
234
|
export async function saveKey(envVar, key) {
|
|
235
|
+
if (!isSafeAuthEnvVarName(envVar))
|
|
236
|
+
throw new Error(`env var ไม่ถูกต้อง: ${envVar}`);
|
|
164
237
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
auth = JSON.parse(await readFile(AUTH_PATH, 'utf8'));
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
/* ยังไม่มีไฟล์ */
|
|
171
|
-
}
|
|
238
|
+
const auth = await readStoredAuthRaw();
|
|
172
239
|
auth[envVar] = key;
|
|
173
240
|
await writeFile(AUTH_PATH, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
174
241
|
await chmod(AUTH_PATH, 0o600); // เจ้าของอ่าน/เขียนเท่านั้น
|
|
175
242
|
process.env[envVar] = key;
|
|
176
243
|
}
|
|
244
|
+
/** ลบ key ที่ Sanook เก็บไว้ใน auth.json (ไม่แตะ env จริงของ shell ภายนอก) */
|
|
245
|
+
export async function removeStoredKey(envVar) {
|
|
246
|
+
if (!isSafeAuthEnvVarName(envVar))
|
|
247
|
+
return false;
|
|
248
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
249
|
+
const auth = await readStoredAuthRaw();
|
|
250
|
+
if (!Object.prototype.hasOwnProperty.call(auth, envVar))
|
|
251
|
+
return false;
|
|
252
|
+
delete auth[envVar];
|
|
253
|
+
await writeFile(AUTH_PATH, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
254
|
+
await chmod(AUTH_PATH, 0o600).catch(() => { });
|
|
255
|
+
delete process.env[envVar];
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
/** ล้าง auth.json ที่ Sanook เก็บไว้ทั้งหมด */
|
|
259
|
+
export async function clearStoredAuth() {
|
|
260
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
261
|
+
const auth = await readStoredAuthRaw();
|
|
262
|
+
for (const envVar of Object.keys(auth))
|
|
263
|
+
delete process.env[envVar];
|
|
264
|
+
await writeFile(AUTH_PATH, '{}\n', { mode: 0o600 });
|
|
265
|
+
await chmod(AUTH_PATH, 0o600).catch(() => { });
|
|
266
|
+
}
|
|
177
267
|
/** โหลด key จาก auth.json เข้า env ตอน boot (ไม่ override env ที่ตั้งไว้แล้ว) */
|
|
178
268
|
export async function loadKeysIntoEnv() {
|
|
179
269
|
try {
|
|
180
|
-
const auth =
|
|
270
|
+
const auth = await readStoredAuthRaw();
|
|
181
271
|
for (const [k, v] of Object.entries(auth)) {
|
|
182
|
-
if (!process.env[k]
|
|
272
|
+
if (!process.env[k])
|
|
183
273
|
process.env[k] = v;
|
|
184
274
|
}
|
|
185
275
|
}
|