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
|
@@ -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/i18n/en.js
CHANGED
|
@@ -36,6 +36,7 @@ export const en = {
|
|
|
36
36
|
codexOptionRecheck: 'Re-check (after install/login)',
|
|
37
37
|
codexOptionBack: '← Choose another provider',
|
|
38
38
|
codexInstallCmd: 'npm i -g @openai/codex',
|
|
39
|
+
codexModelHint: 'ChatGPT plan supports gpt-5.5 · gpt-5.4 · gpt-5.4-mini only (legacy *-codex ids are not available)',
|
|
39
40
|
keyEscHint: '(Esc = back)',
|
|
40
41
|
keyOpenAiCodexHint: 'Have ChatGPT Plus/Pro? Press Esc and pick OpenAI Codex (ChatGPT plan) — no API key needed.',
|
|
41
42
|
keyFormatHint: 'Key format',
|
package/dist/i18n/th.js
CHANGED
|
@@ -36,6 +36,7 @@ export const th = {
|
|
|
36
36
|
codexOptionRecheck: 'เช็กใหม่ (หลังติดตั้ง/login)',
|
|
37
37
|
codexOptionBack: '← กลับไปเลือก provider อื่น',
|
|
38
38
|
codexInstallCmd: 'npm i -g @openai/codex',
|
|
39
|
+
codexModelHint: 'ChatGPT plan ใช้ได้ gpt-5.5 · gpt-5.4 · gpt-5.4-mini เท่านั้น (โมเดล *-codex เก่าใช้ไม่ได้)',
|
|
39
40
|
keyEscHint: '(Esc = กลับ)',
|
|
40
41
|
keyOpenAiCodexHint: 'มี ChatGPT Plus/Pro? กด Esc แล้วเลือก OpenAI Codex (ChatGPT plan) — ไม่ต้อง API key',
|
|
41
42
|
keyFormatHint: 'รูปแบบ key',
|
|
@@ -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
|
@@ -174,10 +174,19 @@ async function runDelegate(opts) {
|
|
|
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
176
|
opts.onEvent?.({ type: 'status', detail: `Codex · ${model} · ${sandbox}` });
|
|
177
|
+
const { normalizeCodexChatGptModel } = await import('./providers/codex.js');
|
|
178
|
+
const normalized = normalizeCodexChatGptModel(model);
|
|
179
|
+
if (normalized.migratedFrom) {
|
|
180
|
+
opts.onEvent?.({
|
|
181
|
+
type: 'status',
|
|
182
|
+
detail: `Codex model ${normalized.migratedFrom} ไม่รองรับ ChatGPT plan → ใช้ ${normalized.model} แทน (sanook model เพื่ออัปเดต: /model codex)`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
177
185
|
let text = '';
|
|
186
|
+
const execModel = normalized.model === PROVIDERS.codex.models.default ? undefined : normalized.model;
|
|
178
187
|
const out = await runCodex({
|
|
179
188
|
prompt,
|
|
180
|
-
model:
|
|
189
|
+
model: execModel,
|
|
181
190
|
sandbox,
|
|
182
191
|
cwd: opts.cwd, // worktree isolation ของ sub-agent
|
|
183
192
|
signal: opts.signal,
|