sanook-cli 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +23 -0
- package/CHANGELOG.md +38 -0
- package/LICENSE +201 -0
- package/README.md +239 -0
- package/dist/agentContext.js +2 -0
- package/dist/approval.js +78 -0
- package/dist/bin.js +461 -0
- package/dist/brain.js +186 -0
- package/dist/commands.js +66 -0
- package/dist/compaction.js +85 -0
- package/dist/config.js +101 -0
- package/dist/cost.js +59 -0
- package/dist/diff.js +36 -0
- package/dist/gateway/auth.js +32 -0
- package/dist/gateway/ledger.js +94 -0
- package/dist/gateway/lock.js +114 -0
- package/dist/gateway/schedule.js +74 -0
- package/dist/gateway/scheduler.js +87 -0
- package/dist/gateway/serve.js +57 -0
- package/dist/gateway/server.js +94 -0
- package/dist/gateway/telegram.js +115 -0
- package/dist/git.js +55 -0
- package/dist/hooks.js +104 -0
- package/dist/knowledge.js +68 -0
- package/dist/loop.js +169 -0
- package/dist/mcp.js +191 -0
- package/dist/memory.js +108 -0
- package/dist/providers/codex.js +86 -0
- package/dist/providers/keys.js +37 -0
- package/dist/providers/models.js +55 -0
- package/dist/providers/registry.js +241 -0
- package/dist/session.js +36 -0
- package/dist/skill-install.js +190 -0
- package/dist/skills.js +111 -0
- package/dist/tools/bash.js +26 -0
- package/dist/tools/edit.js +107 -0
- package/dist/tools/git.js +68 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/list.js +24 -0
- package/dist/tools/permission.js +30 -0
- package/dist/tools/read.js +18 -0
- package/dist/tools/recall.js +12 -0
- package/dist/tools/remember.js +14 -0
- package/dist/tools/schedule.js +61 -0
- package/dist/tools/search.js +54 -0
- package/dist/tools/skill.js +65 -0
- package/dist/tools/task.js +46 -0
- package/dist/tools/util.js +5 -0
- package/dist/tools/write.js +27 -0
- package/dist/ui/app.js +132 -0
- package/dist/ui/banner.js +20 -0
- package/dist/ui/brain-wizard.js +29 -0
- package/dist/ui/render.js +57 -0
- package/dist/ui/setup.js +46 -0
- package/package.json +77 -0
- package/second-brain/AGENTS.md +18 -0
- package/second-brain/CLAUDE.md +96 -0
- package/second-brain/Evals/retrieval-eval.md +30 -0
- package/second-brain/GEMINI.md +15 -0
- package/second-brain/Home.md +33 -0
- package/second-brain/README.md +29 -0
- package/second-brain/Runbooks/ingest-quarantine.md +27 -0
- package/second-brain/Runbooks/sleep-time-consolidation.md +26 -0
- package/second-brain/Shared/AI-Context-Index.md +52 -0
- package/second-brain/Shared/Core-Facts/protected-facts.md +21 -0
- package/second-brain/Shared/Decision-Memory/decision-log.md +24 -0
- package/second-brain/Shared/Memory-Inbox/memory-inbox.md +23 -0
- package/second-brain/Shared/Operating-State/current-state.md +30 -0
- package/second-brain/Shared/Provenance/ingest-log.md +27 -0
- package/second-brain/Shared/Rules/context-assembly-policy.md +28 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +33 -0
- package/second-brain/Shared/Rules/skills-admission.md +30 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +25 -0
- package/second-brain/Templates/bug.md +22 -0
- package/second-brain/Templates/handoff.md +21 -0
- package/second-brain/Templates/project.md +24 -0
- package/second-brain/Templates/session.md +26 -0
- package/second-brain/USER.md +36 -0
- package/second-brain/Vault Structure Map.md +106 -0
- package/skills/agent-tool-mcp-builder/SKILL.md +88 -0
- package/skills/api-design-review/SKILL.md +70 -0
- package/skills/async-concurrency-correctness/SKILL.md +93 -0
- package/skills/audit-accessibility-wcag/SKILL.md +59 -0
- package/skills/audit-technical-seo/SKILL.md +62 -0
- package/skills/auth-jwt-session/SKILL.md +88 -0
- package/skills/brainstorm-design/SKILL.md +73 -0
- package/skills/build-etl-pipeline/SKILL.md +58 -0
- package/skills/build-form-validation/SKILL.md +103 -0
- package/skills/build-office-docs/SKILL.md +80 -0
- package/skills/build-react-component/SKILL.md +116 -0
- package/skills/build-spreadsheet/SKILL.md +106 -0
- package/skills/caching-strategy/SKILL.md +75 -0
- package/skills/cicd-pipeline-author/SKILL.md +65 -0
- package/skills/cloud-cost-optimize/SKILL.md +91 -0
- package/skills/code-comments/SKILL.md +52 -0
- package/skills/code-review/SKILL.md +61 -0
- package/skills/db-migration-safety/SKILL.md +67 -0
- package/skills/debug-frontend-browser/SKILL.md +58 -0
- package/skills/debug-root-cause/SKILL.md +54 -0
- package/skills/dependency-upgrade/SKILL.md +56 -0
- package/skills/deploy-release/SKILL.md +64 -0
- package/skills/diff-table-parity/SKILL.md +58 -0
- package/skills/dockerfile-optimize/SKILL.md +82 -0
- package/skills/error-message/SKILL.md +58 -0
- package/skills/estimate-work/SKILL.md +54 -0
- package/skills/explore-codebase/SKILL.md +73 -0
- package/skills/git-commit-pr/SKILL.md +65 -0
- package/skills/gitops-deploy-workflow/SKILL.md +97 -0
- package/skills/implement-from-design/SKILL.md +69 -0
- package/skills/incident-response-sre/SKILL.md +78 -0
- package/skills/k8s-debug-workload/SKILL.md +135 -0
- package/skills/k8s-manifest-review/SKILL.md +86 -0
- package/skills/llm-eval-harness/SKILL.md +63 -0
- package/skills/manage-client-server-state/SKILL.md +94 -0
- package/skills/mermaid-diagram/SKILL.md +61 -0
- package/skills/message-queue-jobs/SKILL.md +139 -0
- package/skills/naming-helper/SKILL.md +57 -0
- package/skills/observability-instrument/SKILL.md +113 -0
- package/skills/optimize-core-web-vitals/SKILL.md +75 -0
- package/skills/optimize-sql-query/SKILL.md +67 -0
- package/skills/performance-profiling/SKILL.md +65 -0
- package/skills/process-pdf/SKILL.md +107 -0
- package/skills/profile-dataset/SKILL.md +97 -0
- package/skills/prompt-engineering/SKILL.md +70 -0
- package/skills/rag-pipeline/SKILL.md +53 -0
- package/skills/rate-limiting/SKILL.md +96 -0
- package/skills/refactor-cleanup/SKILL.md +54 -0
- package/skills/regex-build/SKILL.md +72 -0
- package/skills/release-notes/SKILL.md +79 -0
- package/skills/rest-graphql-contract/SKILL.md +71 -0
- package/skills/scrape-structured-web-data/SKILL.md +61 -0
- package/skills/secrets-management/SKILL.md +96 -0
- package/skills/security-review/SKILL.md +62 -0
- package/skills/shell-script-robust/SKILL.md +71 -0
- package/skills/style-responsive-tailwind/SKILL.md +70 -0
- package/skills/terraform-plan-review/SKILL.md +95 -0
- package/skills/type-safety-strict/SKILL.md +82 -0
- package/skills/validate-data-quality/SKILL.md +62 -0
- package/skills/wrangle-tabular-data/SKILL.md +75 -0
- package/skills/write-adr/SKILL.md +75 -0
- package/skills/write-analytical-sql/SKILL.md +71 -0
- package/skills/write-data-viz/SKILL.md +58 -0
- package/skills/write-docs/SKILL.md +54 -0
- package/skills/write-plan/SKILL.md +59 -0
- package/skills/write-playwright-e2e/SKILL.md +86 -0
- package/skills/write-prd/SKILL.md +65 -0
- package/skills/write-rfc/SKILL.md +75 -0
- package/skills/write-tests/SKILL.md +50 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runAgent } from './loop.js';
|
|
3
|
+
import { redactKey } from './providers/keys.js';
|
|
4
|
+
import { loadConfig, isFirstRun, loadKeysIntoEnv } from './config.js';
|
|
5
|
+
import { saveSession, latestSession, newSessionId } from './session.js';
|
|
6
|
+
import { closeMcp } from './mcp.js';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
11
|
+
const DIM = '\x1b[2m';
|
|
12
|
+
const RESET = '\x1b[0m';
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
let model;
|
|
15
|
+
let budget;
|
|
16
|
+
let json = false;
|
|
17
|
+
let planMode = false;
|
|
18
|
+
let yes = false;
|
|
19
|
+
const rest = [];
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const a = argv[i];
|
|
22
|
+
if (a === '--model' || a === '-m')
|
|
23
|
+
model = argv[++i];
|
|
24
|
+
else if (a === '--budget' || a === '-b')
|
|
25
|
+
budget = Number.parseFloat(argv[++i] ?? '');
|
|
26
|
+
else if (a === '--json')
|
|
27
|
+
json = true;
|
|
28
|
+
else if (a === '--plan')
|
|
29
|
+
planMode = true;
|
|
30
|
+
else if (a === '--yes' || a === '-y')
|
|
31
|
+
yes = true;
|
|
32
|
+
else if (a === '-p' || a === '--print' || a === '-c' || a === '--continue') {
|
|
33
|
+
/* -p headless flag · -c/--continue resume (handled in main) */
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
rest.push(a);
|
|
37
|
+
}
|
|
38
|
+
return { model, budget, json, prompt: rest.join(' ').trim(), planMode, yes };
|
|
39
|
+
}
|
|
40
|
+
async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'auto') {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
process.on('SIGINT', () => {
|
|
43
|
+
controller.abort();
|
|
44
|
+
process.exit(130);
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const { cost, messages } = await runAgent({
|
|
48
|
+
model,
|
|
49
|
+
prompt,
|
|
50
|
+
history,
|
|
51
|
+
budgetUsd,
|
|
52
|
+
maxSteps,
|
|
53
|
+
planMode,
|
|
54
|
+
permissionMode, // headless ไม่มี approve → ask-mode = ปฏิเสธ mutate (ต้อง --yes)
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
onEvent: (e) => {
|
|
57
|
+
if (json) {
|
|
58
|
+
process.stdout.write(`${JSON.stringify(e)}\n`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (e.type === 'text')
|
|
62
|
+
process.stdout.write(e.text ?? '');
|
|
63
|
+
else if (e.type === 'tool-call')
|
|
64
|
+
process.stdout.write(`\n${DIM}→ ${e.tool}${RESET}\n`);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (!json)
|
|
68
|
+
process.stdout.write(`\n${DIM}${cost.summary()}${RESET}\n`);
|
|
69
|
+
// จำ session ไว้ทำงานต่อได้ (sanook --continue "...") — แก้ concern AI ลืมว่าทำถึงไหน
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
await saveSession({ id: newSessionId(), created: now, updated: now, model, cwd: process.cwd(), messages });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const msg = redactKey(err.message);
|
|
75
|
+
if (json)
|
|
76
|
+
process.stdout.write(`${JSON.stringify({ type: 'error', message: msg })}\n`);
|
|
77
|
+
else
|
|
78
|
+
console.error(`\nERROR: ${msg}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// อ่านจาก package.json (single source of truth) — กัน version constant drift
|
|
83
|
+
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
84
|
+
const HELP = `Sanook — a terminal AI coding agent (BYOK)
|
|
85
|
+
|
|
86
|
+
usage:
|
|
87
|
+
sanook "<task>" run one task (headless)
|
|
88
|
+
sanook interactive REPL
|
|
89
|
+
sanook --json "<task>" headless, JSONL output (for CI/scripts)
|
|
90
|
+
|
|
91
|
+
gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
|
|
92
|
+
sanook serve [--port 8787] เปิด gateway (OpenAI-compat /v1/chat/completions + scheduler)
|
|
93
|
+
sanook cron add "<when>" "<task>" ตั้งงานล่วงหน้า (when: "every 30m" | "09:00" | ISO | now)
|
|
94
|
+
sanook cron list ดู task ทั้งหมด
|
|
95
|
+
sanook cron rm <id> ลบ task
|
|
96
|
+
|
|
97
|
+
skills (69 built-in + ติดตั้งเพิ่มได้):
|
|
98
|
+
sanook skill list ดู skill ทั้งหมด
|
|
99
|
+
sanook skill add <user/repo|url|path> ติดตั้ง skill จาก GitHub / URL / local
|
|
100
|
+
sanook skill remove <name> ลบ skill ที่ติดตั้ง
|
|
101
|
+
sanook models [provider] ดู/verify model id (เทียบ provider จริงถ้ามี key)
|
|
102
|
+
|
|
103
|
+
second brain (Obsidian workspace สำหรับจัดเก็บงาน + ความจำ AI):
|
|
104
|
+
sanook brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
|
|
105
|
+
|
|
106
|
+
config & mcp:
|
|
107
|
+
sanook config [get|set <k> <v>] ดู/แก้ ~/.sanook/config.json (model/budgetUsd/permissionMode)
|
|
108
|
+
sanook mcp [list|add <name> <cmd> …|remove <name>] จัดการ MCP servers
|
|
109
|
+
|
|
110
|
+
flags:
|
|
111
|
+
-m, --model <spec> sonnet/opus/haiku/fable · gpt/codex · gemini · grok · deepseek · mistral · groq · ollama/lmstudio
|
|
112
|
+
or "provider:model-id" (e.g. openai:gpt-5-codex, groq:fast, google:gemini-2.5-flash)
|
|
113
|
+
-b, --budget <usd> stop when estimated cost exceeds this
|
|
114
|
+
-c, --continue resume the latest session (จำว่าทำถึงไหน → ทำต่อ)
|
|
115
|
+
--plan plan mode — สำรวจ+วางแผนเท่านั้น ไม่แก้ไฟล์ (read-only)
|
|
116
|
+
-y, --yes อนุมัติ tool อัตโนมัติ (ข้าม ask-mode permission)
|
|
117
|
+
--json machine-readable JSONL output
|
|
118
|
+
-v, --version
|
|
119
|
+
-h, --help
|
|
120
|
+
|
|
121
|
+
env (BYOK — direct API key only):
|
|
122
|
+
ANTHROPIC_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / OPENAI_API_KEY`;
|
|
123
|
+
/** sanook serve [--port N] [--model spec] — เปิด gateway (HTTP loopback + cron scheduler) อยู่ยาว */
|
|
124
|
+
async function runServe(args) {
|
|
125
|
+
const portIdx = args.indexOf('--port');
|
|
126
|
+
const port = portIdx !== -1 ? Number(args[portIdx + 1]) : 8787;
|
|
127
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
128
|
+
console.error(`port ไม่ถูกต้อง: ${args[portIdx + 1]}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const mIdx = args.findIndex((a) => a === '--model' || a === '-m');
|
|
132
|
+
const config = await loadConfig({ model: mIdx !== -1 ? args[mIdx + 1] : undefined });
|
|
133
|
+
const { startGateway } = await import('./gateway/serve.js');
|
|
134
|
+
process.stdout.write(`${DIM}Sanook gateway — model: ${config.model}${RESET}\n`);
|
|
135
|
+
const stop = await startGateway({
|
|
136
|
+
port,
|
|
137
|
+
model: config.model,
|
|
138
|
+
budgetUsd: config.budgetUsd,
|
|
139
|
+
onLog: (m) => process.stdout.write(`${DIM}[gateway] ${m}${RESET}\n`),
|
|
140
|
+
});
|
|
141
|
+
const shutdown = () => {
|
|
142
|
+
stop();
|
|
143
|
+
process.stdout.write('\n[gateway] หยุดแล้ว\n');
|
|
144
|
+
process.exit(0);
|
|
145
|
+
};
|
|
146
|
+
process.on('SIGINT', shutdown);
|
|
147
|
+
process.on('SIGTERM', shutdown);
|
|
148
|
+
// server + scheduler interval ถือ event loop ไว้ → process อยู่ยาวจนกด Ctrl-C
|
|
149
|
+
}
|
|
150
|
+
/** sanook cron add "<when>" "<task>" | cron list | cron rm <id> */
|
|
151
|
+
async function runCron(args) {
|
|
152
|
+
const [action, ...rest] = args;
|
|
153
|
+
const { listTasks, enqueueTask, removeTask } = await import('./gateway/ledger.js');
|
|
154
|
+
if (action === 'add') {
|
|
155
|
+
const schedule = rest[0];
|
|
156
|
+
const spec = rest.slice(1).join(' ').trim();
|
|
157
|
+
if (!schedule || !spec) {
|
|
158
|
+
console.error('ใช้: sanook cron add "<when>" "<task>" (when: "every 30m" | "09:00" | ISO | now)');
|
|
159
|
+
console.error('หมายเหตุ: when ที่มีช่องว่างต้องครอบ quote เช่น "every 30m"');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
const { parseSchedule } = await import('./gateway/schedule.js');
|
|
163
|
+
const sched = parseSchedule(schedule, Date.now());
|
|
164
|
+
if (!sched) {
|
|
165
|
+
console.error(`schedule ไม่ถูกต้อง: "${schedule}" — ลอง "every 30m", "09:00", ISO, หรือ "now"`);
|
|
166
|
+
if (rest.length > 1 && /^(every|\d)/.test(schedule)) {
|
|
167
|
+
console.error('(ดูเหมือนลืมครอบ quote — when ที่มีช่องว่างต้องเป็น "every 30m" ทั้งก้อน)');
|
|
168
|
+
}
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const task = await enqueueTask({
|
|
172
|
+
kind: sched.recurring ? 'cron' : 'once',
|
|
173
|
+
spec,
|
|
174
|
+
schedule: sched.recurring ? sched.normalized : undefined,
|
|
175
|
+
runAt: sched.runAt,
|
|
176
|
+
});
|
|
177
|
+
const when = new Date(task.runAt).toLocaleString();
|
|
178
|
+
console.log(`เพิ่ม task ${task.id} — รัน ${when}${sched.recurring ? ` แล้วทุก ${sched.normalized}` : ''}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (action === 'rm' || action === 'remove') {
|
|
182
|
+
if (!rest[0]) {
|
|
183
|
+
console.error('ใช้: sanook cron rm <id>');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const ok = await removeTask(rest[0]);
|
|
187
|
+
console.log(ok ? `ลบ task ${rest[0]} แล้ว` : `ไม่เจอ task ${rest[0]}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (action === 'list' || action === undefined) {
|
|
191
|
+
const tasks = await listTasks();
|
|
192
|
+
if (!tasks.length) {
|
|
193
|
+
console.log('ยังไม่มี task — เพิ่มด้วย: sanook cron add "every 1h" "เช็คข่าว AI"');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
for (const t of tasks) {
|
|
197
|
+
const next = new Date(t.runAt).toLocaleString();
|
|
198
|
+
console.log(`${t.id} [${t.status}] ${t.schedule ?? 'once'} next:${next} → ${t.spec.slice(0, 50)}`);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
console.error(`ไม่รู้จัก: cron ${action} — ใช้ add / list / rm`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
/** sanook skill list | add <source> | remove <name> */
|
|
206
|
+
async function runSkill(args) {
|
|
207
|
+
const [action, ...rest] = args;
|
|
208
|
+
if (action === 'add') {
|
|
209
|
+
const source = rest[0];
|
|
210
|
+
if (!source) {
|
|
211
|
+
console.error('ใช้: sanook skill add <github "user/repo" | URL ของ SKILL.md | local path>');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
console.error(`${DIM}⚠ skill = instruction ที่ AI จะทำตาม — ติดตั้งจาก source ที่เชื่อถือเท่านั้น${RESET}`);
|
|
215
|
+
const { installSkill } = await import('./skill-install.js');
|
|
216
|
+
try {
|
|
217
|
+
const installed = await installSkill(source, (m) => process.stderr.write(`${DIM}${m}${RESET}\n`));
|
|
218
|
+
console.log(`ติดตั้ง ${installed.length} skill: ${installed.map((s) => s.name).join(', ')}`);
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
console.error(`ติดตั้งไม่สำเร็จ: ${redactKey(e.message)}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (action === 'remove' || action === 'rm') {
|
|
227
|
+
if (!rest[0]) {
|
|
228
|
+
console.error('ใช้: sanook skill remove <name>');
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
const { removeInstalledSkill } = await import('./skill-install.js');
|
|
232
|
+
const ok = await removeInstalledSkill(rest[0]);
|
|
233
|
+
console.log(ok ? `ลบ skill ${rest[0]} แล้ว` : `ไม่เจอ skill ${rest[0]} ที่ติดตั้งไว้ (bundled ลบไม่ได้)`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// list (default)
|
|
237
|
+
const { loadSkills } = await import('./skills.js');
|
|
238
|
+
const skills = await loadSkills();
|
|
239
|
+
console.log(`${skills.length} skills:`);
|
|
240
|
+
for (const s of skills) {
|
|
241
|
+
const d = s.description.length > 72 ? `${s.description.slice(0, 72)}…` : s.description;
|
|
242
|
+
console.log(` ${s.name} — ${d}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** sanook models [provider] — ดู models + verify กับ provider จริง (flag id ที่ stale) */
|
|
246
|
+
async function runModels(args) {
|
|
247
|
+
const { PROVIDERS } = await import('./providers/registry.js');
|
|
248
|
+
const provider = args[0];
|
|
249
|
+
if (!provider) {
|
|
250
|
+
console.log(`providers: ${Object.keys(PROVIDERS).join(' ')}`);
|
|
251
|
+
console.log('ใช้: sanook models <provider> (ใส่ API key ใน env เพื่อ verify กับของจริง)');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const cfg = PROVIDERS[provider];
|
|
255
|
+
if (!cfg) {
|
|
256
|
+
console.error(`ไม่รู้จัก provider "${provider}" — มี: ${Object.keys(PROVIDERS).join(' ')}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
console.log(`${cfg.label} — curated (registry):`);
|
|
260
|
+
for (const [alias, id] of Object.entries(cfg.models))
|
|
261
|
+
console.log(` ${alias.padEnd(10)} → ${id}`);
|
|
262
|
+
if (cfg.kind === 'delegate') {
|
|
263
|
+
console.log('\n(delegate provider — ไม่มี /models endpoint; ใช้ curated id ด้านบน)');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const { resolveKeyFromEnv } = await import('./providers/keys.js');
|
|
267
|
+
const key = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
268
|
+
if (!key && cfg.requiresKey) {
|
|
269
|
+
console.log(`\n(ใส่ ${cfg.envVar} เพื่อ verify model id กับ provider จริง)`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const { listRemoteModels } = await import('./providers/models.js');
|
|
273
|
+
const live = await listRemoteModels(cfg, key ?? cfg.localPlaceholderKey);
|
|
274
|
+
if (!live.length) {
|
|
275
|
+
console.log('\n(ดึง live models ไม่ได้ — endpoint/key)');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
console.log(`\nlive (${live.length} จาก provider):`);
|
|
279
|
+
console.log(` ${live.slice(0, 30).join('\n ')}${live.length > 30 ? '\n …' : ''}`);
|
|
280
|
+
const liveSet = new Set(live);
|
|
281
|
+
const stale = [...new Set(Object.values(cfg.models))].filter((id) => !liveSet.has(id));
|
|
282
|
+
if (stale.length)
|
|
283
|
+
console.log(`\n⚠ id ใน registry ที่ provider ไม่มีแล้ว (อาจ stale): ${stale.join(', ')}`);
|
|
284
|
+
else
|
|
285
|
+
console.log('\n✓ ทุก curated id มีใน provider');
|
|
286
|
+
}
|
|
287
|
+
/** sanook brain init [path] — scaffold second-brain workspace (interactive ถ้าไม่ใส่ path) */
|
|
288
|
+
async function runBrain(args) {
|
|
289
|
+
if (args[0] !== 'init') {
|
|
290
|
+
console.log(`ใช้: sanook brain init [path] สร้างโครงสร้าง second-brain (Obsidian vault)
|
|
291
|
+
ไม่ใส่ path → wizard ถาม path + ตัวตน
|
|
292
|
+
-y, --yes ใช้ค่า default ทั้งหมด (ต้องระบุ path)`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const rest = args.slice(1);
|
|
296
|
+
const yes = rest.includes('-y') || rest.includes('--yes');
|
|
297
|
+
const pathArg = rest.find((a) => !a.startsWith('-'));
|
|
298
|
+
// interactive: ไม่มี path และไม่ --yes → render BrainWizard
|
|
299
|
+
if (!pathArg && !yes) {
|
|
300
|
+
const { startBrainSetup } = await import('./ui/render.js');
|
|
301
|
+
await startBrainSetup();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const { scaffoldBrain, BRAIN_DEFAULTS, expandHome, wireBrainMcp } = await import('./brain.js');
|
|
305
|
+
const target = expandHome(pathArg ?? join(homedir(), 'Documents', BRAIN_DEFAULTS.vaultName));
|
|
306
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
307
|
+
try {
|
|
308
|
+
const res = await scaffoldBrain(target, { ...BRAIN_DEFAULTS, today });
|
|
309
|
+
const { saveBrainPath } = await import('./config.js');
|
|
310
|
+
await saveBrainPath(target);
|
|
311
|
+
const wired = await wireBrainMcp(target).catch(() => 'skip');
|
|
312
|
+
console.log(`✅ second-brain — ${target}`);
|
|
313
|
+
console.log(` สร้าง ${res.created.length} ไฟล์/โฟลเดอร์ · ข้าม ${res.skipped.length} (มีอยู่แล้ว ไม่ทับ)`);
|
|
314
|
+
console.log(` ${wired === 'added' ? 'wire filesystem MCP เข้า vault แล้ว' : 'MCP: มี server เดิม (ไม่ทับ)'}`);
|
|
315
|
+
console.log(` เปิดใน Obsidian: Open folder as vault → ${target}`);
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
console.error(`สร้างไม่สำเร็จ: ${e.message}`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** อ่าน stdin จนจบ (เมื่อถูก pipe เข้ามา เช่น `git diff | sanook "review"`) */
|
|
323
|
+
async function readStdin() {
|
|
324
|
+
const chunks = [];
|
|
325
|
+
for await (const c of process.stdin)
|
|
326
|
+
chunks.push(c);
|
|
327
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
328
|
+
}
|
|
329
|
+
/** sanook config [get <k> | set <k> <v>] — ดู/แก้ ~/.sanook/config.json โดยไม่ต้องแก้มือ */
|
|
330
|
+
async function runConfig(args) {
|
|
331
|
+
const { readGlobalConfigRaw, patchGlobalConfig } = await import('./config.js');
|
|
332
|
+
const [action, key, ...rest] = args;
|
|
333
|
+
const ALLOWED = ['model', 'budgetUsd', 'permissionMode', 'brainPath'];
|
|
334
|
+
if (action === 'set') {
|
|
335
|
+
if (!key || rest.length === 0) {
|
|
336
|
+
console.error(`ใช้: sanook config set <key> <value> (key: ${ALLOWED.join(' | ')})`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
if (!ALLOWED.includes(key)) {
|
|
340
|
+
console.error(`ตั้งได้เฉพาะ: ${ALLOWED.join(', ')}`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const raw = rest.join(' ');
|
|
344
|
+
await patchGlobalConfig({ [key]: key === 'budgetUsd' ? Number(raw) : raw });
|
|
345
|
+
console.log(`ตั้ง ${key} = ${raw}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (action === 'get') {
|
|
349
|
+
const cfg = await readGlobalConfigRaw();
|
|
350
|
+
console.log(cfg[key] ?? '(ไม่ได้ตั้ง)');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
console.log(`~/.sanook/config.json:\n${JSON.stringify(await readGlobalConfigRaw(), null, 2)}`);
|
|
354
|
+
}
|
|
355
|
+
/** sanook mcp [list | add <name> <command> [args...] | remove <name>] — จัดการ ~/.sanook/mcp.json */
|
|
356
|
+
async function runMcp(args) {
|
|
357
|
+
const mcpPath = join(homedir(), '.sanook', 'mcp.json');
|
|
358
|
+
let cfg = { mcpServers: {} };
|
|
359
|
+
try {
|
|
360
|
+
const parsed = JSON.parse(await readFile(mcpPath, 'utf8'));
|
|
361
|
+
cfg = { mcpServers: parsed.mcpServers ?? {} };
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
/* ยังไม่มีไฟล์ */
|
|
365
|
+
}
|
|
366
|
+
const write = async () => {
|
|
367
|
+
await mkdir(dirname(mcpPath), { recursive: true });
|
|
368
|
+
await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
369
|
+
};
|
|
370
|
+
const [action, name, command, ...cmdArgs] = args;
|
|
371
|
+
if (action === 'add') {
|
|
372
|
+
if (!name || !command) {
|
|
373
|
+
console.error('ใช้: sanook mcp add <name> <command> [args...] (เช่น: mcp add fs npx -y @modelcontextprotocol/server-filesystem /path)');
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
cfg.mcpServers[name] = { command, args: cmdArgs };
|
|
377
|
+
await write();
|
|
378
|
+
console.log(`เพิ่ม MCP server "${name}"`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (action === 'remove' || action === 'rm') {
|
|
382
|
+
if (name && cfg.mcpServers[name]) {
|
|
383
|
+
delete cfg.mcpServers[name];
|
|
384
|
+
await write();
|
|
385
|
+
console.log(`ลบ MCP server "${name}" แล้ว`);
|
|
386
|
+
}
|
|
387
|
+
else
|
|
388
|
+
console.log(`ไม่เจอ MCP server "${name ?? ''}"`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const names = Object.keys(cfg.mcpServers);
|
|
392
|
+
if (!names.length) {
|
|
393
|
+
console.log('ยังไม่มี MCP server — เพิ่ม: sanook mcp add <name> <command> [args...]');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
console.log(`${names.length} MCP servers:`);
|
|
397
|
+
for (const n of names)
|
|
398
|
+
console.log(` ${n} — ${cfg.mcpServers[n].command} ${(cfg.mcpServers[n].args ?? []).join(' ')}`);
|
|
399
|
+
}
|
|
400
|
+
async function main() {
|
|
401
|
+
const argv = process.argv.slice(2);
|
|
402
|
+
if (argv.includes('-v') || argv.includes('--version')) {
|
|
403
|
+
console.log(VERSION);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
407
|
+
console.log(HELP);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// โหลด API key จาก ~/.sanook/auth.json เข้า env (ไม่ override env ที่ตั้งไว้แล้ว)
|
|
411
|
+
await loadKeysIntoEnv();
|
|
412
|
+
process.on('exit', closeMcp); // ปิด MCP server (kill child) ตอนจบ
|
|
413
|
+
// subcommands: serve · cron — match เฉพาะรูปแบบที่ถูกต้อง กัน prompt unquoted ("serve coffee") misfire
|
|
414
|
+
if (argv[0] === 'serve' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
415
|
+
return runServe(argv.slice(1));
|
|
416
|
+
if (argv[0] === 'cron' && ['add', 'list', 'rm', 'remove', undefined].includes(argv[1])) {
|
|
417
|
+
return runCron(argv.slice(1));
|
|
418
|
+
}
|
|
419
|
+
if (argv[0] === 'skill' && ['list', 'add', 'remove', 'rm', undefined].includes(argv[1])) {
|
|
420
|
+
return runSkill(argv.slice(1));
|
|
421
|
+
}
|
|
422
|
+
if (argv[0] === 'models')
|
|
423
|
+
return runModels(argv.slice(1));
|
|
424
|
+
if (argv[0] === 'brain' && ['init', undefined].includes(argv[1]))
|
|
425
|
+
return runBrain(argv.slice(1));
|
|
426
|
+
if (argv[0] === 'config' && ['get', 'set', 'list', undefined].includes(argv[1]))
|
|
427
|
+
return runConfig(argv.slice(1));
|
|
428
|
+
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', undefined].includes(argv[1]))
|
|
429
|
+
return runMcp(argv.slice(1));
|
|
430
|
+
const { model, budget, json, prompt: argPrompt, planMode, yes } = parseArgs(argv);
|
|
431
|
+
const budgetUsd = Number.isFinite(budget) ? budget : undefined;
|
|
432
|
+
// stdin piping: `git diff | sanook "review this"` → ผนวก stdin เข้า prompt (headless/CI)
|
|
433
|
+
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
434
|
+
const prompt = piped ? `${argPrompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : argPrompt;
|
|
435
|
+
if (prompt) {
|
|
436
|
+
const config = await loadConfig({ model, budgetUsd });
|
|
437
|
+
// --continue / -c → โหลด session ล่าสุดมาต่อ (จำว่าทำถึงไหน)
|
|
438
|
+
const history = argv.includes('--continue') || argv.includes('-c') ? (await latestSession())?.messages : undefined;
|
|
439
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// interactive — ครั้งแรก (ยังไม่มี config) → setup wizard ก่อนเข้า REPL
|
|
443
|
+
if (await isFirstRun()) {
|
|
444
|
+
const { startSetup } = await import('./ui/render.js');
|
|
445
|
+
await startSetup();
|
|
446
|
+
}
|
|
447
|
+
const config = await loadConfig({ model, budgetUsd });
|
|
448
|
+
// --continue / -c → โหลด conversation ล่าสุดเข้า REPL (เดิม resume ได้แค่ headless)
|
|
449
|
+
const initialHistory = argv.includes('--continue') || argv.includes('-c') ? (await latestSession())?.messages : undefined;
|
|
450
|
+
const { startRepl } = await import('./ui/render.js');
|
|
451
|
+
startRepl({
|
|
452
|
+
initialModel: config.model,
|
|
453
|
+
budgetUsd: config.budgetUsd,
|
|
454
|
+
permissionMode: yes ? 'auto' : config.permissionMode,
|
|
455
|
+
initialHistory,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
main().catch((err) => {
|
|
459
|
+
console.error(redactKey(err.message ?? String(err)));
|
|
460
|
+
process.exit(1);
|
|
461
|
+
});
|
package/dist/brain.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/** ขยาย ~ ขึ้นต้น path เป็น home dir */
|
|
6
|
+
export function expandHome(p) {
|
|
7
|
+
return p === '~' || p.startsWith('~/') ? join(homedir(), p.slice(1)) : p;
|
|
8
|
+
}
|
|
9
|
+
// bundled rich templates (Home/USER/constitution/core memory/Templates) — sibling ของ skills/ ใน package
|
|
10
|
+
// (ship ผ่าน package.json "files", ไม่ผ่าน tsc — เหมือน BUNDLED_SKILLS ใน skills.ts)
|
|
11
|
+
const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'second-brain');
|
|
12
|
+
export const BRAIN_DEFAULTS = {
|
|
13
|
+
ownerName: 'Owner',
|
|
14
|
+
aiName: 'ผู้ช่วย',
|
|
15
|
+
aiPronoun: 'เรา',
|
|
16
|
+
language: 'ไทย + tech-en',
|
|
17
|
+
tone: 'กระชับ ตรงประเด็น สุภาพ',
|
|
18
|
+
vaultName: 'Second Brain',
|
|
19
|
+
autonomy: 'ask-on-risk',
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* โฟลเดอร์ทั้งหมด + บทบาท (จาก GEMINI.md §B.0 Folder Role Table) → generate _Index.md ให้ทุกอัน
|
|
23
|
+
* top-level parent = Home · Shared/<x> parent = Shared/_Index (Shared เองชี้ Home)
|
|
24
|
+
*/
|
|
25
|
+
// ⚠ sync กับ second-brain/Vault Structure Map.md — แก้ role/โฟลเดอร์ ต้องแก้ทั้งสองที่ (มี test กัน drift)
|
|
26
|
+
export const FOLDERS = [
|
|
27
|
+
// Core (MVV)
|
|
28
|
+
{ dir: 'Projects', role: 'workspace ของงานจริง — 1 โฟลเดอร์ = 1 โปรเจค (overview/context/current-state)' },
|
|
29
|
+
{ dir: 'Sessions', role: 'flat chronological log ของงาน + checkpoint (YYYY-MM-DD-<topic>.md)' },
|
|
30
|
+
{ dir: 'Intake', role: 'จุดรับของใหม่เข้า vault (raw input + task framing) ก่อนกระจายเข้าปลายทาง' },
|
|
31
|
+
{ dir: 'Intake/_Quarantine', role: 'external content (web/paste) ที่ยัง untrusted — scan injection ก่อน promote (ดู Runbooks/ingest-quarantine)' },
|
|
32
|
+
{ dir: 'Intake/Raw Sources', role: 'ต้นฉบับ external ที่ผ่าน scan แล้ว — immutable read-only, source:: ชี้มาที่นี่ได้' },
|
|
33
|
+
{ dir: 'Skills', role: 'reusable unit ที่ executable + ผ่าน verification command — ไม่ใช่ prose (นั่นคือ Runbooks) ดู Shared/Rules/skills-admission' },
|
|
34
|
+
{ dir: 'Runbooks', role: 'prose how-to ที่อ่านแล้วทำตามเอง (setup/deploy/maintain) — ไม่ใช่ runnable unit (นั่นคือ Skills)' },
|
|
35
|
+
{ dir: 'Templates', role: 'แม่แบบโน้ต — instantiate จากที่นี่ตอนสร้างโน้ตใหม่' },
|
|
36
|
+
{ dir: 'Bugs', role: 'bug report reproducible ลงวันที่ ไม่ลบ — project bug ก็มาที่นี่ (global, flat) + link กลับ project' },
|
|
37
|
+
{ dir: 'Handoffs', role: 'เอกสารส่งมอบงานค้าง 1 ชิ้น (state + next steps) — snapshot ครั้งเดียว ไม่ใช่ live coordination' },
|
|
38
|
+
// Direction
|
|
39
|
+
{ dir: 'Goals', role: 'north-star + objective รายไตรมาส/ปี (finite, มีวันจบ) — ไม่เก็บ live status (นั่นคือ Operating-State)' },
|
|
40
|
+
{ dir: 'Areas', role: 'PARA — โดเมนงานต่อเนื่องที่ไม่มีวันจบ (brand/trading/content...)' },
|
|
41
|
+
// Knowledge pipeline
|
|
42
|
+
{ dir: 'Research', role: 'finding ที่อิงแหล่งภายนอก (มี source::) + market scan + reference synthesis' },
|
|
43
|
+
{ dir: 'Learning', role: 'knowledge ที่ตัวเองกลั่น/deep-dive ตาม topic (ไม่มี external source) — curated MOC' },
|
|
44
|
+
{ dir: 'Distillations', role: 'หลักการ evergreen ที่กลั่นนิ่งแล้ว (เห็น ≥3 ครั้ง) — atomic' },
|
|
45
|
+
{ dir: 'Retrospectives', role: 'reflection หลังงาน (event-triggered: what worked/failed)' },
|
|
46
|
+
{ dir: 'Reviews', role: 'review ตาม cadence (time-triggered: weekly/monthly) + vault health' },
|
|
47
|
+
{ dir: 'Traces', role: 'exploration/reasoning chain ยาว (คำถามใหญ่เกินโน้ตเดียว)' },
|
|
48
|
+
{ dir: 'Prompts', role: 'prompt text/template ที่หยิบมารันได้ทันที (input ให้ LLM)' },
|
|
49
|
+
{ dir: 'Acceptance', role: 'golden input→expected-output fixtures ที่ใช้ตัดสิน done/not-done — ไม่ใช่ checklist, ไม่ใช่ runner (นั่นคือ Evals)' },
|
|
50
|
+
{ dir: 'Checklists', role: 'preflight/postflight gate (ticklist ก่อน-หลังลงมือ) — ไม่เก็บ expected output' },
|
|
51
|
+
// Frontier loops
|
|
52
|
+
{ dir: 'Playbooks', role: 'กลยุทธ์/ลำดับการตัดสินใจที่ปรับดีขึ้นจากผลจริง (how-to-decide) — ไม่ใช่ prompt text, ไม่ใช่ runnable unit' },
|
|
53
|
+
{ dir: 'Evals', role: 'quality loop ที่รัน Acceptance/golden-set แล้ว error-analysis + self-eval (runner + ผล, ไม่เก็บ case เอง)' },
|
|
54
|
+
{ dir: 'Entities', role: 'canonical page ต่อ entity/person/org/concept (LLM-wiki, bi-temporal)' },
|
|
55
|
+
// Shared (สมองกลาง) — Shared/_Index เองชี้ Home
|
|
56
|
+
{ dir: 'Shared', role: 'สมองกลาง: memory + rules + coordination (เข้าผ่าน AI-Context-Index)' },
|
|
57
|
+
{ dir: 'Shared/Operating-State', role: 'live status/metrics ตอนนี้ (current-state + health/queue)' },
|
|
58
|
+
{ dir: 'Shared/User-Memory', role: 'สิ่งที่ AI เรียนรู้เกี่ยวกับเจ้าของระหว่างทำงาน — preference/response-example (mutable)' },
|
|
59
|
+
{ dir: 'Shared/Decision-Memory', role: 'การตัดสินใจที่ AI บันทึก locked (latest-wins + supersedes)' },
|
|
60
|
+
{ dir: 'Shared/Memory-Inbox', role: 'candidate durable memory ที่ยังไม่ชัด/ขัดกัน — รอ promote (เคลียร์ทุก weekly)' },
|
|
61
|
+
{ dir: 'Shared/Rules', role: 'กฎ operating always-on (memory/frontmatter/context-assembly/graph)' },
|
|
62
|
+
{ dir: 'Shared/Tech-Standards', role: 'มาตรฐานเทคนิค (MCP/stack/DoD/verification)' },
|
|
63
|
+
{ dir: 'Shared/Core-Facts', role: 'ground truth ที่เจ้าของเขียนเอง — read-only, AI ไม่ supersede/ไม่แก้' },
|
|
64
|
+
{ dir: 'Shared/Coordination', role: 'live coordination ของหลาย agent พร้อมกัน (NOW.md baton + task-board + registry) — ไม่ใช่เอกสารส่งมอบ (นั่นคือ Handoffs)' },
|
|
65
|
+
{ dir: 'Shared/Working-Memory', role: 'scratchpad ระหว่าง 1 task — ลบทิ้งได้หลังจบ ไม่มีวัน promote' },
|
|
66
|
+
{ dir: 'Shared/User-Persona', role: 'identity profile ที่เปลี่ยนน้อยมาก (บทบาท/ค่านิยม/ภาษา/timezone) — human-owned, read-only' },
|
|
67
|
+
{ dir: 'Shared/Provenance', role: 'lineage ledger — ทุก claim ชี้ source:: ได้ (ingest-log)' },
|
|
68
|
+
{ dir: 'Shared/Archive', role: 'cold storage — โน้ตที่ stale/retired ออกจาก retrieval (ไม่ลบ)' },
|
|
69
|
+
];
|
|
70
|
+
/** แทน {{KEY}} ด้วยค่าจริงจาก config */
|
|
71
|
+
export function substitute(text, cfg) {
|
|
72
|
+
const map = {
|
|
73
|
+
OWNER_NAME: cfg.ownerName,
|
|
74
|
+
OWNER_ADDRESS: cfg.ownerName,
|
|
75
|
+
AI_NAME: cfg.aiName,
|
|
76
|
+
AI_PRONOUN: cfg.aiPronoun,
|
|
77
|
+
LANGUAGE: cfg.language,
|
|
78
|
+
TONE: cfg.tone,
|
|
79
|
+
VAULT_NAME: cfg.vaultName,
|
|
80
|
+
AUTONOMY: cfg.autonomy,
|
|
81
|
+
DATE: cfg.today,
|
|
82
|
+
};
|
|
83
|
+
return text.replace(/\{\{(\w+)\}\}/g, (whole, key) => (key in map ? map[key] : whole));
|
|
84
|
+
}
|
|
85
|
+
/** generate _Index.md ของโฟลเดอร์ — frontmatter + role + up:: (ตาม §18 / §B.3 rule 2-3) */
|
|
86
|
+
function renderIndex(dir, role, cfg) {
|
|
87
|
+
const name = dir.split('/').pop() ?? dir;
|
|
88
|
+
// parent = _Index ของโฟลเดอร์แม่ (nested) หรือ Home (top-level)
|
|
89
|
+
const parent = dir.includes('/') ? `${dir.split('/').slice(0, -1).join('/')}/_Index` : 'Home';
|
|
90
|
+
const tag = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
91
|
+
return `---
|
|
92
|
+
tags: [index, moc, ${tag}]
|
|
93
|
+
note_type: moc
|
|
94
|
+
created: ${cfg.today}
|
|
95
|
+
updated: ${cfg.today}
|
|
96
|
+
parent: "[[${parent}]]"
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
# ${name}
|
|
100
|
+
|
|
101
|
+
> ${role}
|
|
102
|
+
|
|
103
|
+
_(ยังว่าง — โน้ตในโฟลเดอร์นี้จะถูกลิงก์ที่นี่)_
|
|
104
|
+
|
|
105
|
+
up:: [[${parent}]]
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
async function exists(p) {
|
|
109
|
+
try {
|
|
110
|
+
await stat(p);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** write ถ้ายังไม่มี (create-if-missing — §B.3 rule 1: ห้ามทับของเดิม) */
|
|
118
|
+
async function writeIfMissing(path, content, created, skipped) {
|
|
119
|
+
await mkdir(dirname(path), { recursive: true });
|
|
120
|
+
if (await exists(path)) {
|
|
121
|
+
skipped.push(path);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await writeFile(path, content, 'utf8');
|
|
125
|
+
created.push(path);
|
|
126
|
+
}
|
|
127
|
+
/** เดิน bundled template dir → คืน rel path ของไฟล์ทั้งหมด (ไม่มี dir → คืน []) */
|
|
128
|
+
async function walk(dir, base = dir) {
|
|
129
|
+
const out = [];
|
|
130
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
131
|
+
for (const e of entries) {
|
|
132
|
+
const full = join(dir, e.name);
|
|
133
|
+
if (e.isDirectory())
|
|
134
|
+
out.push(...(await walk(full, base)));
|
|
135
|
+
else
|
|
136
|
+
out.push(full.slice(base.length + 1));
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* สร้างโครง second-brain ที่ targetPath — create-if-missing เสมอ
|
|
142
|
+
* 1) ทุกโฟลเดอร์ใน FOLDERS + _Index.md (generated)
|
|
143
|
+
* 2) rich seed files จาก bundled second-brain/ (Home/USER/constitution/core memory/Templates) + substitute
|
|
144
|
+
* 3) .obsidian/app.json ว่าง (ให้ Obsidian จำเป็น vault)
|
|
145
|
+
*/
|
|
146
|
+
export async function scaffoldBrain(targetPath, cfg) {
|
|
147
|
+
const created = [];
|
|
148
|
+
const skipped = [];
|
|
149
|
+
// 1) folders + generated _Index
|
|
150
|
+
for (const { dir, role } of FOLDERS) {
|
|
151
|
+
await mkdir(join(targetPath, dir), { recursive: true });
|
|
152
|
+
await writeIfMissing(join(targetPath, dir, '_Index.md'), renderIndex(dir, role, cfg), created, skipped);
|
|
153
|
+
}
|
|
154
|
+
// 2) rich seed files (substitute placeholders)
|
|
155
|
+
for (const rel of await walk(TEMPLATE_DIR)) {
|
|
156
|
+
const raw = await readFile(join(TEMPLATE_DIR, rel), 'utf8');
|
|
157
|
+
await writeIfMissing(join(targetPath, rel), substitute(raw, cfg), created, skipped);
|
|
158
|
+
}
|
|
159
|
+
// 3) make Obsidian recognise it as a vault
|
|
160
|
+
await writeIfMissing(join(targetPath, '.obsidian', 'app.json'), '{}\n', created, skipped);
|
|
161
|
+
return { created, skipped };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* wire filesystem MCP server ชี้ไป vault ใน ~/.sanook/mcp.json (merge — ไม่ทับ server เดิม)
|
|
165
|
+
* → agent อ่าน/เขียน vault ที่เพิ่ง scaffold ได้ทันที (ไม่ต้อง hand-author mcp.json)
|
|
166
|
+
*/
|
|
167
|
+
export async function wireBrainMcp(vaultPath) {
|
|
168
|
+
const mcpPath = join(homedir(), '.sanook', 'mcp.json');
|
|
169
|
+
let cfg = {};
|
|
170
|
+
try {
|
|
171
|
+
cfg = JSON.parse(await readFile(mcpPath, 'utf8'));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* ยังไม่มีไฟล์ */
|
|
175
|
+
}
|
|
176
|
+
cfg.mcpServers ??= {};
|
|
177
|
+
if (cfg.mcpServers['second-brain'])
|
|
178
|
+
return 'exists';
|
|
179
|
+
cfg.mcpServers['second-brain'] = {
|
|
180
|
+
command: 'npx',
|
|
181
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', vaultPath],
|
|
182
|
+
};
|
|
183
|
+
await mkdir(dirname(mcpPath), { recursive: true });
|
|
184
|
+
await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
185
|
+
return 'added';
|
|
186
|
+
}
|