sanook-cli 0.5.7 → 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 +34 -0
- package/README.md +392 -42
- package/README.th.md +15 -8
- package/dist/auto-maintain.js +113 -0
- package/dist/bin.js +63 -15
- package/dist/brain-final.js +8 -4
- package/dist/brain-new.js +9 -5
- package/dist/brain-repair.js +7 -4
- package/dist/brand.js +17 -0
- package/dist/commands.js +3 -0
- package/dist/config.js +5 -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/install-info.js +91 -0
- package/dist/memory.js +236 -16
- package/dist/persona.js +300 -0
- package/dist/project-scaffold.js +4 -2
- package/dist/self-improve-synth.js +86 -0
- package/dist/self-improve.js +203 -0
- package/dist/session-brain.js +10 -1
- package/dist/slash-completion.js +1 -0
- package/dist/ui/app.js +118 -27
- package/dist/ui/input-view.js +104 -0
- package/dist/ui/persona-wizard.js +89 -0
- package/dist/ui/render.js +78 -5
- package/dist/ui/tool-activity.js +118 -0
- package/dist/ui/tool-trail.js +15 -3
- package/package.json +9 -1
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,6 +148,7 @@ 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
|
|
@@ -216,7 +236,7 @@ config & mcp:
|
|
|
216
236
|
${BRAND.cliName} runtimes [--json] ดู Python/Rust optional runtime + บทบาทใน Sanook
|
|
217
237
|
${BRAND.cliName} web status [--json] ดู web-search/fetch readiness และ grounding policy
|
|
218
238
|
${BRAND.cliName} tools ดู tool surface ที่ agent ใช้ได้
|
|
219
|
-
${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)
|
|
220
240
|
${BRAND.cliName} mcp [search|info|install|test|doctor|enable|disable|preset|list|add|remove] จัดการ MCP servers
|
|
221
241
|
${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
|
|
222
242
|
|
|
@@ -309,6 +329,16 @@ async function runDashboard(args = []) {
|
|
|
309
329
|
process.on('SIGINT', shutdown);
|
|
310
330
|
process.on('SIGTERM', shutdown);
|
|
311
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
|
+
}
|
|
312
342
|
async function runTools(_args = []) {
|
|
313
343
|
const { tools } = await import('./tools/index.js');
|
|
314
344
|
const names = Object.keys(tools).sort();
|
|
@@ -527,12 +557,15 @@ async function runAgentSetupSummary() {
|
|
|
527
557
|
console.log(` maxSteps: ${cfg.maxSteps}`);
|
|
528
558
|
console.log(` budgetUsd: ${cfg.budgetUsd ?? '(not set)'}`);
|
|
529
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 อัตโนมัติ)`);
|
|
530
562
|
console.log(` insights: ${BRAND.cliName} insights [--days N]`);
|
|
531
563
|
console.log('\nแก้ค่าได้ด้วย:');
|
|
532
564
|
console.log(` ${BRAND.cliName} config set personality concise`);
|
|
533
565
|
console.log(` ${BRAND.cliName} config set permissionMode ask`);
|
|
534
566
|
console.log(` ${BRAND.cliName} config set budgetUsd 0.25`);
|
|
535
567
|
console.log(` ${BRAND.cliName} config set fallbackModel haiku`);
|
|
568
|
+
console.log(` ${BRAND.cliName} config set brainTranscript on`);
|
|
536
569
|
}
|
|
537
570
|
async function runGatewayDoctor() {
|
|
538
571
|
const { checkGateway, formatGatewayDoctorReport } = await import('./gateway/doctor.js');
|
|
@@ -3436,6 +3469,8 @@ async function runConfig(args) {
|
|
|
3436
3469
|
'maxSteps',
|
|
3437
3470
|
'permissionMode',
|
|
3438
3471
|
'brainPath',
|
|
3472
|
+
'brainTranscript',
|
|
3473
|
+
'autoMaintain',
|
|
3439
3474
|
'pricing',
|
|
3440
3475
|
'cacheTtl',
|
|
3441
3476
|
'compaction',
|
|
@@ -3493,6 +3528,17 @@ async function runConfig(args) {
|
|
|
3493
3528
|
const { expandHome } = await import('./brain.js');
|
|
3494
3529
|
value = resolve(expandHome(raw.trim()));
|
|
3495
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
|
+
}
|
|
3496
3542
|
else if (key === 'thinking') {
|
|
3497
3543
|
// เก็บเป็น number (budget) หรือ boolean ให้ตรง ConfigSchema (ไม่เก็บ string)
|
|
3498
3544
|
value = parseThinkingConfigValue(raw);
|
|
@@ -4111,6 +4157,8 @@ async function main() {
|
|
|
4111
4157
|
if (argv[0] === 'dashboard' && (argv.length === 1 || !argv[1].startsWith('-') || argv[1] === '--port')) {
|
|
4112
4158
|
return runDashboard(argv.slice(1));
|
|
4113
4159
|
}
|
|
4160
|
+
if (argv[0] === 'persona' && (argv.length === 1 || argv[1].startsWith('-')))
|
|
4161
|
+
return runPersona(argv.slice(1));
|
|
4114
4162
|
if (argv[0] === 'web' && ['status', 'doctor', 'fetch', 'search', 'setup', undefined].includes(argv[1]))
|
|
4115
4163
|
return runWeb(argv.slice(1));
|
|
4116
4164
|
if (argv[0] === 'tools' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
@@ -4150,7 +4198,7 @@ async function main() {
|
|
|
4150
4198
|
// A management command word whose subcommand didn't match any route above → don't silently
|
|
4151
4199
|
// fall through and run it as an LLM task (costly + confusing). These words intentionally
|
|
4152
4200
|
// require a valid subcommand; an NL prompt starting with one can still be quoted as a single arg.
|
|
4153
|
-
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']);
|
|
4154
4202
|
if (argv[0] && MANAGEMENT_WORDS.has(argv[0]) && argv[1] && !argv[1].startsWith('-')) {
|
|
4155
4203
|
console.error(`${BRAND.cliName}: ไม่รู้จัก subcommand "${argv[0]} ${argv[1]}" — ดูวิธีใช้: ${BRAND.cliName} --help`);
|
|
4156
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
|
}
|
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
|
@@ -25,6 +25,9 @@ export const BRAND_ENV = {
|
|
|
25
25
|
disableUpdateCheck: 'SANOOK_DISABLE_UPDATE_CHECK',
|
|
26
26
|
disableWorklog: 'SANOOK_DISABLE_WORKLOG',
|
|
27
27
|
disableUsageLedger: 'SANOOK_DISABLE_USAGE',
|
|
28
|
+
brainTranscript: 'SANOOK_BRAIN_TRANSCRIPT',
|
|
29
|
+
disableSelfImprove: 'SANOOK_DISABLE_SELF_IMPROVE',
|
|
30
|
+
selfImproveThreshold: 'SANOOK_SELF_IMPROVE_THRESHOLD',
|
|
28
31
|
trustProject: 'SANOOK_TRUST_PROJECT',
|
|
29
32
|
};
|
|
30
33
|
export function appHomePath(...parts) {
|
|
@@ -49,3 +52,17 @@ export function worklogEnabled() {
|
|
|
49
52
|
export function usageLedgerEnabled() {
|
|
50
53
|
return persistenceEnabled() && !envFlag(BRAND_ENV.disableUsageLedger);
|
|
51
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)
|
package/dist/config.js
CHANGED
|
@@ -32,6 +32,11 @@ export const ConfigSchema = z.object({
|
|
|
32
32
|
permissionMode: z.enum(['auto', 'ask']).default('ask'),
|
|
33
33
|
// path ของ second-brain workspace ที่ scaffold ไว้ (sanook brain) — optional
|
|
34
34
|
brainPath: z.string().optional(),
|
|
35
|
+
// เก็บบทสนทนาเต็ม (prompt + คำตอบ AI ทุก turn) ลง vault Sessions/*-chat.md — opt-in (vault โตไว)
|
|
36
|
+
brainTranscript: z.boolean().optional().catch(undefined),
|
|
37
|
+
// auto-maintenance: consolidate memory+vault อัตโนมัติ (รายสัปดาห์ตอน startup) + distill session → memory.
|
|
38
|
+
// default on (undefined = on); ตั้ง false เพื่อปิด หรือ env SANOOK_DISABLE_AUTO_MAINTAIN=1
|
|
39
|
+
autoMaintain: z.boolean().optional().catch(undefined),
|
|
35
40
|
// pricing override/extension per "provider:model" → ทำให้ budget cap ใช้ได้กับ model ที่ยังไม่มีในตาราง
|
|
36
41
|
pricing: PricingOverrideSchema.optional(),
|
|
37
42
|
// ── token/cost tuning (ดู agentTuning) — .catch กันค่า config.json ผิดทำ boot พัง (agentTuning อ่าน raw + coerce เองด้วย) ──
|