sanook-cli 0.5.5 → 0.5.8
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 +55 -0
- package/README.md +392 -42
- package/README.th.md +15 -8
- package/dist/auto-maintain.js +113 -0
- package/dist/bin.js +77 -15
- package/dist/brain-final.js +8 -4
- package/dist/brain-link.js +73 -0
- package/dist/brain-new.js +9 -5
- package/dist/brain-repair.js +7 -4
- package/dist/brand.js +21 -0
- package/dist/commands.js +7 -1
- package/dist/config.js +40 -29
- package/dist/cost.js +20 -0
- package/dist/dashboard/api-helpers.js +112 -3
- package/dist/dashboard/server.js +85 -1
- package/dist/dashboard/static/app.js +381 -0
- package/dist/dashboard/static/styles.css +36 -0
- package/dist/dashboard/terminal.js +214 -0
- package/dist/diff.js +22 -8
- package/dist/gateway/session.js +4 -0
- package/dist/install-info.js +91 -0
- package/dist/loop.js +31 -4
- package/dist/memory.js +236 -16
- package/dist/persona.js +300 -0
- package/dist/project-scaffold.js +4 -2
- package/dist/providers/registry.js +11 -1
- package/dist/self-improve-synth.js +86 -0
- package/dist/self-improve.js +203 -0
- package/dist/session-brain.js +112 -0
- package/dist/slash-completion.js +1 -0
- package/dist/ui/app.js +154 -30
- package/dist/ui/input-view.js +104 -0
- package/dist/ui/persona-wizard.js +89 -0
- package/dist/ui/render.js +87 -5
- package/dist/ui/tool-activity.js +118 -0
- package/dist/ui/tool-trail.js +15 -3
- package/dist/usage-cli.js +160 -0
- package/dist/usage-ledger.js +169 -0
- package/package.json +11 -2
- package/scripts/postinstall.mjs +4 -4
package/README.th.md
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
# Sanook CLI
|
|
4
4
|
|
|
5
|
-
**AI coding agent ใน terminal
|
|
5
|
+
**AI coding agent ใน terminal ที่จำงานข้าม session ได้ — open-source**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<sub>BYOK · 9 providers · MCP · Obsidian second brain · gateway & cron</sub>
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/sanook-cli)
|
|
10
|
+
[](https://www.npmjs.com/package/sanook-cli)
|
|
11
|
+
[](LICENSE)
|
|
12
12
|
|
|
13
|
-
🇬🇧 [Read in English](README.md)
|
|
13
|
+
🇬🇧 [Read in English](README.md) · รายละเอียด memory / second brain อัปเดตใน [README ภาษาอังกฤษ § Memory](README.md#memory--second-brain)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -31,9 +31,11 @@ prompt → LLM → เรียก tool → ผลลัพธ์ → loop →
|
|
|
31
31
|
ติดตั้งแบบ **global** (ต้องมี `-g`) — ต้องมี **Node ≥ 22** (เช็กด้วย `node -v`):
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npm install -g sanook-cli
|
|
34
|
+
npm install -g sanook-cli # หรือ: npx sanook-cli
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
ช่องทางติดตั้งหลายแพลตฟอร์ม (curl · PowerShell · Homebrew · WinGet) ดูตารางใน [README หลัก](README.md#quickstart) — ตอนนี้ใช้ได้จริงคือ **npm/npx**, ช่องทางอื่น scaffold ไว้แล้วรอ setup ครั้งเดียว (`docs/INSTALL_INFRA.md`)
|
|
38
|
+
|
|
37
39
|
> ⚠️ **`'sanook' is not recognized` / command not found?**
|
|
38
40
|
> แปลว่าลงแบบ local — `npm i sanook-cli` (ไม่มี `-g`) มันลงในโฟลเดอร์ปัจจุบัน **ไม่เข้า PATH** คำสั่ง `sanook` เลยหาไม่เจอ
|
|
39
41
|
> แก้: ลงใหม่ด้วย `npm install -g sanook-cli` · หรือเรียกผ่าน **`npx sanook`** (ใช้ตัวที่ลง local ไปแล้วได้เลย)
|
|
@@ -44,6 +46,8 @@ npm install -g sanook-cli
|
|
|
44
46
|
```bash
|
|
45
47
|
sanook setup # เลือก provider + model และเสนอสร้าง second brain
|
|
46
48
|
sanook model # กลับมาเปลี่ยน provider/model ภายหลัง
|
|
49
|
+
sanook persona # ตอบคำถามสั้นๆ ให้ AI รู้จักคุณ (เก็บลง memory + second brain)
|
|
50
|
+
sanook dashboard # เปิด web admin UI (terminal/skills/memory/usage/install)
|
|
47
51
|
sanook auth add anthropic --api-key sk-ant-... --use
|
|
48
52
|
|
|
49
53
|
# macOS / Linux
|
|
@@ -132,7 +136,10 @@ Microsoft Teams ตอนนี้รองรับ proactive delivery/cron ผ
|
|
|
132
136
|
|
|
133
137
|
- **BYOK + 9 providers** — Anthropic, Google, OpenAI, xAI, Mistral, Groq, Ollama, LM Studio, Codex
|
|
134
138
|
- **Familiar CLI** — `sanook setup`, `sanook model`, `sanook auth`, `sanook chat -q`, `sanook gateway`, `sanook status`, `sanook sessions`, `sanook dump`, `sanook tools`, `sanook send`
|
|
135
|
-
- **Second brain** — `sanook brain init` สร้าง workspace Obsidian ให้ AI จำงานข้ามวัน
|
|
139
|
+
- **Second brain** — `sanook brain init` สร้าง workspace Obsidian ให้ AI จำงานข้ามวัน · ทุกโฟลเดอร์มี `_Index.md` บอกหน้าที่ + มี `Vault Structure Map.md` รวมทั้งหมด
|
|
140
|
+
- **Persona** — `sanook persona` ตอบคำถาม (A/B/C/D + พิมพ์เอง) ให้ AI รู้จักคุณ แล้วเก็บเป็น protected memory + โปรไฟล์ใน vault (`Shared/User-Persona/persona.md`)
|
|
141
|
+
- **Self-improvement** — ตรวจงานที่สั่งซ้ำ แล้วสร้าง skill ให้อัตโนมัติเมื่อถึง threshold (แจ้งใน terminal + เห็นใน Dashboard) · ปิดด้วย `SANOOK_DISABLE_SELF_IMPROVE=1`
|
|
142
|
+
- **Dashboard** — `sanook dashboard` เปิด web admin UI (loopback): web terminal (agent console + raw shell), skills, memory, usage, self-improve, install
|
|
136
143
|
- **Startup cockpit** — เปิด `sanook` แล้วเห็น wordmark, service routes (Code, Brain, Connect, Ship), และสัญญาณ readiness จริงของ second-brain, MCP, skills, git branch ทันที
|
|
137
144
|
- **Web grounding** — `sanook web status` แยก local brain search ออกจาก true internet search, ตรวจ MCP web/search/fetch ที่ตั้งไว้, และโชว์ policy เรื่อง citation + prompt-injection จากเว็บ
|
|
138
145
|
- **Tools** — อ่าน/เขียน/แก้ไฟล์ · รัน bash/Python/Rust แบบ approval-gated · git · grep/glob พร้อม permission gate
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/auto-maintain.ts — "second brain ดูแลตัวเอง" — งาน maintenance ที่เคยต้องสั่งเอง
|
|
3
|
+
// (sanook brain consolidate / distill) ให้ทำอัตโนมัติ:
|
|
4
|
+
// • startup: ถ้าครบ ~1 สัปดาห์ → consolidate memory + vault (dedup, archive stale, index) แบบ background
|
|
5
|
+
// • exit / headless turn: distill บทสนทนา → durable memory (knowledge compound เอง)
|
|
6
|
+
// ทั้งหมด best-effort (ไม่ทำให้ flow ล้ม) และ "ไม่ลบของ" — ใช้ archive (กู้คืนได้) ไม่ใช่ delete.
|
|
7
|
+
// ปิดได้: config `autoMaintain=false` หรือ env SANOOK_DISABLE_AUTO_MAINTAIN=1.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { appHomePath, envFlag, persistenceEnabled } from './brand.js';
|
|
11
|
+
import { loadConfig } from './config.js';
|
|
12
|
+
const STATE_FILE = 'auto-maintain.json';
|
|
13
|
+
/** วิ่ง vault/memory consolidation อย่างมากสัปดาห์ละครั้ง — กัน startup ทำงานหนักทุกครั้ง */
|
|
14
|
+
const CONSOLIDATE_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
15
|
+
/** distill เก็บได้สูงสุดกี่ fact ต่อ session — กัน memory ท่วมจาก session เดียว */
|
|
16
|
+
const MAX_DISTILL_FACTS = 8;
|
|
17
|
+
/**
|
|
18
|
+
* auto-maintenance เปิดโดย default. ปิดเมื่อ:
|
|
19
|
+
* - persistence ปิด (ไม่มีที่เก็บ memory อยู่แล้ว)
|
|
20
|
+
* - env SANOOK_DISABLE_AUTO_MAINTAIN=1
|
|
21
|
+
* - config autoMaintain === false (ผู้ใช้ตั้งปิดเอง)
|
|
22
|
+
*/
|
|
23
|
+
export async function autoMaintainEnabled() {
|
|
24
|
+
if (!persistenceEnabled())
|
|
25
|
+
return false;
|
|
26
|
+
if (envFlag('SANOOK_DISABLE_AUTO_MAINTAIN'))
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
const cfg = await loadConfig({});
|
|
30
|
+
return cfg.autoMaintain !== false;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return true; // อ่าน config ไม่ได้ → default on
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function readState() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(appHomePath(STATE_FILE), 'utf8');
|
|
39
|
+
const v = JSON.parse(raw);
|
|
40
|
+
return { lastConsolidate: typeof v.lastConsolidate === 'number' ? v.lastConsolidate : 0 };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { lastConsolidate: 0 };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function writeState(state) {
|
|
47
|
+
try {
|
|
48
|
+
await writeFile(appHomePath(STATE_FILE), `${JSON.stringify(state, null, 2)}\n`);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* best-effort — ไม่ critical ถ้าเขียน state ไม่ได้ */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** true ถ้าถึงกำหนด consolidate รอบถัดไปแล้ว (เปิด + ครบ interval) — แยกไว้ให้ test ได้ */
|
|
55
|
+
export async function isConsolidationDue(now = Date.now()) {
|
|
56
|
+
if (!(await autoMaintainEnabled()))
|
|
57
|
+
return false;
|
|
58
|
+
const { lastConsolidate } = await readState();
|
|
59
|
+
return now - lastConsolidate >= CONSOLIDATE_INTERVAL_MS;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* เรียกตอน REPL เริ่ม (background, fire-and-forget): ถ้าครบสัปดาห์ → consolidate memory + vault
|
|
63
|
+
* (dedup, archive stale ตาม decay, รัน retrieval ข้ามเพื่อความเร็ว) แล้วจดเวลาไว้. คืน status สั้น
|
|
64
|
+
* ถ้ารัน, null ถ้าข้าม/ปิด. ไม่ throw (best-effort) เพื่อไม่กระทบการเปิด REPL.
|
|
65
|
+
*/
|
|
66
|
+
export async function maybeStartupMaintain(now = Date.now()) {
|
|
67
|
+
if (!(await isConsolidationDue(now)))
|
|
68
|
+
return null;
|
|
69
|
+
// จดเวลา "ก่อน" รัน — กัน REPL หลายตัว/รันซ้อนยิง consolidate พร้อมกัน (จดทันทีถือว่า claim รอบนี้)
|
|
70
|
+
await writeState({ lastConsolidate: now });
|
|
71
|
+
try {
|
|
72
|
+
const cfg = await loadConfig({});
|
|
73
|
+
const { runBrainConsolidate } = await import('./brain-consolidate.js');
|
|
74
|
+
const report = await runBrainConsolidate({
|
|
75
|
+
brainPath: cfg.brainPath,
|
|
76
|
+
apply: true,
|
|
77
|
+
archive: true, // ย้าย stale → archive (กู้คืนได้) ไม่ลบ
|
|
78
|
+
memory: true,
|
|
79
|
+
runRetrieval: false, // ข้าม retrieval eval ตอน startup เพื่อความเร็ว
|
|
80
|
+
});
|
|
81
|
+
if (!report.ok)
|
|
82
|
+
return null;
|
|
83
|
+
const changes = (report.steps ?? []).reduce((n, s) => n + (s.applied?.length ?? 0), 0);
|
|
84
|
+
return changes > 0 ? `auto-maintain: จัดระเบียบ memory + vault (${changes} รายการ)` : null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* distill บทสนทนา → durable auto-memory (knowledge ที่ compound ข้าม session). เรียกตอนจบ session
|
|
92
|
+
* (REPL exit) และตอนจบ turn (headless). best-effort, ไม่ throw. คืนจำนวน fact ที่เขียน.
|
|
93
|
+
*/
|
|
94
|
+
export async function autoDistillToMemory(messages) {
|
|
95
|
+
if (!Array.isArray(messages) || !messages.length)
|
|
96
|
+
return 0;
|
|
97
|
+
if (!(await autoMaintainEnabled()))
|
|
98
|
+
return 0;
|
|
99
|
+
try {
|
|
100
|
+
const { distilledFactsFromMessages } = await import('./session-distill.js');
|
|
101
|
+
const { appendMemory } = await import('./memory.js');
|
|
102
|
+
const facts = distilledFactsFromMessages(messages).slice(0, MAX_DISTILL_FACTS);
|
|
103
|
+
let written = 0;
|
|
104
|
+
for (const fact of facts) {
|
|
105
|
+
await appendMemory(fact).catch(() => { });
|
|
106
|
+
written += 1;
|
|
107
|
+
}
|
|
108
|
+
return written;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/bin.js
CHANGED
|
@@ -34,7 +34,7 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
34
34
|
process.stderr.write(`${DIM}⚠ fallback model ${fallbackModel} ไม่มี pricing → budget cap จะไม่ทำงานถ้า fallback ถูกใช้${RESET}\n`);
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
|
-
const { cost, messages } = await runAgent({
|
|
37
|
+
const { cost, messages, text } = await runAgent({
|
|
38
38
|
model,
|
|
39
39
|
fallbackModel,
|
|
40
40
|
prompt,
|
|
@@ -65,22 +65,41 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
65
65
|
}
|
|
66
66
|
// จำ session ไว้ทำงานต่อได้ (sanook --continue "...") — แก้ concern AI ลืมว่าทำถึงไหน
|
|
67
67
|
const now = new Date().toISOString();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const headlessSessionId = newSessionId();
|
|
69
|
+
await saveSession({ id: headlessSessionId, created: now, updated: now, model, cwd: process.cwd(), messages });
|
|
70
|
+
// auto-worklog (ย่อ) + บทสนทนาเต็ม (ถ้าเปิด brainTranscript) เข้า second-brain
|
|
71
|
+
const { getBrainPath, appendBrainWorklog, appendBrainTranscript } = await import('./memory.js');
|
|
71
72
|
const brain = await getBrainPath();
|
|
72
73
|
if (brain) {
|
|
73
74
|
await appendBrainWorklog(brain, { prompt, summary: cost.summary(), model, today: now.slice(0, 10) }).catch(() => { });
|
|
75
|
+
await appendBrainTranscript(brain, {
|
|
76
|
+
sessionId: headlessSessionId,
|
|
77
|
+
prompt,
|
|
78
|
+
answer: text,
|
|
79
|
+
model,
|
|
80
|
+
createdIso: now,
|
|
81
|
+
}).catch(() => { });
|
|
82
|
+
}
|
|
83
|
+
// self-improvement: งานเดิมที่สั่งซ้ำถึง threshold → สร้าง skill อัตโนมัติ + แจ้งใน stderr
|
|
84
|
+
try {
|
|
85
|
+
const { maybeAutoSkill } = await import('./self-improve.js');
|
|
86
|
+
const { defaultSkillSynthesizer } = await import('./self-improve-synth.js');
|
|
87
|
+
const { loadSkills, saveSkill } = await import('./skills.js');
|
|
88
|
+
const existing = new Set((await loadSkills()).map((s) => s.name));
|
|
89
|
+
const auto = await maybeAutoSkill(prompt, { synthesize: defaultSkillSynthesizer(model), saveSkill, existingSkillNames: existing });
|
|
90
|
+
if (auto.created && auto.announcement)
|
|
91
|
+
process.stderr.write(`${auto.announcement}\n`);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* best-effort */
|
|
74
95
|
}
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
for (const fact of distilledFactsFromMessages(messages))
|
|
83
|
-
await appendMemory(fact).catch(() => { });
|
|
96
|
+
// auto-distill durable decisions/gotchas/preferences from this turn into the compounding memory store
|
|
97
|
+
// so the self-retrieving brain surfaces them next time. Default ON via autoMaintain (gating + cap live
|
|
98
|
+
// in auto-maintain.ts); legacy SANOOK_AUTO_DISTILL still forces it on. Best-effort.
|
|
99
|
+
{
|
|
100
|
+
const { autoDistillToMemory, autoMaintainEnabled } = await import('./auto-maintain.js');
|
|
101
|
+
if (envFlag('SANOOK_AUTO_DISTILL') || (await autoMaintainEnabled()))
|
|
102
|
+
await autoDistillToMemory(messages);
|
|
84
103
|
}
|
|
85
104
|
}
|
|
86
105
|
catch (err) {
|
|
@@ -129,9 +148,11 @@ usage:
|
|
|
129
148
|
${BRAND.cliName} setup [section] setup wizard (model | gateway | tools | agent | brain)
|
|
130
149
|
${BRAND.cliName} dashboard [--port] Sanook Dashboard (local web admin UI)
|
|
131
150
|
${BRAND.cliName} model choose provider + model
|
|
151
|
+
${BRAND.cliName} persona ตอบคำถาม persona → AI เข้าใจคุณ (เก็บลง memory + second-brain)
|
|
132
152
|
${BRAND.cliName} --json "<task>" headless, JSONL output (for CI/scripts)
|
|
133
153
|
${BRAND.cliName} sessions list/resume-audit saved conversation sessions
|
|
134
154
|
${BRAND.cliName} insights local usage/session insights
|
|
155
|
+
${BRAND.cliName} usage [daily|...] token/cost ledger (ccusage-style)
|
|
135
156
|
${BRAND.cliName} dump [--show-keys] support snapshot (secrets redacted)
|
|
136
157
|
${BRAND.cliName} prompt-size [--json] inspect prompt/context budget without calling a model
|
|
137
158
|
${BRAND.cliName} runtimes [--json] inspect optional Python/Rust runtime surface
|
|
@@ -215,7 +236,7 @@ config & mcp:
|
|
|
215
236
|
${BRAND.cliName} runtimes [--json] ดู Python/Rust optional runtime + บทบาทใน Sanook
|
|
216
237
|
${BRAND.cliName} web status [--json] ดู web-search/fetch readiness และ grounding policy
|
|
217
238
|
${BRAND.cliName} tools ดู tool surface ที่ agent ใช้ได้
|
|
218
|
-
${BRAND.cliName} config [get|set <k> <v>] ดู/แก้ ${appHomePath('config.json')} (model/budgetUsd/permissionMode/cacheTtl/compaction/contextCompression/thinking/embeddingModel)
|
|
239
|
+
${BRAND.cliName} config [get|set <k> <v>] ดู/แก้ ${appHomePath('config.json')} (model/fallbackModel/budgetUsd/maxSteps/permissionMode/brainPath/brainTranscript/autoMaintain/cacheTtl/compaction/contextCompression/thinking/summaryModel/embeddingModel/personality)
|
|
219
240
|
${BRAND.cliName} mcp [search|info|install|test|doctor|enable|disable|preset|list|add|remove] จัดการ MCP servers
|
|
220
241
|
${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
|
|
221
242
|
|
|
@@ -308,6 +329,16 @@ async function runDashboard(args = []) {
|
|
|
308
329
|
process.on('SIGINT', shutdown);
|
|
309
330
|
process.on('SIGTERM', shutdown);
|
|
310
331
|
}
|
|
332
|
+
async function runPersona(args = []) {
|
|
333
|
+
if (args[0] === '-h' || args[0] === '--help') {
|
|
334
|
+
console.log(`ใช้: ${BRAND.cliName} persona`);
|
|
335
|
+
console.log('ตอบคำถามสั้นๆ เพื่อบอก AI ว่าคุณเป็นใคร + อยากให้ทำงานยังไง');
|
|
336
|
+
console.log('— บันทึกลง auto-memory (protected) และโปรไฟล์ใน second-brain vault');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const { startPersonaSetup } = await import('./ui/render.js');
|
|
340
|
+
await startPersonaSetup();
|
|
341
|
+
}
|
|
311
342
|
async function runTools(_args = []) {
|
|
312
343
|
const { tools } = await import('./tools/index.js');
|
|
313
344
|
const names = Object.keys(tools).sort();
|
|
@@ -526,12 +557,15 @@ async function runAgentSetupSummary() {
|
|
|
526
557
|
console.log(` maxSteps: ${cfg.maxSteps}`);
|
|
527
558
|
console.log(` budgetUsd: ${cfg.budgetUsd ?? '(not set)'}`);
|
|
528
559
|
console.log(` brainPath: ${cfg.brainPath ?? '(not set)'}`);
|
|
560
|
+
console.log(` brainTranscript:${cfg.brainTranscript ? ' on' : ' off'}`);
|
|
561
|
+
console.log(` autoMaintain: ${cfg.autoMaintain === false ? 'off' : 'on'} (consolidate+distill อัตโนมัติ)`);
|
|
529
562
|
console.log(` insights: ${BRAND.cliName} insights [--days N]`);
|
|
530
563
|
console.log('\nแก้ค่าได้ด้วย:');
|
|
531
564
|
console.log(` ${BRAND.cliName} config set personality concise`);
|
|
532
565
|
console.log(` ${BRAND.cliName} config set permissionMode ask`);
|
|
533
566
|
console.log(` ${BRAND.cliName} config set budgetUsd 0.25`);
|
|
534
567
|
console.log(` ${BRAND.cliName} config set fallbackModel haiku`);
|
|
568
|
+
console.log(` ${BRAND.cliName} config set brainTranscript on`);
|
|
535
569
|
}
|
|
536
570
|
async function runGatewayDoctor() {
|
|
537
571
|
const { checkGateway, formatGatewayDoctorReport } = await import('./gateway/doctor.js');
|
|
@@ -2317,6 +2351,17 @@ async function runSessions(args) {
|
|
|
2317
2351
|
console.error(`ไม่รู้จัก: sessions ${action}\n${sessionUsage()}`);
|
|
2318
2352
|
process.exit(1);
|
|
2319
2353
|
}
|
|
2354
|
+
async function runUsage(args) {
|
|
2355
|
+
const { parseUsageArgs, renderUsageReport, usageHelpText } = await import('./usage-cli.js');
|
|
2356
|
+
const parsed = parseUsageArgs(args);
|
|
2357
|
+
if (!parsed) {
|
|
2358
|
+
console.log(usageHelpText());
|
|
2359
|
+
if (args.length && !args.includes('-h') && !args.includes('--help'))
|
|
2360
|
+
process.exit(2);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
console.log(await renderUsageReport(parsed));
|
|
2364
|
+
}
|
|
2320
2365
|
async function runInsights(args) {
|
|
2321
2366
|
const { parseInsightsArgs } = await import('./insights-args.js');
|
|
2322
2367
|
const parsed = parseInsightsArgs(args);
|
|
@@ -3424,6 +3469,8 @@ async function runConfig(args) {
|
|
|
3424
3469
|
'maxSteps',
|
|
3425
3470
|
'permissionMode',
|
|
3426
3471
|
'brainPath',
|
|
3472
|
+
'brainTranscript',
|
|
3473
|
+
'autoMaintain',
|
|
3427
3474
|
'pricing',
|
|
3428
3475
|
'cacheTtl',
|
|
3429
3476
|
'compaction',
|
|
@@ -3481,6 +3528,17 @@ async function runConfig(args) {
|
|
|
3481
3528
|
const { expandHome } = await import('./brain.js');
|
|
3482
3529
|
value = resolve(expandHome(raw.trim()));
|
|
3483
3530
|
}
|
|
3531
|
+
else if (key === 'brainTranscript' || key === 'autoMaintain') {
|
|
3532
|
+
const v = raw.trim().toLowerCase();
|
|
3533
|
+
if (['on', 'true', '1', 'yes'].includes(v))
|
|
3534
|
+
value = true;
|
|
3535
|
+
else if (['off', 'false', '0', 'no'].includes(v))
|
|
3536
|
+
value = false;
|
|
3537
|
+
else {
|
|
3538
|
+
console.error(`${key} ต้องเป็น on/off, true/false หรือ yes/no`);
|
|
3539
|
+
process.exit(1);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3484
3542
|
else if (key === 'thinking') {
|
|
3485
3543
|
// เก็บเป็น number (budget) หรือ boolean ให้ตรง ConfigSchema (ไม่เก็บ string)
|
|
3486
3544
|
value = parseThinkingConfigValue(raw);
|
|
@@ -4084,6 +4142,8 @@ async function main() {
|
|
|
4084
4142
|
return runAuth(argv.slice(1));
|
|
4085
4143
|
if (argv[0] === 'sessions' || argv[0] === 'session')
|
|
4086
4144
|
return runSessions(argv.slice(1));
|
|
4145
|
+
if (argv[0] === 'usage')
|
|
4146
|
+
return runUsage(argv.slice(1));
|
|
4087
4147
|
if (argv[0] === 'insights')
|
|
4088
4148
|
return runInsights(argv.slice(1));
|
|
4089
4149
|
if (argv[0] === 'memory' && ['log', 'stats', undefined].includes(argv[1]))
|
|
@@ -4097,6 +4157,8 @@ async function main() {
|
|
|
4097
4157
|
if (argv[0] === 'dashboard' && (argv.length === 1 || !argv[1].startsWith('-') || argv[1] === '--port')) {
|
|
4098
4158
|
return runDashboard(argv.slice(1));
|
|
4099
4159
|
}
|
|
4160
|
+
if (argv[0] === 'persona' && (argv.length === 1 || argv[1].startsWith('-')))
|
|
4161
|
+
return runPersona(argv.slice(1));
|
|
4100
4162
|
if (argv[0] === 'web' && ['status', 'doctor', 'fetch', 'search', 'setup', undefined].includes(argv[1]))
|
|
4101
4163
|
return runWeb(argv.slice(1));
|
|
4102
4164
|
if (argv[0] === 'tools' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
@@ -4136,7 +4198,7 @@ async function main() {
|
|
|
4136
4198
|
// A management command word whose subcommand didn't match any route above → don't silently
|
|
4137
4199
|
// fall through and run it as an LLM task (costly + confusing). These words intentionally
|
|
4138
4200
|
// require a valid subcommand; an NL prompt starting with one can still be quoted as a single arg.
|
|
4139
|
-
const MANAGEMENT_WORDS = new Set(['config', 'mcp', 'brain', 'web', 'trust', 'cron', 'skill', 'init', 'dashboard']);
|
|
4201
|
+
const MANAGEMENT_WORDS = new Set(['config', 'mcp', 'brain', 'web', 'trust', 'cron', 'skill', 'init', 'dashboard', 'persona']);
|
|
4140
4202
|
if (argv[0] && MANAGEMENT_WORDS.has(argv[0]) && argv[1] && !argv[1].startsWith('-')) {
|
|
4141
4203
|
console.error(`${BRAND.cliName}: ไม่รู้จัก subcommand "${argv[0]} ${argv[1]}" — ดูวิธีใช้: ${BRAND.cliName} --help`);
|
|
4142
4204
|
process.exit(1);
|
package/dist/brain-final.js
CHANGED
|
@@ -212,12 +212,13 @@ function instantiateTemplate(raw, options) {
|
|
|
212
212
|
const titleTask = options.task;
|
|
213
213
|
let content = raw
|
|
214
214
|
.replaceAll('YYYY-MM-DD', options.today)
|
|
215
|
-
.replaceAll('<task/topic>', titleTask)
|
|
215
|
+
.replaceAll('<task/topic>', () => titleTask)
|
|
216
216
|
.replace('tags: [template, final-gate, verification, dod]', 'tags: [final-gate, verification, dod]')
|
|
217
217
|
.replace('tags: [template, final-gate, verification, lite]', 'tags: [final-gate, verification, lite]')
|
|
218
218
|
.replace('parent: "[[Templates/_Index]]"', 'parent: "[[Sessions/_Index]]"')
|
|
219
219
|
.replace('up:: [[Templates/_Index]]', 'up:: [[Sessions/_Index]]')
|
|
220
|
-
|
|
220
|
+
// replacer functions throughout — `$`-sequences in the user task/title must be written literally
|
|
221
|
+
.replace('<paste owner request or goal text here>', () => options.task);
|
|
221
222
|
if (options.fromDiff)
|
|
222
223
|
content = injectDiffEvidence(content, options.diffFiles);
|
|
223
224
|
return content;
|
|
@@ -234,7 +235,9 @@ function injectDiffEvidence(content, diffFiles) {
|
|
|
234
235
|
function replaceFilesChangedRows(content, fileRows) {
|
|
235
236
|
const fullPattern = /(Files changed:\n\n\| File\/path \| Change summary \| Evidence \|\n\|---\|---\|---\|\n)\| `(?:<path>|<file>)` \| \| \|/;
|
|
236
237
|
const litePattern = /(Changed files:\n\n\| File \| Change summary \| Evidence \|\n\|---\|---\|---\|\n)\| `(?:<path>|<file>)` \| \| \|/;
|
|
237
|
-
|
|
238
|
+
// replacer functions keep the $1 backreference (the captured header) while writing fileRows
|
|
239
|
+
// literally — a changed file path can legitimately contain a `$`, which a string replacement would mangle.
|
|
240
|
+
return content.replace(fullPattern, (_m, p1) => `${p1}${fileRows}`).replace(litePattern, (_m, p1) => `${p1}${fileRows}`);
|
|
238
241
|
}
|
|
239
242
|
async function readTemplate(brainPath, template) {
|
|
240
243
|
const vaultTemplate = join(brainPath, 'Templates', template);
|
|
@@ -284,7 +287,8 @@ async function maybeAppendSessionIndex(brainPath, relPath, task) {
|
|
|
284
287
|
return false;
|
|
285
288
|
const line = `- ${link} — final gate: ${task}`;
|
|
286
289
|
const marker = '\nup:: [[Home]]';
|
|
287
|
-
|
|
290
|
+
// replacer function so `$`-sequences in the task text aren't interpreted as String.replace patterns
|
|
291
|
+
const next = content.includes(marker) ? content.replace(marker, () => `\n${line}\n${marker}`) : `${content.trimEnd()}\n${line}\n`;
|
|
288
292
|
await writeFile(indexPath, next, 'utf8');
|
|
289
293
|
return true;
|
|
290
294
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { BRAND } from './brand.js';
|
|
4
|
+
import { scaffoldProjectWorkspace } from './project-scaffold.js';
|
|
5
|
+
async function exists(path) {
|
|
6
|
+
try {
|
|
7
|
+
await stat(path);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Wire a freshly scaffolded vault to the current repo: Projects/<slug>/ + SANOOK.md memory stub. */
|
|
15
|
+
export async function linkBrainToProject(options) {
|
|
16
|
+
const cwd = options.cwd ?? process.cwd();
|
|
17
|
+
const brainPath = options.brainPath;
|
|
18
|
+
const title = options.title?.trim() || basename(cwd) || 'Project';
|
|
19
|
+
const today = options.today ?? new Date().toISOString().slice(0, 10);
|
|
20
|
+
const warnings = [];
|
|
21
|
+
const scaffold = await scaffoldProjectWorkspace({
|
|
22
|
+
brainPath,
|
|
23
|
+
title,
|
|
24
|
+
repoPath: cwd,
|
|
25
|
+
today,
|
|
26
|
+
});
|
|
27
|
+
if (!scaffold.ok && scaffold.skipped.length) {
|
|
28
|
+
warnings.push(...scaffold.warnings);
|
|
29
|
+
}
|
|
30
|
+
else if (scaffold.warnings.length) {
|
|
31
|
+
warnings.push(...scaffold.warnings);
|
|
32
|
+
}
|
|
33
|
+
const memoryFile = join(cwd, BRAND.memoryFileName);
|
|
34
|
+
let memoryCreated = false;
|
|
35
|
+
if (!(await exists(memoryFile))) {
|
|
36
|
+
const body = [
|
|
37
|
+
`# ${BRAND.productName} project memory`,
|
|
38
|
+
'',
|
|
39
|
+
`> Linked to second-brain vault: \`${brainPath}\``,
|
|
40
|
+
scaffold.ok || scaffold.slug ? `> Project workspace: \`Projects/${scaffold.slug}/\`` : '',
|
|
41
|
+
'',
|
|
42
|
+
'## Conventions',
|
|
43
|
+
'',
|
|
44
|
+
'- Decisions, gotchas, and preferences discovered in this repo belong here or in the vault.',
|
|
45
|
+
`- Session summaries are auto-written to \`Sessions/\` in the vault on exit (Ctrl+C / /quit).`,
|
|
46
|
+
'',
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join('\n');
|
|
50
|
+
await writeFile(memoryFile, `${body}\n`, 'utf8');
|
|
51
|
+
memoryCreated = true;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
try {
|
|
55
|
+
const current = await readFile(memoryFile, 'utf8');
|
|
56
|
+
if (!current.includes(brainPath)) {
|
|
57
|
+
await writeFile(memoryFile, `${current.trimEnd()}\n\n<!-- ${BRAND.productName} -->\nsecond-brain: ${brainPath}\nproject: Projects/${scaffold.slug}/\n`, 'utf8');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
warnings.push(`Could not update existing ${BRAND.memoryFileName}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
ok: scaffold.ok || scaffold.skipped.length > 0,
|
|
66
|
+
brainPath,
|
|
67
|
+
projectSlug: scaffold.slug,
|
|
68
|
+
projectRelDir: scaffold.relDir,
|
|
69
|
+
memoryFile,
|
|
70
|
+
memoryCreated,
|
|
71
|
+
warnings,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/brain-new.js
CHANGED
|
@@ -173,7 +173,8 @@ export function instantiateNoteTemplate(raw, options) {
|
|
|
173
173
|
let content = raw.replaceAll('YYYY-MM-DD', options.today).replaceAll('{{DATE}}', options.today);
|
|
174
174
|
const config = BRAIN_NOTE_TYPES[options.type];
|
|
175
175
|
for (const placeholder of config.titlePlaceholders) {
|
|
176
|
-
|
|
176
|
+
// replacer function so `$`-sequences in the note title are written literally
|
|
177
|
+
content = content.replaceAll(placeholder, () => options.title);
|
|
177
178
|
}
|
|
178
179
|
content = content.replace(/^# .+$/m, (heading) => {
|
|
179
180
|
if (heading.includes(options.title))
|
|
@@ -192,16 +193,18 @@ export function instantiateNoteTemplate(raw, options) {
|
|
|
192
193
|
return `# ${options.title}`;
|
|
193
194
|
return heading;
|
|
194
195
|
});
|
|
196
|
+
// replacer functions throughout — the parent path is user-derived (--output), so `$`-sequences in it
|
|
197
|
+
// must be written literally, not interpreted as String.replace patterns.
|
|
195
198
|
const parentValue = `"[[${options.parent}]]"`;
|
|
196
199
|
if (/^parent:/m.test(content)) {
|
|
197
|
-
content = content.replace(/^parent:.*$/m, `parent: ${parentValue}`);
|
|
200
|
+
content = content.replace(/^parent:.*$/m, () => `parent: ${parentValue}`);
|
|
198
201
|
}
|
|
199
202
|
else if (/^---[\s\S]*?---/m.test(content)) {
|
|
200
|
-
content = content.replace(/^---\n/m, `---\nparent: ${parentValue}\n`);
|
|
203
|
+
content = content.replace(/^---\n/m, () => `---\nparent: ${parentValue}\n`);
|
|
201
204
|
}
|
|
202
205
|
const upLink = `up:: [[${options.parent}]]`;
|
|
203
206
|
if (content.includes('up:: [['))
|
|
204
|
-
content = content.replace(/^up:: \[\[[^\]]+\]\]\s*$/m, upLink);
|
|
207
|
+
content = content.replace(/^up:: \[\[[^\]]+\]\]\s*$/m, () => upLink);
|
|
205
208
|
else
|
|
206
209
|
content = `${content.trimEnd()}\n\n${upLink}\n`;
|
|
207
210
|
if (options.type === 'golden-case')
|
|
@@ -359,7 +362,8 @@ async function maybeAppendDestinationIndex(brainPath, indexRel, noteRel, title,
|
|
|
359
362
|
return false;
|
|
360
363
|
const line = `- ${link} — ${type}: ${title}`;
|
|
361
364
|
const marker = '\nup:: [[Home]]';
|
|
362
|
-
|
|
365
|
+
// replacer function so `$`-sequences in the note title aren't interpreted as String.replace patterns
|
|
366
|
+
const next = content.includes(marker) ? content.replace(marker, () => `\n${line}\n${marker}`) : `${content.trimEnd()}\n${line}\n`;
|
|
363
367
|
await writeFile(indexPath, next, 'utf8');
|
|
364
368
|
return true;
|
|
365
369
|
}
|
package/dist/brain-repair.js
CHANGED
|
@@ -56,8 +56,9 @@ export function applyPurposeFix(content) {
|
|
|
56
56
|
if (/^>\s+/m.test(content))
|
|
57
57
|
return content;
|
|
58
58
|
const match = content.match(/^---[\s\S]*?---\n?/);
|
|
59
|
+
// replacer function so `$`-sequences in the matched frontmatter are written literally
|
|
59
60
|
if (match)
|
|
60
|
-
return content.replace(match[0], `${match[0]}${PURPOSE_PLACEHOLDER}`);
|
|
61
|
+
return content.replace(match[0], () => `${match[0]}${PURPOSE_PLACEHOLDER}`);
|
|
61
62
|
return `${PURPOSE_PLACEHOLDER}${content}`;
|
|
62
63
|
}
|
|
63
64
|
export function applyParentFix(content, relPath) {
|
|
@@ -68,7 +69,8 @@ export function applyParentFix(content, relPath) {
|
|
|
68
69
|
const match = content.match(/^---\n([\s\S]*?)---\n/);
|
|
69
70
|
if (!match)
|
|
70
71
|
return `---\n${parentLine}\n---\n\n${content}`;
|
|
71
|
-
|
|
72
|
+
// replacer function so `$`-sequences in the inferred parent path aren't interpreted as replace patterns
|
|
73
|
+
return content.replace(/^---\n/, () => `---\n${parentLine}\n`);
|
|
72
74
|
}
|
|
73
75
|
export function applyUpLinkFix(content, relPath) {
|
|
74
76
|
if (content.includes('up:: [['))
|
|
@@ -111,11 +113,12 @@ export function applyContextPackIndexFix(indexContent, packName, packContent) {
|
|
|
111
113
|
const description = extractBlockquotePurpose(packContent) || 'context pack';
|
|
112
114
|
const line = `- ${link} — ${description}`;
|
|
113
115
|
const marker = '\n## Use Rule';
|
|
116
|
+
// replacer functions so `$`-sequences in the pack description aren't interpreted as replace patterns
|
|
114
117
|
if (indexContent.includes(marker))
|
|
115
|
-
return indexContent.replace(marker, `\n${line}\n${marker}`);
|
|
118
|
+
return indexContent.replace(marker, () => `\n${line}\n${marker}`);
|
|
116
119
|
const upMarker = '\nup:: [[Shared/_Index]]';
|
|
117
120
|
if (indexContent.includes(upMarker))
|
|
118
|
-
return indexContent.replace(upMarker, `\n${line}\n${upMarker}`);
|
|
121
|
+
return indexContent.replace(upMarker, () => `\n${line}\n${upMarker}`);
|
|
119
122
|
return `${indexContent.trimEnd()}\n${line}\n`;
|
|
120
123
|
}
|
|
121
124
|
export async function collectRepairActions(brainPath, expectedFolders = FOLDERS.map((folder) => folder.dir)) {
|
package/dist/brand.js
CHANGED
|
@@ -24,6 +24,10 @@ export const BRAND_ENV = {
|
|
|
24
24
|
disablePersistence: 'SANOOK_DISABLE_PERSISTENCE',
|
|
25
25
|
disableUpdateCheck: 'SANOOK_DISABLE_UPDATE_CHECK',
|
|
26
26
|
disableWorklog: 'SANOOK_DISABLE_WORKLOG',
|
|
27
|
+
disableUsageLedger: 'SANOOK_DISABLE_USAGE',
|
|
28
|
+
brainTranscript: 'SANOOK_BRAIN_TRANSCRIPT',
|
|
29
|
+
disableSelfImprove: 'SANOOK_DISABLE_SELF_IMPROVE',
|
|
30
|
+
selfImproveThreshold: 'SANOOK_SELF_IMPROVE_THRESHOLD',
|
|
27
31
|
trustProject: 'SANOOK_TRUST_PROJECT',
|
|
28
32
|
};
|
|
29
33
|
export function appHomePath(...parts) {
|
|
@@ -45,3 +49,20 @@ export function persistenceEnabled() {
|
|
|
45
49
|
export function worklogEnabled() {
|
|
46
50
|
return !envFlag(BRAND_ENV.disableWorklog);
|
|
47
51
|
}
|
|
52
|
+
export function usageLedgerEnabled() {
|
|
53
|
+
return persistenceEnabled() && !envFlag(BRAND_ENV.disableUsageLedger);
|
|
54
|
+
}
|
|
55
|
+
/** env-level force for full-transcript-to-vault; config.brainTranscript is the persistent toggle */
|
|
56
|
+
export function brainTranscriptEnvForced() {
|
|
57
|
+
return envFlag(BRAND_ENV.brainTranscript);
|
|
58
|
+
}
|
|
59
|
+
/** self-improvement (auto-skill จากงานที่ทำซ้ำ) — เปิด default, ปิดด้วย SANOOK_DISABLE_SELF_IMPROVE=1 */
|
|
60
|
+
export function selfImproveEnabled() {
|
|
61
|
+
return persistenceEnabled() && !envFlag(BRAND_ENV.disableSelfImprove);
|
|
62
|
+
}
|
|
63
|
+
/** จำนวนครั้งที่งานคล้ายกันต้องเกิดก่อน auto-สร้าง skill (default 3) — override ด้วย env */
|
|
64
|
+
export function selfImproveThreshold() {
|
|
65
|
+
const raw = process.env[BRAND_ENV.selfImproveThreshold]?.trim();
|
|
66
|
+
const n = raw ? Number(raw) : NaN;
|
|
67
|
+
return Number.isInteger(n) && n >= 2 ? n : 3;
|
|
68
|
+
}
|
package/dist/commands.js
CHANGED
|
@@ -15,6 +15,7 @@ export const HELP_TEXT = `คำสั่ง:
|
|
|
15
15
|
/model [spec] ดู/เปลี่ยน model — /model เปิด picker 2 ขั้น (provider → model)
|
|
16
16
|
/setup ดูขั้นตอน setup wizard (model · agent · tools · gateway · brain)
|
|
17
17
|
/dashboard เปิด Sanook Dashboard (local web UI)
|
|
18
|
+
/persona ตั้งค่า persona (ใครคุณเป็น + อยากให้ AI ทำงานยังไง)
|
|
18
19
|
/personality [name]
|
|
19
20
|
ดู/ตั้ง personality overlay
|
|
20
21
|
/details [thinking|tools] [hidden|collapsed|expanded]
|
|
@@ -206,6 +207,8 @@ export function parseCommand(input, ctx) {
|
|
|
206
207
|
handled: true,
|
|
207
208
|
message: `Sanook Dashboard — รัน: ${BRAND.cliName} dashboard\n แล้วเปิด http://127.0.0.1:9119 (Chat · Files · Logs · Cron · Channels)`,
|
|
208
209
|
};
|
|
210
|
+
case 'persona':
|
|
211
|
+
return { handled: true, action: 'personaSetup', message: 'เปิด persona wizard…' };
|
|
209
212
|
case 'personality': {
|
|
210
213
|
const raw = args.join(' ').trim();
|
|
211
214
|
if (!raw)
|
|
@@ -282,7 +285,10 @@ export function parseCommand(input, ctx) {
|
|
|
282
285
|
return { handled: true, action: 'rewind' };
|
|
283
286
|
case 'cost':
|
|
284
287
|
case 'usage':
|
|
285
|
-
return {
|
|
288
|
+
return {
|
|
289
|
+
handled: true,
|
|
290
|
+
message: `${ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)'}\n→ ${BRAND.cliName} usage daily`,
|
|
291
|
+
};
|
|
286
292
|
case 'insights': {
|
|
287
293
|
const parsed = parseInsightsArgs(args);
|
|
288
294
|
if (parsed === null)
|