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/README.th.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  # Sanook CLI
4
4
 
5
- **AI coding agent ใน terminal ที่ "จำงานข้ามวันได้" — open-source**
5
+ **AI coding agent ใน terminal ที่จำงานข้าม session ได้ — open-source**
6
6
 
7
- ใส่ API key ของคุณเอง (BYOK) · 9 providers · MCP · มี **"สมองที่สอง" (second brain)** ที่ทำให้ AI จำ context ข้าม session ได้ — สิ่งที่ Claude Code / Codex / Gemini CLI ลืมทุกครั้งที่ปิด terminal
7
+ <sub>BYOK · 9 providers · MCP · Obsidian second brain · gateway & cron</sub>
8
8
 
9
- [![npm](https://img.shields.io/npm/v/sanook-cli.svg?color=2563eb)](https://www.npmjs.com/package/sanook-cli)
10
- [![downloads](https://img.shields.io/npm/dm/sanook-cli.svg?color=2563eb)](https://www.npmjs.com/package/sanook-cli)
11
- [![License](https://img.shields.io/badge/license-Apache--2.0-22c55e.svg)](LICENSE)
9
+ [![npm](https://img.shields.io/npm/v/sanook-cli.svg?style=flat-square&color=111827&labelColor=1f2937)](https://www.npmjs.com/package/sanook-cli)
10
+ [![downloads](https://img.shields.io/npm/dm/sanook-cli.svg?style=flat-square&color=111827&labelColor=1f2937)](https://www.npmjs.com/package/sanook-cli)
11
+ [![License](https://img.shields.io/badge/license-Apache--2.0-22c55e?style=flat-square&labelColor=1f2937)](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
- await saveSession({ id: newSessionId(), created: now, updated: now, model, cwd: process.cwd(), messages });
69
- // auto-worklog เข้า second-brain (ถ้าตั้ง brainPath) "vault จำว่าวันนี้ทำอะไร"
70
- const { getBrainPath, appendBrainWorklog } = await import('./memory.js');
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
- // opt-in (experimental, default OFF): auto-distill durable decisions/gotchas/preferences from this
76
- // session into the compounding memory store so the self-retrieving brain surfaces them next time.
77
- // Off by default per experiment H5 extraction is high-precision (~0.88) but end-to-end recall is
78
- // gated by retrieval quality (semantic helps). Enable with SANOOK_AUTO_DISTILL=1.
79
- if (envFlag('SANOOK_AUTO_DISTILL')) {
80
- const { distilledFactsFromMessages } = await import('./session-distill.js');
81
- const { appendMemory } = await import('./memory.js');
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);
@@ -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
- .replace('<paste owner request or goal text here>', options.task);
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
- return content.replace(fullPattern, `$1${fileRows}`).replace(litePattern, `$1${fileRows}`);
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
- const next = content.includes(marker) ? content.replace(marker, `\n${line}\n${marker}`) : `${content.trimEnd()}\n${line}\n`;
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
- content = content.replaceAll(placeholder, options.title);
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
- const next = content.includes(marker) ? content.replace(marker, `\n${line}\n${marker}`) : `${content.trimEnd()}\n${line}\n`;
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
  }
@@ -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
- return content.replace(/^---\n/, `---\n${parentLine}\n`);
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 เองด้วย) ──