sanook-cli 0.5.5 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +392 -42
- package/README.th.md +15 -8
- package/dist/auto-maintain.js +113 -0
- package/dist/bin.js +77 -15
- package/dist/brain-final.js +8 -4
- package/dist/brain-link.js +73 -0
- package/dist/brain-new.js +9 -5
- package/dist/brain-repair.js +7 -4
- package/dist/brand.js +21 -0
- package/dist/commands.js +7 -1
- package/dist/config.js +40 -29
- package/dist/cost.js +20 -0
- package/dist/dashboard/api-helpers.js +112 -3
- package/dist/dashboard/server.js +85 -1
- package/dist/dashboard/static/app.js +381 -0
- package/dist/dashboard/static/styles.css +36 -0
- package/dist/dashboard/terminal.js +214 -0
- package/dist/diff.js +22 -8
- package/dist/gateway/session.js +4 -0
- package/dist/install-info.js +91 -0
- package/dist/loop.js +31 -4
- package/dist/memory.js +236 -16
- package/dist/persona.js +300 -0
- package/dist/project-scaffold.js +4 -2
- package/dist/providers/registry.js +11 -1
- package/dist/self-improve-synth.js +86 -0
- package/dist/self-improve.js +203 -0
- package/dist/session-brain.js +112 -0
- package/dist/slash-completion.js +1 -0
- package/dist/ui/app.js +154 -30
- package/dist/ui/input-view.js +104 -0
- package/dist/ui/persona-wizard.js +89 -0
- package/dist/ui/render.js +87 -5
- package/dist/ui/tool-activity.js +118 -0
- package/dist/ui/tool-trail.js +15 -3
- package/dist/usage-cli.js +160 -0
- package/dist/usage-ledger.js +169 -0
- package/package.json +11 -2
- package/scripts/postinstall.mjs +4 -4
package/dist/config.js
CHANGED
|
@@ -4,9 +4,12 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { appHomePath, appProjectPath, BRAND } from './brand.js';
|
|
5
5
|
import { projectRoot, projectTrustStatus } from './trust.js';
|
|
6
6
|
import { registerPricing } from './cost.js';
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export function configHomeDir() {
|
|
8
|
+
return appHomePath();
|
|
9
|
+
}
|
|
10
|
+
function authPath() {
|
|
11
|
+
return join(configHomeDir(), 'auth.json');
|
|
12
|
+
}
|
|
10
13
|
const AUTH_ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
14
|
const RESERVED_AUTH_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
12
15
|
const PricingKeySchema = z.string().regex(/^[^:\s]+:\S+$/, 'key ต้องเป็น provider:model');
|
|
@@ -29,6 +32,11 @@ export const ConfigSchema = z.object({
|
|
|
29
32
|
permissionMode: z.enum(['auto', 'ask']).default('ask'),
|
|
30
33
|
// path ของ second-brain workspace ที่ scaffold ไว้ (sanook brain) — optional
|
|
31
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),
|
|
32
40
|
// pricing override/extension per "provider:model" → ทำให้ budget cap ใช้ได้กับ model ที่ยังไม่มีในตาราง
|
|
33
41
|
pricing: PricingOverrideSchema.optional(),
|
|
34
42
|
// ── token/cost tuning (ดู agentTuning) — .catch กันค่า config.json ผิดทำ boot พัง (agentTuning อ่าน raw + coerce เองด้วย) ──
|
|
@@ -102,6 +110,9 @@ export async function agentTuning() {
|
|
|
102
110
|
return { cacheTtl, thinkingBudget, compaction, contextCompression, summaryModel };
|
|
103
111
|
}
|
|
104
112
|
const warnedBadConfigKeys = new Set();
|
|
113
|
+
function globalConfigPath() {
|
|
114
|
+
return join(configHomeDir(), 'config.json');
|
|
115
|
+
}
|
|
105
116
|
/**
|
|
106
117
|
* Validate the merged config, but degrade gracefully: a malformed strict field (bad model/maxSteps/
|
|
107
118
|
* permissionMode/budgetUsd/pricing in a hand-edited config.json) is dropped to its default with a
|
|
@@ -153,7 +164,7 @@ function sanitizeUntrustedProjectConfig(cfg) {
|
|
|
153
164
|
* (config flat — shallow merge พอ; strip undefined ใน overrides กัน override ทับ default)
|
|
154
165
|
*/
|
|
155
166
|
export async function loadConfig(overrides = {}, cwd = process.cwd()) {
|
|
156
|
-
const global = await readJson(
|
|
167
|
+
const global = await readJson(globalConfigPath());
|
|
157
168
|
const root = await projectRoot(cwd);
|
|
158
169
|
const projectRaw = await readJson(appProjectPath(root, 'config.json'));
|
|
159
170
|
const trust = await projectTrustStatus(root);
|
|
@@ -207,7 +218,7 @@ export function parsePricingOverride(raw) {
|
|
|
207
218
|
/** ครั้งแรกที่รัน (ยังไม่มี global config) → ต้องทำ setup wizard */
|
|
208
219
|
export async function isFirstRun() {
|
|
209
220
|
try {
|
|
210
|
-
await readFile(
|
|
221
|
+
await readFile(globalConfigPath(), 'utf8');
|
|
211
222
|
return false;
|
|
212
223
|
}
|
|
213
224
|
catch {
|
|
@@ -216,32 +227,32 @@ export async function isFirstRun() {
|
|
|
216
227
|
}
|
|
217
228
|
/** บันทึก global config (model/provider/locale ที่เลือกตอน setup) */
|
|
218
229
|
export async function saveGlobalConfig(cfg) {
|
|
219
|
-
await mkdir(
|
|
220
|
-
const existing = await readJson(
|
|
221
|
-
await writeFile(
|
|
222
|
-
await chmod(
|
|
230
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
231
|
+
const existing = await readJson(globalConfigPath());
|
|
232
|
+
await writeFile(globalConfigPath(), `${JSON.stringify({ ...existing, ...cfg }, null, 2)}\n`, { mode: 0o600 });
|
|
233
|
+
await chmod(globalConfigPath(), 0o600).catch(() => { });
|
|
223
234
|
}
|
|
224
235
|
/** บันทึก path ของ second-brain workspace ลง global config (merge — ไม่ทับ field อื่น) */
|
|
225
236
|
export async function saveBrainPath(path) {
|
|
226
|
-
await mkdir(
|
|
227
|
-
const existing = await readJson(
|
|
228
|
-
await writeFile(
|
|
229
|
-
await chmod(
|
|
237
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
238
|
+
const existing = await readJson(globalConfigPath());
|
|
239
|
+
await writeFile(globalConfigPath(), `${JSON.stringify({ ...existing, brainPath: path }, null, 2)}\n`, { mode: 0o600 });
|
|
240
|
+
await chmod(globalConfigPath(), 0o600).catch(() => { });
|
|
230
241
|
}
|
|
231
242
|
/** อ่าน config.json ดิบ (ไม่ apply default/schema) — สำหรับ `sanook config` */
|
|
232
243
|
export async function readGlobalConfigRaw() {
|
|
233
|
-
return readJson(
|
|
244
|
+
return readJson(globalConfigPath());
|
|
234
245
|
}
|
|
235
246
|
/** path ของ auth.json (ใช้โชว์ใน CLI เท่านั้น; ห้าม print raw secret) */
|
|
236
247
|
export function authConfigPath() {
|
|
237
|
-
return
|
|
248
|
+
return authPath();
|
|
238
249
|
}
|
|
239
250
|
function isSafeAuthEnvVarName(name) {
|
|
240
251
|
return AUTH_ENV_VAR_RE.test(name) && !RESERVED_AUTH_KEYS.has(name);
|
|
241
252
|
}
|
|
242
253
|
/** อ่าน auth.json ดิบแบบกรองเฉพาะ string values — caller ต้อง redact ก่อนโชว์ */
|
|
243
254
|
export async function readStoredAuthRaw() {
|
|
244
|
-
const raw = await readJson(
|
|
255
|
+
const raw = await readJson(authPath());
|
|
245
256
|
const auth = {};
|
|
246
257
|
for (const [k, v] of Object.entries(raw)) {
|
|
247
258
|
if (isSafeAuthEnvVarName(k) && typeof v === 'string')
|
|
@@ -251,44 +262,44 @@ export async function readStoredAuthRaw() {
|
|
|
251
262
|
}
|
|
252
263
|
/** merge patch ลง config.json (สำหรับ `sanook config set`) */
|
|
253
264
|
export async function patchGlobalConfig(patch) {
|
|
254
|
-
await mkdir(
|
|
255
|
-
const existing = await readJson(
|
|
256
|
-
await writeFile(
|
|
257
|
-
await chmod(
|
|
265
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
266
|
+
const existing = await readJson(globalConfigPath());
|
|
267
|
+
await writeFile(globalConfigPath(), `${JSON.stringify({ ...existing, ...patch }, null, 2)}\n`, { mode: 0o600 });
|
|
268
|
+
await chmod(globalConfigPath(), 0o600).catch(() => { });
|
|
258
269
|
}
|
|
259
270
|
/** บันทึก API key ลง ~/.sanook/auth.json (chmod 0600) + set env ทันทีสำหรับ session นี้ */
|
|
260
271
|
export async function saveKey(envVar, key) {
|
|
261
272
|
if (!isSafeAuthEnvVarName(envVar))
|
|
262
273
|
throw new Error(`env var ไม่ถูกต้อง: ${envVar}`);
|
|
263
|
-
await mkdir(
|
|
274
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
264
275
|
const auth = await readStoredAuthRaw();
|
|
265
276
|
auth[envVar] = key;
|
|
266
|
-
await writeFile(
|
|
267
|
-
await chmod(
|
|
277
|
+
await writeFile(authPath(), `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
278
|
+
await chmod(authPath(), 0o600); // เจ้าของอ่าน/เขียนเท่านั้น
|
|
268
279
|
process.env[envVar] = key;
|
|
269
280
|
}
|
|
270
281
|
/** ลบ key ที่ Sanook เก็บไว้ใน auth.json (ไม่แตะ env จริงของ shell ภายนอก) */
|
|
271
282
|
export async function removeStoredKey(envVar) {
|
|
272
283
|
if (!isSafeAuthEnvVarName(envVar))
|
|
273
284
|
return false;
|
|
274
|
-
await mkdir(
|
|
285
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
275
286
|
const auth = await readStoredAuthRaw();
|
|
276
287
|
if (!Object.prototype.hasOwnProperty.call(auth, envVar))
|
|
277
288
|
return false;
|
|
278
289
|
delete auth[envVar];
|
|
279
|
-
await writeFile(
|
|
280
|
-
await chmod(
|
|
290
|
+
await writeFile(authPath(), `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
291
|
+
await chmod(authPath(), 0o600).catch(() => { });
|
|
281
292
|
delete process.env[envVar];
|
|
282
293
|
return true;
|
|
283
294
|
}
|
|
284
295
|
/** ล้าง auth.json ที่ Sanook เก็บไว้ทั้งหมด */
|
|
285
296
|
export async function clearStoredAuth() {
|
|
286
|
-
await mkdir(
|
|
297
|
+
await mkdir(configHomeDir(), { recursive: true });
|
|
287
298
|
const auth = await readStoredAuthRaw();
|
|
288
299
|
for (const envVar of Object.keys(auth))
|
|
289
300
|
delete process.env[envVar];
|
|
290
|
-
await writeFile(
|
|
291
|
-
await chmod(
|
|
301
|
+
await writeFile(authPath(), '{}\n', { mode: 0o600 });
|
|
302
|
+
await chmod(authPath(), 0o600).catch(() => { });
|
|
292
303
|
}
|
|
293
304
|
/** โหลด key จาก auth.json เข้า env ตอน boot (ไม่ override env ที่ตั้งไว้แล้ว) */
|
|
294
305
|
export async function loadKeysIntoEnv() {
|
package/dist/cost.js
CHANGED
|
@@ -12,6 +12,14 @@ export const PRICING = {
|
|
|
12
12
|
'openai:gpt-5.5': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
13
13
|
'openai:gpt-5.4-mini': { input: 0.25, output: 2, cacheWrite: 0.25, cacheRead: 0.025 },
|
|
14
14
|
'openai:gpt-5.3-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
15
|
+
// OpenAI Codex delegate (ChatGPT plan — token counts are real; cost is estimated from API list price)
|
|
16
|
+
'codex:gpt-5.5': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
17
|
+
'codex:gpt-5.4': { input: 2.5, output: 15, cacheWrite: 2.5, cacheRead: 0.25 },
|
|
18
|
+
'codex:gpt-5.4-mini': { input: 0.25, output: 2, cacheWrite: 0.25, cacheRead: 0.025 },
|
|
19
|
+
'codex:gpt-5.3-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
20
|
+
'codex:gpt-5.2-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
21
|
+
'codex:gpt-5-codex': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.125 },
|
|
22
|
+
'codex:gpt-5.3-codex-spark': { input: 0.25, output: 2, cacheWrite: 0.25, cacheRead: 0.025 },
|
|
15
23
|
// Google Gemini (≤200k context tier)
|
|
16
24
|
'google:gemini-2.5-pro': { input: 1.25, output: 10, cacheWrite: 1.25, cacheRead: 0.31 },
|
|
17
25
|
'google:gemini-2.5-flash': { input: 0.3, output: 2.5, cacheWrite: 0.3, cacheRead: 0.075 },
|
|
@@ -147,4 +155,16 @@ export class CostMeter {
|
|
|
147
155
|
const budget = this.budgetUsd != null ? ` / budget $${this.budgetUsd}` : '';
|
|
148
156
|
return `tokens: ${total} (in ${this.inTok} · out ${this.outTok} · cache-read ${this.cacheReadTok} · cache-write ${this.cacheWriteTok}) · cost ${cost}${budget}`;
|
|
149
157
|
}
|
|
158
|
+
snapshot() {
|
|
159
|
+
return {
|
|
160
|
+
specKey: this.specKey,
|
|
161
|
+
inputTokens: this.inTok,
|
|
162
|
+
outputTokens: this.outTok,
|
|
163
|
+
cacheReadTokens: this.cacheReadTok,
|
|
164
|
+
cacheWriteTokens: this.cacheWriteTok,
|
|
165
|
+
totalTokens: this.inTok + this.outTok + this.cacheReadTok + this.cacheWriteTok,
|
|
166
|
+
costUsd: this.spent,
|
|
167
|
+
hasPricing: this.hasPricing,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
150
170
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve, relative } from 'node:path';
|
|
2
|
+
import { join, resolve, relative, isAbsolute } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { appHomePath, BRAND } from '../brand.js';
|
|
5
5
|
import { loadConfig } from '../config.js';
|
|
@@ -54,12 +54,21 @@ export async function dashboardLogsTail(maxLines = 200) {
|
|
|
54
54
|
function safeRoot(root) {
|
|
55
55
|
return resolve(root);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* True only if `target` is the root itself or strictly inside it. Uses path.relative (not startsWith)
|
|
59
|
+
* so a sibling dir sharing the root's name-prefix (e.g. .sanook-secrets vs .sanook) and absolute-path
|
|
60
|
+
* escapes are both rejected — prevents directory traversal in the dashboard file API.
|
|
61
|
+
*/
|
|
62
|
+
function isWithin(target, root) {
|
|
63
|
+
const rel = relative(safeRoot(root), target);
|
|
64
|
+
return !rel.startsWith('..') && !isAbsolute(rel);
|
|
65
|
+
}
|
|
57
66
|
export async function dashboardListFiles(subpath = '') {
|
|
58
67
|
const config = await loadConfig({});
|
|
59
68
|
const roots = [appHomePath(), config.brainPath ? resolve(config.brainPath) : null].filter(Boolean);
|
|
60
69
|
const root = safeRoot(roots[0] ?? appHomePath());
|
|
61
70
|
const target = safeRoot(join(root, subpath.replace(/^\/+/, '')));
|
|
62
|
-
if (!
|
|
71
|
+
if (!roots.some((r) => isWithin(target, r))) {
|
|
63
72
|
throw new Error('path not allowed');
|
|
64
73
|
}
|
|
65
74
|
const entries = await readdir(target, { withFileTypes: true });
|
|
@@ -75,7 +84,7 @@ export async function dashboardReadFile(subpath) {
|
|
|
75
84
|
const config = await loadConfig({});
|
|
76
85
|
const allowedRoots = [appHomePath(), config.brainPath ? resolve(config.brainPath) : null].filter(Boolean);
|
|
77
86
|
const target = safeRoot(subpath.startsWith('/') ? subpath : join(appHomePath(), subpath));
|
|
78
|
-
if (!allowedRoots.some((root) => target
|
|
87
|
+
if (!allowedRoots.some((root) => isWithin(target, root)))
|
|
79
88
|
throw new Error('path not allowed');
|
|
80
89
|
const info = await stat(target);
|
|
81
90
|
if (!info.isFile())
|
|
@@ -85,3 +94,103 @@ export async function dashboardReadFile(subpath) {
|
|
|
85
94
|
const content = await readFile(target, 'utf8');
|
|
86
95
|
return { path: relative(homedir(), target) || target, content };
|
|
87
96
|
}
|
|
97
|
+
export async function dashboardSkills() {
|
|
98
|
+
const { loadSkills } = await import('../skills.js');
|
|
99
|
+
const { loadLedger } = await import('../self-improve.js');
|
|
100
|
+
const [skills, ledger] = await Promise.all([loadSkills(), loadLedger().catch(() => ({ families: [] }))]);
|
|
101
|
+
const autoNames = new Set((ledger.families ?? []).map((f) => f.skillName).filter((n) => Boolean(n)));
|
|
102
|
+
return {
|
|
103
|
+
skills: skills.map((s) => ({
|
|
104
|
+
name: s.name,
|
|
105
|
+
description: s.description,
|
|
106
|
+
whenToUse: s.whenToUse ?? null,
|
|
107
|
+
auto: autoNames.has(s.name),
|
|
108
|
+
})),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export async function dashboardMemory() {
|
|
112
|
+
const { loadStore, activeFacts } = await import('../memory-store.js');
|
|
113
|
+
const config = await loadConfig({});
|
|
114
|
+
const store = await loadStore();
|
|
115
|
+
const facts = activeFacts(store)
|
|
116
|
+
.slice()
|
|
117
|
+
.sort((a, b) => b.lastAccessed - a.lastAccessed)
|
|
118
|
+
.map((f) => ({
|
|
119
|
+
id: f.id,
|
|
120
|
+
text: f.text,
|
|
121
|
+
noteType: f.noteType,
|
|
122
|
+
trust: f.trust,
|
|
123
|
+
tier: f.tier,
|
|
124
|
+
importance: Math.round(f.importance * 100) / 100,
|
|
125
|
+
created: f.created,
|
|
126
|
+
lastAccessed: f.lastAccessed,
|
|
127
|
+
accessCount: f.accessCount,
|
|
128
|
+
}));
|
|
129
|
+
return { facts, brainPath: config.brainPath ?? null };
|
|
130
|
+
}
|
|
131
|
+
// ---- Usage / cost ledger ---------------------------------------------------
|
|
132
|
+
export async function dashboardUsage() {
|
|
133
|
+
const { loadUsageEvents, aggregateUsageEvents } = await import('../usage-ledger.js');
|
|
134
|
+
const events = await loadUsageEvents();
|
|
135
|
+
const daily = aggregateUsageEvents(events, 'daily').slice(-30);
|
|
136
|
+
const totals = events.reduce((acc, e) => {
|
|
137
|
+
acc.turns += 1;
|
|
138
|
+
acc.inputTokens += e.inputTokens;
|
|
139
|
+
acc.outputTokens += e.outputTokens;
|
|
140
|
+
acc.totalTokens += e.inputTokens + e.outputTokens + e.cacheReadTokens + e.cacheWriteTokens;
|
|
141
|
+
acc.costUsd += e.costUsd ?? 0;
|
|
142
|
+
return acc;
|
|
143
|
+
}, { turns: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 });
|
|
144
|
+
return { totals, daily };
|
|
145
|
+
}
|
|
146
|
+
// ---- Install commands (multi-platform) -------------------------------------
|
|
147
|
+
export {} from '../install-info.js';
|
|
148
|
+
import { dashboardInstallPayload } from '../install-info.js';
|
|
149
|
+
export function dashboardInstall() {
|
|
150
|
+
return dashboardInstallPayload();
|
|
151
|
+
}
|
|
152
|
+
export async function dashboardPersona() {
|
|
153
|
+
const { loadPersonaAnswers } = await import('../memory.js');
|
|
154
|
+
const { PERSONA_QUESTIONS } = await import('../persona.js');
|
|
155
|
+
const { BRAND } = await import('../brand.js');
|
|
156
|
+
const config = await loadConfig({});
|
|
157
|
+
const brainPath = config.brainPath ?? null;
|
|
158
|
+
const answers = await loadPersonaAnswers();
|
|
159
|
+
const rows = PERSONA_QUESTIONS.map((q) => {
|
|
160
|
+
const v = (answers[q.id] ?? '').trim();
|
|
161
|
+
return {
|
|
162
|
+
id: q.id,
|
|
163
|
+
label: q.label,
|
|
164
|
+
value: v,
|
|
165
|
+
display: v ? (q.type === 'select' ? (q.options?.find((o) => o.value === v)?.label ?? v) : v) : '—',
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
const hasProfile = rows.some((r) => r.value);
|
|
169
|
+
const profilePath = brainPath ? `${brainPath}/Shared/User-Persona/persona.md` : null;
|
|
170
|
+
return {
|
|
171
|
+
brainPath,
|
|
172
|
+
profilePath,
|
|
173
|
+
rows,
|
|
174
|
+
hasProfile,
|
|
175
|
+
cliCommand: `${BRAND.cliName} persona`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export async function dashboardSelfImprove() {
|
|
179
|
+
const { loadLedger } = await import('../self-improve.js');
|
|
180
|
+
const { selfImproveEnabled, selfImproveThreshold } = await import('../brand.js');
|
|
181
|
+
const ledger = await loadLedger();
|
|
182
|
+
const families = (ledger.families ?? [])
|
|
183
|
+
.slice()
|
|
184
|
+
.sort((a, b) => b.lastSeen - a.lastSeen)
|
|
185
|
+
.map((f) => ({
|
|
186
|
+
sig: f.sig,
|
|
187
|
+
terms: f.terms,
|
|
188
|
+
sample: f.samples[f.samples.length - 1] ?? '',
|
|
189
|
+
count: f.count,
|
|
190
|
+
skillCreated: f.skillCreated,
|
|
191
|
+
skillName: f.skillName,
|
|
192
|
+
firstSeen: f.firstSeen,
|
|
193
|
+
lastSeen: f.lastSeen,
|
|
194
|
+
}));
|
|
195
|
+
return { enabled: selfImproveEnabled(), threshold: selfImproveThreshold(), families };
|
|
196
|
+
}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -29,6 +29,17 @@ function json(res, status, body) {
|
|
|
29
29
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
30
30
|
res.end(`${JSON.stringify(body)}\n`);
|
|
31
31
|
}
|
|
32
|
+
async function packageVersion() {
|
|
33
|
+
if (process.env.npm_package_version)
|
|
34
|
+
return process.env.npm_package_version;
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
37
|
+
return typeof pkg.version === 'string' && pkg.version ? pkg.version : 'dev';
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'dev';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
32
43
|
async function handleApi(req, res, pathname) {
|
|
33
44
|
if (req.method === 'GET' && pathname === '/api/status') {
|
|
34
45
|
const config = await loadConfig({});
|
|
@@ -36,7 +47,7 @@ async function handleApi(req, res, pathname) {
|
|
|
36
47
|
json(res, 200, {
|
|
37
48
|
product: 'Sanook Dashboard',
|
|
38
49
|
cli: BRAND.cliName,
|
|
39
|
-
version:
|
|
50
|
+
version: await packageVersion(),
|
|
40
51
|
model: config.model,
|
|
41
52
|
locale: config.locale,
|
|
42
53
|
brainPath: config.brainPath ?? null,
|
|
@@ -91,6 +102,53 @@ async function handleApi(req, res, pathname) {
|
|
|
91
102
|
json(res, 200, await dashboardListFiles(sub));
|
|
92
103
|
return true;
|
|
93
104
|
}
|
|
105
|
+
if (req.method === 'GET' && pathname === '/api/skills') {
|
|
106
|
+
const { dashboardSkills } = await import('./api-helpers.js');
|
|
107
|
+
json(res, 200, await dashboardSkills());
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (req.method === 'GET' && pathname === '/api/memory') {
|
|
111
|
+
const { dashboardMemory } = await import('./api-helpers.js');
|
|
112
|
+
json(res, 200, await dashboardMemory());
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (req.method === 'GET' && pathname === '/api/usage') {
|
|
116
|
+
const { dashboardUsage } = await import('./api-helpers.js');
|
|
117
|
+
json(res, 200, await dashboardUsage());
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (req.method === 'GET' && pathname === '/api/self-improve') {
|
|
121
|
+
const { dashboardSelfImprove } = await import('./api-helpers.js');
|
|
122
|
+
json(res, 200, await dashboardSelfImprove());
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (req.method === 'GET' && pathname === '/api/install') {
|
|
126
|
+
const { dashboardInstall } = await import('./api-helpers.js');
|
|
127
|
+
json(res, 200, dashboardInstall());
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if (req.method === 'GET' && pathname === '/api/persona') {
|
|
131
|
+
const { dashboardPersona } = await import('./api-helpers.js');
|
|
132
|
+
json(res, 200, await dashboardPersona());
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (req.method === 'POST' && pathname === '/api/terminal/run') {
|
|
136
|
+
const { handleTerminalRun } = await import('./terminal.js');
|
|
137
|
+
await handleTerminalRun(req, res);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (req.method === 'POST' && pathname === '/api/terminal/reset') {
|
|
141
|
+
const url = new URL(req.url ?? '/', 'http://local');
|
|
142
|
+
const { resetTerminalSession } = await import('./terminal.js');
|
|
143
|
+
resetTerminalSession(url.searchParams.get('session') ?? 'web');
|
|
144
|
+
json(res, 200, { ok: true });
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (req.method === 'GET' && pathname === '/api/terminal/shell-status') {
|
|
148
|
+
const { shellStatus } = await import('./terminal.js');
|
|
149
|
+
json(res, 200, await shellStatus());
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
94
152
|
if (req.method === 'GET' && pathname === '/api/chat/status') {
|
|
95
153
|
json(res, 200, {
|
|
96
154
|
hint: `Use ${BRAND.cliName} in terminal, or start ${BRAND.cliName} serve for HTTP chat`,
|
|
@@ -146,6 +204,22 @@ async function serveStatic(res, staticDir, pathname) {
|
|
|
146
204
|
}
|
|
147
205
|
}
|
|
148
206
|
}
|
|
207
|
+
async function serveInstallScript(res, pathname) {
|
|
208
|
+
if (pathname !== '/install.sh' && pathname !== '/install.ps1')
|
|
209
|
+
return false;
|
|
210
|
+
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..');
|
|
211
|
+
const name = pathname === '/install.sh' ? 'install.sh' : 'install.ps1';
|
|
212
|
+
try {
|
|
213
|
+
const body = await readFile(join(root, 'scripts', name), 'utf8');
|
|
214
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=300' });
|
|
215
|
+
res.end(body);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
res.writeHead(404);
|
|
219
|
+
res.end('install script not found');
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
149
223
|
export async function startDashboardServer(opts = {}) {
|
|
150
224
|
const port = opts.port ?? 9119;
|
|
151
225
|
const host = opts.host ?? '127.0.0.1';
|
|
@@ -161,6 +235,8 @@ export async function startDashboardServer(opts = {}) {
|
|
|
161
235
|
json(res, 404, { error: 'not found' });
|
|
162
236
|
return;
|
|
163
237
|
}
|
|
238
|
+
if (req.method === 'GET' && (await serveInstallScript(res, url.pathname)))
|
|
239
|
+
return;
|
|
164
240
|
await serveStatic(res, staticDir, url.pathname);
|
|
165
241
|
}
|
|
166
242
|
catch (e) {
|
|
@@ -171,6 +247,14 @@ export async function startDashboardServer(opts = {}) {
|
|
|
171
247
|
server.once('error', reject);
|
|
172
248
|
server.listen(port, host, () => resolve());
|
|
173
249
|
});
|
|
250
|
+
// raw shell over ws (no-op if node-pty/ws not installed)
|
|
251
|
+
try {
|
|
252
|
+
const { attachShell } = await import('./terminal.js');
|
|
253
|
+
await attachShell(server);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
/* optional */
|
|
257
|
+
}
|
|
174
258
|
log(`Sanook Dashboard — http://${host}:${port}`);
|
|
175
259
|
return () => server.close();
|
|
176
260
|
}
|