sanook-cli 0.5.7 → 0.5.9
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 +42 -0
- package/README.md +392 -42
- package/README.th.md +15 -8
- package/dist/auto-maintain.js +113 -0
- package/dist/bin.js +63 -15
- package/dist/brain-final.js +8 -4
- package/dist/brain-new.js +9 -5
- package/dist/brain-repair.js +7 -4
- package/dist/brand.js +17 -0
- package/dist/commands.js +17 -6
- package/dist/config.js +11 -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/i18n/en.js +1 -0
- package/dist/i18n/th.js +1 -0
- package/dist/install-info.js +91 -0
- package/dist/loop.js +10 -1
- package/dist/memory.js +236 -16
- package/dist/model-picker.js +4 -1
- package/dist/persona.js +300 -0
- package/dist/project-scaffold.js +4 -2
- package/dist/providers/codex.js +75 -2
- package/dist/providers/models.js +17 -2
- package/dist/providers/registry.js +6 -13
- package/dist/self-improve-synth.js +86 -0
- package/dist/self-improve.js +203 -0
- package/dist/session-brain.js +10 -1
- package/dist/slash-completion.js +1 -0
- package/dist/ui/app.js +118 -27
- package/dist/ui/input-view.js +104 -0
- package/dist/ui/persona-wizard.js +89 -0
- package/dist/ui/render.js +78 -5
- package/dist/ui/setup.js +3 -4
- package/dist/ui/tool-activity.js +118 -0
- package/dist/ui/tool-trail.js +15 -3
- package/package.json +9 -1
|
@@ -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
|
}
|