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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { runAgent } from '../loop.js';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import { redactKey } from '../providers/keys.js';
|
|
4
|
+
import { describeToolCall } from '../ui/tool-activity.js';
|
|
5
|
+
const HISTORY = new Map(); // sessionId → conversation (localhost single-user)
|
|
6
|
+
const MAX_HISTORY_SESSIONS = 20;
|
|
7
|
+
function sseSend(res, event) {
|
|
8
|
+
if (res.destroyed || res.writableEnded)
|
|
9
|
+
return; // client gone — don't write to a dead socket
|
|
10
|
+
try {
|
|
11
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* socket closed mid-write */
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function readJsonBody(req) {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
for await (const chunk of req)
|
|
20
|
+
chunks.push(Buffer.from(chunk));
|
|
21
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(raw || '{}');
|
|
24
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** POST /api/terminal/run — body {prompt, sessionId, autoApprove?} → SSE stream of agent events */
|
|
31
|
+
export async function handleTerminalRun(req, res) {
|
|
32
|
+
const body = await readJsonBody(req);
|
|
33
|
+
const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : '';
|
|
34
|
+
const sessionId = typeof body.sessionId === 'string' && body.sessionId ? body.sessionId : 'web';
|
|
35
|
+
const autoApprove = body.autoApprove !== false; // default true (localhost = same trust as running CLI)
|
|
36
|
+
res.writeHead(200, {
|
|
37
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
38
|
+
'cache-control': 'no-cache, no-transform',
|
|
39
|
+
connection: 'keep-alive',
|
|
40
|
+
});
|
|
41
|
+
if (!prompt) {
|
|
42
|
+
sseSend(res, { type: 'error', message: 'empty prompt' });
|
|
43
|
+
sseSend(res, { type: 'done' });
|
|
44
|
+
res.end();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const rememberedFacts = [];
|
|
48
|
+
// abort the agent run if the browser disconnects mid-stream — otherwise it keeps executing
|
|
49
|
+
// (and metering cost) and writing to a closed socket. RunAgent honors opts.signal.
|
|
50
|
+
const ac = new AbortController();
|
|
51
|
+
const onClose = () => ac.abort();
|
|
52
|
+
res.on('close', onClose);
|
|
53
|
+
// headers are already sent above, so everything that can throw stays inside this try and emits an
|
|
54
|
+
// SSE error here — otherwise the throw propagates to the server's catch, which calls writeHead(500)
|
|
55
|
+
// on an already-headed response (ERR_HTTP_HEADERS_SENT).
|
|
56
|
+
try {
|
|
57
|
+
const config = await loadConfig({});
|
|
58
|
+
const model = config.model;
|
|
59
|
+
const history = HISTORY.get(sessionId) ?? [];
|
|
60
|
+
sseSend(res, { type: 'status', detail: `Agent · ${model}` });
|
|
61
|
+
const { messages } = await runAgent({
|
|
62
|
+
model,
|
|
63
|
+
prompt,
|
|
64
|
+
history,
|
|
65
|
+
maxSteps: 20,
|
|
66
|
+
permissionMode: autoApprove ? 'auto' : 'ask',
|
|
67
|
+
signal: ac.signal,
|
|
68
|
+
usageMeta: { sessionId: `web:${sessionId}`, source: 'repl' },
|
|
69
|
+
onEvent: (e) => {
|
|
70
|
+
switch (e.type) {
|
|
71
|
+
case 'text':
|
|
72
|
+
if (e.text)
|
|
73
|
+
sseSend(res, { type: 'text', text: e.text });
|
|
74
|
+
break;
|
|
75
|
+
case 'reasoning':
|
|
76
|
+
if (e.text)
|
|
77
|
+
sseSend(res, { type: 'reasoning', text: e.text });
|
|
78
|
+
break;
|
|
79
|
+
case 'tool-call': {
|
|
80
|
+
if (e.tool === 'remember') {
|
|
81
|
+
const fact = e.detail?.fact;
|
|
82
|
+
if (typeof fact === 'string' && fact.trim())
|
|
83
|
+
rememberedFacts.push(fact.trim());
|
|
84
|
+
}
|
|
85
|
+
const activity = describeToolCall(e.tool ?? 'tool', e.detail);
|
|
86
|
+
sseSend(res, { type: 'tool-call', tool: e.tool, title: activity.title, diff: activity.diff ?? null });
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'tool-result':
|
|
90
|
+
sseSend(res, { type: 'tool-result', tool: e.tool });
|
|
91
|
+
break;
|
|
92
|
+
case 'status':
|
|
93
|
+
if (typeof e.detail === 'string')
|
|
94
|
+
sseSend(res, { type: 'status', detail: e.detail });
|
|
95
|
+
break;
|
|
96
|
+
case 'error':
|
|
97
|
+
sseSend(res, { type: 'error', message: redactKey(String(e.detail ?? e.text ?? 'error')) });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
HISTORY.set(sessionId, messages);
|
|
103
|
+
if (HISTORY.size > MAX_HISTORY_SESSIONS)
|
|
104
|
+
HISTORY.delete([...HISTORY.keys()][0]);
|
|
105
|
+
for (const fact of rememberedFacts)
|
|
106
|
+
sseSend(res, { type: 'memory', fact });
|
|
107
|
+
// ✨ self-improvement (same path as REPL/headless)
|
|
108
|
+
try {
|
|
109
|
+
const { maybeAutoSkill } = await import('../self-improve.js');
|
|
110
|
+
const { defaultSkillSynthesizer } = await import('../self-improve-synth.js');
|
|
111
|
+
const { loadSkills, saveSkill } = await import('../skills.js');
|
|
112
|
+
const existing = new Set((await loadSkills()).map((s) => s.name));
|
|
113
|
+
const auto = await maybeAutoSkill(prompt, { synthesize: defaultSkillSynthesizer(model), saveSkill, existingSkillNames: existing });
|
|
114
|
+
if (auto.created && auto.skillName)
|
|
115
|
+
sseSend(res, { type: 'skill', name: auto.skillName, count: auto.count });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* best-effort */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
sseSend(res, { type: 'error', message: redactKey(err.message) });
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
res.off('close', onClose);
|
|
126
|
+
}
|
|
127
|
+
sseSend(res, { type: 'done' });
|
|
128
|
+
if (!res.writableEnded)
|
|
129
|
+
res.end();
|
|
130
|
+
}
|
|
131
|
+
export function resetTerminalSession(sessionId) {
|
|
132
|
+
HISTORY.delete(sessionId);
|
|
133
|
+
}
|
|
134
|
+
// ---- Raw shell (optional node-pty + ws) ------------------------------------
|
|
135
|
+
let shellAvailability = null;
|
|
136
|
+
// computed specifiers so TS/bundler don't statically resolve these optional, possibly-absent deps
|
|
137
|
+
const PTY_MODULE = 'node-pty';
|
|
138
|
+
const WS_MODULE = 'ws';
|
|
139
|
+
export async function shellStatus() {
|
|
140
|
+
if (shellAvailability)
|
|
141
|
+
return shellAvailability;
|
|
142
|
+
const missing = [];
|
|
143
|
+
try {
|
|
144
|
+
await import(PTY_MODULE);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
missing.push('node-pty');
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await import(WS_MODULE);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
missing.push('ws');
|
|
154
|
+
}
|
|
155
|
+
shellAvailability = missing.length
|
|
156
|
+
? { available: false, reason: `ติดตั้ง dependency เสริมก่อน: npm i ${missing.join(' ')}` }
|
|
157
|
+
: { available: true, reason: 'ready' };
|
|
158
|
+
return shellAvailability;
|
|
159
|
+
}
|
|
160
|
+
/** Attach a ws upgrade handler for the raw shell at /api/terminal/shell. No-op if deps missing. */
|
|
161
|
+
export async function attachShell(server) {
|
|
162
|
+
const status = await shellStatus();
|
|
163
|
+
if (!status.available)
|
|
164
|
+
return;
|
|
165
|
+
// dynamic import keeps node-pty/ws optional at build/runtime
|
|
166
|
+
const pty = (await import(PTY_MODULE));
|
|
167
|
+
const wsmod = (await import(WS_MODULE));
|
|
168
|
+
const wss = new wsmod.WebSocketServer({ noServer: true });
|
|
169
|
+
server.on('upgrade', (req, socket, head) => {
|
|
170
|
+
const url = new URL(req.url ?? '/', 'http://local');
|
|
171
|
+
if (url.pathname !== '/api/terminal/shell') {
|
|
172
|
+
// not ours — destroy the half-open upgraded socket so it doesn't leak (this is the only
|
|
173
|
+
// 'upgrade' handler on the dashboard server; with noServer ws, Node won't auto-close it).
|
|
174
|
+
socket.destroy();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
178
|
+
const shell = process.platform === 'win32' ? 'powershell.exe' : process.env.SHELL || 'bash';
|
|
179
|
+
const term = pty.spawn(shell, [], {
|
|
180
|
+
name: 'xterm-color',
|
|
181
|
+
cols: 80,
|
|
182
|
+
rows: 24,
|
|
183
|
+
cwd: process.env.HOME || process.cwd(),
|
|
184
|
+
env: process.env,
|
|
185
|
+
});
|
|
186
|
+
const safeKill = () => {
|
|
187
|
+
try {
|
|
188
|
+
term.kill();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* pty already exited — kill() can throw for a reaped pid on some platforms */
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
term.onData((data) => ws.send(JSON.stringify({ type: 'data', data })));
|
|
195
|
+
term.onExit(() => ws.close());
|
|
196
|
+
ws.on('message', (raw) => {
|
|
197
|
+
try {
|
|
198
|
+
const msg = JSON.parse(String(raw));
|
|
199
|
+
if (msg.type === 'data' && typeof msg.data === 'string')
|
|
200
|
+
term.write(msg.data);
|
|
201
|
+
else if (msg.type === 'resize' && msg.cols && msg.rows)
|
|
202
|
+
term.resize(msg.cols, msg.rows);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* ignore malformed frame */
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
ws.on('close', safeKill);
|
|
209
|
+
// without an 'error' listener, a ws error (abrupt TCP reset / protocol violation) is rethrown
|
|
210
|
+
// by Node's EventEmitter as an uncaught exception and crashes the whole dashboard server.
|
|
211
|
+
ws.on('error', safeKill);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
package/dist/diff.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// minimal unified-ish diff (zero dep) — โชว์ให้เห็นว่าแก้อะไรก่อน/หลัง โปร่งใส
|
|
2
2
|
const MAX_LINES = 14;
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* core prefix/suffix-trim diff — shared by renderEditDiff (string) and the REPL's colored
|
|
5
|
+
* diffLines (src/ui/tool-activity.ts) so the algorithm lives in one place. Callers pick their
|
|
6
|
+
* own line cap; only the formatting differs.
|
|
7
|
+
*/
|
|
8
|
+
export function editDiffSegments(oldStr, newStr, max = MAX_LINES) {
|
|
5
9
|
const oldL = oldStr.split('\n');
|
|
6
10
|
const newL = newStr.split('\n');
|
|
7
11
|
// ตัด common prefix/suffix lines ที่เหมือนกัน เพื่อโชว์เฉพาะส่วนที่เปลี่ยน
|
|
@@ -15,15 +19,25 @@ export function renderEditDiff(oldStr, newStr) {
|
|
|
15
19
|
suf++;
|
|
16
20
|
const oldMid = oldL.slice(pre, oldL.length - suf);
|
|
17
21
|
const newMid = newL.slice(pre, newL.length - suf);
|
|
22
|
+
return {
|
|
23
|
+
removed: oldMid.slice(0, max),
|
|
24
|
+
added: newMid.slice(0, max),
|
|
25
|
+
moreRemoved: Math.max(0, oldMid.length - max),
|
|
26
|
+
moreAdded: Math.max(0, newMid.length - max),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** diff ของ edit (old block → new block) — render เป็น -old / +new */
|
|
30
|
+
export function renderEditDiff(oldStr, newStr) {
|
|
31
|
+
const seg = editDiffSegments(oldStr, newStr, MAX_LINES);
|
|
18
32
|
const lines = [];
|
|
19
|
-
for (const l of
|
|
33
|
+
for (const l of seg.removed)
|
|
20
34
|
lines.push(`- ${l}`);
|
|
21
|
-
if (
|
|
22
|
-
lines.push(` …(-${
|
|
23
|
-
for (const l of
|
|
35
|
+
if (seg.moreRemoved)
|
|
36
|
+
lines.push(` …(-${seg.moreRemoved} บรรทัด)`);
|
|
37
|
+
for (const l of seg.added)
|
|
24
38
|
lines.push(`+ ${l}`);
|
|
25
|
-
if (
|
|
26
|
-
lines.push(` …(+${
|
|
39
|
+
if (seg.moreAdded)
|
|
40
|
+
lines.push(` …(+${seg.moreAdded} บรรทัด)`);
|
|
27
41
|
return lines.join('\n');
|
|
28
42
|
}
|
|
29
43
|
/** สรุปการ write — จำนวนบรรทัด/ตัวอักษร + ถ้าเขียนทับ บอก before→after */
|
package/dist/gateway/session.js
CHANGED
|
@@ -172,6 +172,10 @@ async function runAndSaveGatewayTurn(opts, existing, prompt, history, model) {
|
|
|
172
172
|
maxSteps: opts.maxSteps ?? 20,
|
|
173
173
|
budgetUsd: opts.budgetUsd,
|
|
174
174
|
permissionMode: opts.permissionMode ?? 'ask',
|
|
175
|
+
usageMeta: {
|
|
176
|
+
sessionId: `${opts.platform}:${opts.target}`,
|
|
177
|
+
source: 'gateway',
|
|
178
|
+
},
|
|
175
179
|
});
|
|
176
180
|
await saveGatewayState(opts, existing, model, messages);
|
|
177
181
|
return { text, messages, suppressDelivery: shouldSuppressDelivery(text) };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Multi-platform install metadata — single source for Dashboard, README sync, and docs.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
export const INSTALL_PKG = 'sanook-cli';
|
|
6
|
+
export const INSTALL_REPO = 'Sir-chawakorn/sanook-cli';
|
|
7
|
+
export const INSTALL_REPO_URL = `https://github.com/${INSTALL_REPO}`;
|
|
8
|
+
export const INSTALL_BRANCH = 'main';
|
|
9
|
+
/** Custom domain when DNS + Pages are configured */
|
|
10
|
+
export const INSTALL_DOMAIN = 'sanook.ai';
|
|
11
|
+
/** GitHub Pages project URL (works when gh-pages branch is deployed) */
|
|
12
|
+
export const INSTALL_PAGES_URL = `https://${INSTALL_REPO.split('/')[0]}.github.io/${INSTALL_REPO.split('/')[1]}`;
|
|
13
|
+
/** jsDelivr CDN — stable short URL without custom domain */
|
|
14
|
+
export const INSTALL_CDN_URL = `https://cdn.jsdelivr.net/gh/${INSTALL_REPO}@${INSTALL_BRANCH}/scripts`;
|
|
15
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
/** package.json version at build time (repo root when running from src/) */
|
|
17
|
+
export function installPkgVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(readFileSync(join(__dir, '..', 'package.json'), 'utf8'));
|
|
20
|
+
return pkg.version ?? 'latest';
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return 'latest';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function installScriptUrl(name, preferDomain = false) {
|
|
27
|
+
if (preferDomain)
|
|
28
|
+
return `https://${INSTALL_DOMAIN}/${name}`;
|
|
29
|
+
return `https://raw.githubusercontent.com/${INSTALL_REPO}/${INSTALL_BRANCH}/scripts/${name}`;
|
|
30
|
+
}
|
|
31
|
+
export function installScriptPagesUrl(name) {
|
|
32
|
+
return `${INSTALL_PAGES_URL}/${name}`;
|
|
33
|
+
}
|
|
34
|
+
export function installScriptCdnUrl(name) {
|
|
35
|
+
return `${INSTALL_CDN_URL}/${name}`;
|
|
36
|
+
}
|
|
37
|
+
/** Install channels shown on Dashboard + docs. `ready` = users can run today without extra infra. */
|
|
38
|
+
export function installMethods() {
|
|
39
|
+
const sh = installScriptUrl('install.sh');
|
|
40
|
+
const ps1 = installScriptUrl('install.ps1');
|
|
41
|
+
const shPages = installScriptPagesUrl('install.sh');
|
|
42
|
+
const shDomain = installScriptUrl('install.sh', true);
|
|
43
|
+
const shCdn = installScriptCdnUrl('install.sh');
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
id: 'npm',
|
|
47
|
+
label: 'npm / npx',
|
|
48
|
+
recommended: true,
|
|
49
|
+
ready: true,
|
|
50
|
+
commands: [
|
|
51
|
+
{ os: 'macOS / Linux / Windows', cmd: `npm install -g ${INSTALL_PKG}` },
|
|
52
|
+
{ os: 'Run without installing', cmd: `npx ${INSTALL_PKG}` },
|
|
53
|
+
],
|
|
54
|
+
note: 'ต้องมี Node.js ≥ 22',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'curl',
|
|
58
|
+
label: 'Install script',
|
|
59
|
+
ready: true,
|
|
60
|
+
commands: [
|
|
61
|
+
{ os: 'macOS / Linux / WSL (GitHub raw)', cmd: `curl -fsSL ${sh} | bash` },
|
|
62
|
+
{ os: 'Windows PowerShell (GitHub raw)', cmd: `irm ${ps1} | iex` },
|
|
63
|
+
{ os: 'CDN (jsDelivr)', cmd: `curl -fsSL ${shCdn} | bash` },
|
|
64
|
+
{ os: 'GitHub Pages', cmd: `curl -fsSL ${shPages} | bash` },
|
|
65
|
+
{ os: `Short URL (${INSTALL_DOMAIN})`, cmd: `curl -fsSL ${shDomain} | bash` },
|
|
66
|
+
],
|
|
67
|
+
note: `${INSTALL_DOMAIN} ต้องตั้ง DNS ที่ GoDaddy ก่อน — ดู scripts/configure-sanook-ai-dns.sh`,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'homebrew',
|
|
71
|
+
label: 'Homebrew',
|
|
72
|
+
ready: true,
|
|
73
|
+
commands: [
|
|
74
|
+
{ os: 'macOS / Linux (trust tap ครั้งแรก)', cmd: `brew trust ${INSTALL_REPO.split('/')[0]}/tap` },
|
|
75
|
+
{ os: 'macOS / Linux', cmd: `brew tap ${INSTALL_REPO.split('/')[0]}/tap` },
|
|
76
|
+
{ os: 'macOS / Linux', cmd: `brew install ${INSTALL_PKG}` },
|
|
77
|
+
],
|
|
78
|
+
note: `Live ที่ homebrew-tap — brew tap ${INSTALL_REPO.split('/')[0]}/tap && brew install ${INSTALL_PKG}`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'winget',
|
|
82
|
+
label: 'WinGet',
|
|
83
|
+
ready: false,
|
|
84
|
+
commands: [{ os: 'Windows', cmd: 'winget install Sanook.SanookCLI' }],
|
|
85
|
+
note: 'CLA ลงนามแล้ว — PR #391114 รอ validation + merge (release zip v0.5.7 พร้อม)',
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
export function dashboardInstallPayload() {
|
|
90
|
+
return { pkg: INSTALL_PKG, version: installPkgVersion(), methods: installMethods() };
|
|
91
|
+
}
|
package/dist/loop.js
CHANGED
|
@@ -19,6 +19,7 @@ import { agentTuning, loadConfig } from './config.js';
|
|
|
19
19
|
import { BRAND, envFlag } from './brand.js';
|
|
20
20
|
import { semanticRecallHits } from './knowledge.js';
|
|
21
21
|
import { personalityPrompt } from './personality.js';
|
|
22
|
+
import { recordAgentUsage, usageFromCodexPayload } from './usage-ledger.js';
|
|
22
23
|
// auto-compact เมื่อ context ใกล้เต็ม — conservative (safe สำหรับ model 200K, เผื่อ output)
|
|
23
24
|
const AUTO_COMPACT_TOKENS = 120_000;
|
|
24
25
|
const OS_LABEL = process.platform === 'win32'
|
|
@@ -148,7 +149,6 @@ async function maybeWrapWithHeadroom(model) {
|
|
|
148
149
|
* แกน harness — agent loop: LLM -> tool -> result -> loop จนเสร็จ
|
|
149
150
|
* multi-provider (BYOK) ผ่าน registry + cost meter + budget cap
|
|
150
151
|
*/
|
|
151
|
-
/** delegate path — spawn official codex CLI (ChatGPT plan quota) แทน SDK loop */
|
|
152
152
|
async function runDelegate(opts) {
|
|
153
153
|
const { runCodex } = await import('./providers/codex.js');
|
|
154
154
|
const meter = new CostMeter(specKey(opts.model), opts.budgetUsd, agentContext.getStore()?.sharedBudget);
|
|
@@ -173,10 +173,11 @@ async function runDelegate(opts) {
|
|
|
173
173
|
// sandbox: plan/ask-mode → read-only (สกัด approval รายไฟล์ของ codex ไม่ได้ จึงไม่ให้แก้);
|
|
174
174
|
// auto (--yes / config auto) → workspace-write เพื่อให้ codex แก้ไฟล์ได้จริง (ไม่งั้นเป็น coding agent ที่แก้อะไรไม่ได้)
|
|
175
175
|
const sandbox = opts.planMode || (opts.permissionMode ?? 'ask') === 'ask' ? 'read-only' : 'workspace-write';
|
|
176
|
+
opts.onEvent?.({ type: 'status', detail: `Codex · ${model} · ${sandbox}` });
|
|
176
177
|
let text = '';
|
|
177
178
|
const out = await runCodex({
|
|
178
179
|
prompt,
|
|
179
|
-
model: model ===
|
|
180
|
+
model: model === PROVIDERS.codex.models.default ? undefined : model,
|
|
180
181
|
sandbox,
|
|
181
182
|
cwd: opts.cwd, // worktree isolation ของ sub-agent
|
|
182
183
|
signal: opts.signal,
|
|
@@ -190,6 +191,9 @@ async function runDelegate(opts) {
|
|
|
190
191
|
opts.onEvent?.({ type: 'text', text: delta });
|
|
191
192
|
}
|
|
192
193
|
else if (e.type === 'usage') {
|
|
194
|
+
const parsed = usageFromCodexPayload(e.usage);
|
|
195
|
+
if (parsed)
|
|
196
|
+
meter.add(parsed);
|
|
193
197
|
opts.onEvent?.({ type: 'finish', detail: 'codex · ChatGPT quota' });
|
|
194
198
|
}
|
|
195
199
|
},
|
|
@@ -200,7 +204,26 @@ async function runDelegate(opts) {
|
|
|
200
204
|
{ role: 'user', content: opts.prompt },
|
|
201
205
|
{ role: 'assistant', content: text },
|
|
202
206
|
];
|
|
203
|
-
return { messages, text, cost: meter };
|
|
207
|
+
return finishAgentRun(opts, { messages, text, cost: meter });
|
|
208
|
+
}
|
|
209
|
+
function inferUsageSource(opts) {
|
|
210
|
+
if (opts.usageMeta?.source)
|
|
211
|
+
return opts.usageMeta.source;
|
|
212
|
+
if ((opts.subagentDepth ?? 0) > 0)
|
|
213
|
+
return 'subagent';
|
|
214
|
+
if (opts.planMode)
|
|
215
|
+
return 'plan';
|
|
216
|
+
return 'headless';
|
|
217
|
+
}
|
|
218
|
+
function finishAgentRun(opts, result) {
|
|
219
|
+
recordAgentUsage({
|
|
220
|
+
model: opts.model,
|
|
221
|
+
cost: result.cost,
|
|
222
|
+
cwd: opts.cwd ?? agentCwd(),
|
|
223
|
+
sessionId: opts.usageMeta?.sessionId,
|
|
224
|
+
source: inferUsageSource(opts),
|
|
225
|
+
});
|
|
226
|
+
return result;
|
|
204
227
|
}
|
|
205
228
|
export async function runAgent(opts) {
|
|
206
229
|
// context ผ่าน AsyncLocalStorage (ไม่ใช่ process.env global) → parallel sub-agent ไม่ชนกัน
|
|
@@ -213,6 +236,7 @@ export async function runAgent(opts) {
|
|
|
213
236
|
if (PROVIDERS[parseSpec(opts.model).provider]?.kind === 'delegate') {
|
|
214
237
|
return runDelegate(opts);
|
|
215
238
|
}
|
|
239
|
+
opts.onEvent?.({ type: 'status', detail: `Agent · ${opts.model}` });
|
|
216
240
|
const rawModel = resolveModel(opts.model); // throws ถ้าไม่มี key / provider ผิด
|
|
217
241
|
let meter = new CostMeter(specKey(opts.model), opts.budgetUsd, sharedBudget);
|
|
218
242
|
// โหลด context: auto-memory + skills + git state + repo map + project SANOOK.md → system prompt
|
|
@@ -355,14 +379,17 @@ export async function runAgent(opts) {
|
|
|
355
379
|
opts.onEvent?.({ type: 'text', text: part.text });
|
|
356
380
|
break;
|
|
357
381
|
case 'reasoning-delta':
|
|
382
|
+
opts.onEvent?.({ type: 'status', detail: 'Thinking…' });
|
|
358
383
|
opts.onEvent?.({ type: 'reasoning', text: part.text });
|
|
359
384
|
break;
|
|
360
385
|
case 'tool-call':
|
|
361
386
|
if (isMutatingTool(part.toolName))
|
|
362
387
|
sideEffectToolSeen = true;
|
|
388
|
+
opts.onEvent?.({ type: 'status', detail: `Tool · ${part.toolName}` });
|
|
363
389
|
opts.onEvent?.({ type: 'tool-call', tool: part.toolName, detail: part.input });
|
|
364
390
|
break;
|
|
365
391
|
case 'tool-result':
|
|
392
|
+
opts.onEvent?.({ type: 'status', detail: `Done · ${part.toolName}` });
|
|
366
393
|
opts.onEvent?.({ type: 'tool-result', tool: part.toolName, detail: part.output });
|
|
367
394
|
break;
|
|
368
395
|
case 'error':
|
|
@@ -409,5 +436,5 @@ export async function runAgent(opts) {
|
|
|
409
436
|
throw new Error(cleanProviderError(streamError));
|
|
410
437
|
const response = await result.response;
|
|
411
438
|
// คืน history เต็ม (conversation + response messages) — ไม่รวม system (กัน user turn เก่าหาย + ไม่ save system ซ้ำ)
|
|
412
|
-
return { messages: [...conversation, ...response.messages], text, cost: meter };
|
|
439
|
+
return finishAgentRun(opts, { messages: [...conversation, ...response.messages], text, cost: meter });
|
|
413
440
|
}
|