sanook-cli 0.5.0 → 0.5.2
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 +161 -3
- package/CHANGELOG.md +83 -5
- package/README.md +240 -23
- package/README.th.md +87 -6
- package/dist/approval.js +6 -0
- package/dist/bin.js +3045 -210
- package/dist/brain-context.js +223 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +371 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +12 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +152 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/commands.js +172 -13
- package/dist/compaction.js +96 -11
- package/dist/config.js +118 -28
- package/dist/context-compression.js +191 -0
- package/dist/cost.js +49 -15
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +37 -8
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +357 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/email.js +472 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +18 -0
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +343 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/insights-args.js +35 -0
- package/dist/insights.js +86 -0
- package/dist/loop.js +123 -24
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-registry.js +350 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +44 -6
- package/dist/memory.js +100 -33
- package/dist/orchestrate.js +49 -19
- package/dist/personality.js +58 -0
- package/dist/providers/codex.js +86 -38
- package/dist/providers/keys.js +1 -1
- package/dist/providers/models.js +22 -6
- package/dist/providers/registry.js +38 -49
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +75 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session.js +93 -7
- package/dist/skill-install.js +29 -12
- package/dist/support-dump.js +175 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +923 -9
- package/dist/tools/read.js +16 -4
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +217 -13
- package/dist/tools/task.js +18 -7
- package/dist/tools/timeout.js +21 -3
- package/dist/trust.js +11 -1
- package/dist/ui/app.js +57 -11
- package/dist/ui/brain-wizard.js +2 -2
- package/dist/ui/history.js +37 -5
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/render.js +55 -15
- package/dist/ui/setup.js +107 -10
- package/dist/update.js +24 -11
- package/dist/worktree.js +175 -4
- package/package.json +4 -4
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +3 -1
- package/second-brain/Projects/sanook-cli/_Index.md +26 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/_Index.md +6 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +22 -3
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +4 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/ui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useRef
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { Box, Text, Static, useApp, useInput } from 'ink';
|
|
@@ -9,8 +9,9 @@ import { saveSession, newSessionId } from '../session.js';
|
|
|
9
9
|
import { getBrainPath, appendBrainWorklog } from '../memory.js';
|
|
10
10
|
import { autoCompact, estimateTokens, summarizeCompact } from '../compaction.js';
|
|
11
11
|
import { makeSummarizer } from '../summarize.js';
|
|
12
|
-
import { agentTuning } from '../config.js';
|
|
12
|
+
import { agentTuning, patchGlobalConfig } from '../config.js';
|
|
13
13
|
import { snapshotWorkTree, restoreWorkTree } from '../checkpoint.js';
|
|
14
|
+
import { renderInsights } from '../insights.js';
|
|
14
15
|
import { useEditor } from './useEditor.js';
|
|
15
16
|
import { loadHistory, appendHistory } from './history.js';
|
|
16
17
|
import { expandMentions } from './mentions.js';
|
|
@@ -18,11 +19,16 @@ import { BRAND } from '../brand.js';
|
|
|
18
19
|
import { Banner } from './banner.js';
|
|
19
20
|
const execFileP = promisify(execFile);
|
|
20
21
|
const PRE_TURN_COMPACT_TOKENS = 100_000; // session ยาวมากเท่านั้นถึง summarize ก่อน turn (mode summarize)
|
|
21
|
-
export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = 'ask', initialHistory }) {
|
|
22
|
+
export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = 'ask', initialHistory, initialNote }) {
|
|
22
23
|
const { exit } = useApp();
|
|
23
|
-
const [history, setHistory] = useState(
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const [history, setHistory] = useState(() => {
|
|
25
|
+
const seed = [];
|
|
26
|
+
if (initialNote)
|
|
27
|
+
seed.push({ id: -2, role: 'system', text: initialNote });
|
|
28
|
+
if (initialHistory?.length)
|
|
29
|
+
seed.push({ id: -1, role: 'system', text: `↻ ต่อจาก session ก่อน (${initialHistory.length} ข้อความ)` });
|
|
30
|
+
return seed;
|
|
31
|
+
});
|
|
26
32
|
const [streaming, setStreaming] = useState('');
|
|
27
33
|
const [busy, setBusy] = useState(false);
|
|
28
34
|
const [model, setModel] = useState(initialModel);
|
|
@@ -35,6 +41,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
35
41
|
const approvalResolve = useRef(null);
|
|
36
42
|
const replHistory = useRef(loadHistory()); // prompt เก่า (persist) สำหรับ ↑/↓
|
|
37
43
|
const checkpoints = useRef([]);
|
|
44
|
+
const lastRun = useRef(null);
|
|
38
45
|
const editor = useEditor(replHistory.current);
|
|
39
46
|
// real-time steering: หยุด turn ที่กำลังรัน (abort) + คิวข้อความที่พิมพ์ระหว่าง busy
|
|
40
47
|
const abortRef = useRef(null);
|
|
@@ -94,6 +101,13 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
94
101
|
if (a === 'submit') {
|
|
95
102
|
const v = editor.value.trim();
|
|
96
103
|
editor.reset();
|
|
104
|
+
const slash = parseSlashInvocation(v);
|
|
105
|
+
if (slash?.name === 'stop') {
|
|
106
|
+
addTurn('user', v);
|
|
107
|
+
abortRef.current?.abort();
|
|
108
|
+
clearQueue();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
97
111
|
if (v)
|
|
98
112
|
enqueue(v);
|
|
99
113
|
}
|
|
@@ -126,9 +140,26 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
126
140
|
: ` · ไฟล์: ${r.reason}`;
|
|
127
141
|
}
|
|
128
142
|
msgsRef.current = msgsRef.current.slice(0, cp.msgLen);
|
|
143
|
+
lastRun.current = null;
|
|
129
144
|
setHistory((h) => h.filter((t) => t.id < cp.turnId));
|
|
130
145
|
addTurn('system', `↩ ย้อนกลับ 1 turn${note}`);
|
|
131
146
|
}
|
|
147
|
+
async function retryLastTurn() {
|
|
148
|
+
const previous = lastRun.current;
|
|
149
|
+
if (!previous) {
|
|
150
|
+
addTurn('user', '/retry');
|
|
151
|
+
addTurn('system', 'ยังไม่มี turn ให้ retry');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
msgsRef.current = msgsRef.current.slice(0, previous.msgLen);
|
|
155
|
+
checkpoints.current = checkpoints.current.filter((cp) => cp.turnId < previous.turnId);
|
|
156
|
+
setHistory((h) => h.filter((t) => t.id < previous.turnId));
|
|
157
|
+
const mark = { turnId: idRef.current, msgLen: previous.msgLen };
|
|
158
|
+
const preview = previous.userText.length > 120 ? `${previous.userText.slice(0, 117)}...` : previous.userText;
|
|
159
|
+
addTurn('user', '/retry');
|
|
160
|
+
addTurn('system', `retry: ${preview}`);
|
|
161
|
+
await runAssistantTurn(previous.promptText, previous.images, mark, previous.userText);
|
|
162
|
+
}
|
|
132
163
|
/** บีบ context: 'summarize' (ใช้ model ถูกย่อ) ถ้าตั้งไว้ ไม่งั้น 'truncate' (zero-LLM) */
|
|
133
164
|
async function compactHistory(targetTokens, label) {
|
|
134
165
|
const before = estimateTokens(msgsRef.current);
|
|
@@ -170,7 +201,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
170
201
|
addTurn('system', `custom command /${slash.name} ว่าง`);
|
|
171
202
|
return;
|
|
172
203
|
}
|
|
173
|
-
await runAssistantTurn(expanded, [], mark);
|
|
204
|
+
await runAssistantTurn(expanded, [], mark, text);
|
|
174
205
|
return;
|
|
175
206
|
}
|
|
176
207
|
}
|
|
@@ -183,6 +214,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
183
214
|
if (cmd.action === 'clear') {
|
|
184
215
|
msgsRef.current = [];
|
|
185
216
|
checkpoints.current = [];
|
|
217
|
+
lastRun.current = null;
|
|
186
218
|
return setHistory([]);
|
|
187
219
|
}
|
|
188
220
|
if (cmd.action === 'compact') {
|
|
@@ -191,6 +223,20 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
191
223
|
}
|
|
192
224
|
if (cmd.action === 'diff')
|
|
193
225
|
return void runGit(['diff', '--stat'], 'diff');
|
|
226
|
+
if (cmd.action === 'retry')
|
|
227
|
+
return void retryLastTurn();
|
|
228
|
+
if (cmd.action === 'personality') {
|
|
229
|
+
void patchGlobalConfig({ personality: cmd.personalityChange || undefined })
|
|
230
|
+
.then(() => addTurn('system', cmd.message ?? 'ตั้ง personality แล้ว'))
|
|
231
|
+
.catch((e) => addTurn('system', `personality: ${e.message}`));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (cmd.action === 'insights') {
|
|
235
|
+
void renderInsights({ days: cmd.insightsDays, cwd: cmd.insightsAll ? null : undefined })
|
|
236
|
+
.then((msg) => addTurn('system', msg))
|
|
237
|
+
.catch((e) => addTurn('system', `insights: ${e.message}`));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
194
240
|
if (cmd.action === 'undo') {
|
|
195
241
|
void runGit(['stash', 'push', '-u', '-m', BRAND.undoStashMessage], 'undo').then(() => addTurn('system', 'กู้คืน: git stash pop'));
|
|
196
242
|
return;
|
|
@@ -207,9 +253,10 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
207
253
|
const { text: expanded, images, errors } = await expandMentions(text);
|
|
208
254
|
if (errors.length)
|
|
209
255
|
addTurn('system', `@mention: ${errors.join(' · ')}`);
|
|
210
|
-
await runAssistantTurn(expanded, images, mark);
|
|
256
|
+
await runAssistantTurn(expanded, images, mark, text);
|
|
211
257
|
}
|
|
212
|
-
async function runAssistantTurn(promptText, images, mark) {
|
|
258
|
+
async function runAssistantTurn(promptText, images, mark, userText = promptText) {
|
|
259
|
+
lastRun.current = { ...mark, userText, promptText, images };
|
|
213
260
|
// proactive summarize-compaction สำหรับ session ยาวมาก (เฉพาะ mode summarize) — เริ่ม turn ให้ context lean
|
|
214
261
|
// (mode truncate: ปล่อยให้ loop.ts ตัดต่อ-step เอา; ไม่บีบที่นี่ กัน latency)
|
|
215
262
|
if (estimateTokens(msgsRef.current) > PRE_TURN_COMPACT_TOKENS) {
|
|
@@ -299,9 +346,8 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
299
346
|
if (next)
|
|
300
347
|
void submit(next);
|
|
301
348
|
}
|
|
302
|
-
const banner = useMemo(() => _jsx(Banner, { model: initialModel }), [initialModel]);
|
|
303
349
|
const costHint = lastCost.current.includes('cost ') ? lastCost.current.split('cost ')[1] : '';
|
|
304
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
350
|
+
return (_jsxs(Box, { flexDirection: "column", children: [history.length === 0 ? _jsx(Banner, { model: model }) : null, _jsx(Static, { items: history, children: (turn) => _jsx(TurnView, { turn: turn }, turn.id) }), streaming ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: streaming }) })) : null, queued.length ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: queued.map((q, i) => (_jsxs(Text, { dimColor: true, children: ["\u23F3 \u0E04\u0E34\u0E27 ", i + 1, ": ", q.length > 64 ? `${q.slice(0, 64)}…` : q] }, i))) })) : null, approvalReq ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["\u0E2D\u0E19\u0E38\u0E21\u0E31\u0E15\u0E34\u0E23\u0E31\u0E19 ", approvalReq.tool, "?"] }), _jsx(Text, { dimColor: true, children: approvalReq.summary }), _jsx(Text, { dimColor: true, children: "y = \u0E23\u0E31\u0E19 \u00B7 n = \u0E1B\u0E0F\u0E34\u0E40\u0E2A\u0E18" })] })) : (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? 'gray' : 'blue', paddingX: 1, children: [_jsx(Text, { color: busy ? 'gray' : 'cyan', children: busy ? '· ' : '› ' }), _jsx(InputView, { value: editor.value, cursor: editor.cursor, busy: busy })] })), _jsxs(Text, { dimColor: true, children: [' ', model, " \u00B7 ", permissionMode === 'ask' ? 'ask-mode' : 'auto', " \u00B7 /help \u00B7 @file \u00B7 \u2191 history", costHint ? ` · ${costHint}` : ''] })] }));
|
|
305
351
|
}
|
|
306
352
|
/** input ที่มี cursor (inverse) + placeholder — minimal; รับ input ได้แม้ busy (ต่อคิว) */
|
|
307
353
|
function InputView({ value, cursor, busy }) {
|
package/dist/ui/brain-wizard.js
CHANGED
|
@@ -15,10 +15,10 @@ export function BrainWizard({ onComplete }) {
|
|
|
15
15
|
return (_jsxs(Box, { flexDirection: "column", gap: 1, marginY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "\uD83E\uDDE0 \u0E2A\u0E23\u0E49\u0E32\u0E07 Second Brain workspace" }), step === 'path' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "1. \u0E27\u0E32\u0E07\u0E42\u0E04\u0E23\u0E07\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E44\u0E27\u0E49\u0E17\u0E35\u0E48\u0E44\u0E2B\u0E19? (Enter = default)" }), _jsxs(Text, { color: "gray", children: [" ", DEFAULT_PATH] }), _jsx(TextInput, { defaultValue: DEFAULT_PATH, placeholder: DEFAULT_PATH, onSubmit: (v) => {
|
|
16
16
|
setPath(v.trim() || DEFAULT_PATH);
|
|
17
17
|
setStep('owner');
|
|
18
|
-
} })] })), step === 'owner' && (_jsxs(Box, { flexDirection: "column", children: [
|
|
18
|
+
} })] })), step === 'owner' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["2. \u0E40\u0E23\u0E35\u0E22\u0E01\u0E04\u0E38\u0E13\u0E27\u0E48\u0E32\u0E2D\u0E30\u0E44\u0E23\u0E14\u0E35? (\u0E0A\u0E37\u0E48\u0E2D/\u0E0A\u0E37\u0E48\u0E2D\u0E40\u0E25\u0E48\u0E19 \u2014 Enter \u0E40\u0E1B\u0E25\u0E48\u0E32 = \u0E43\u0E0A\u0E49 \"", BRAIN_DEFAULTS.ownerName, "\")"] }), _jsx(TextInput, { placeholder: BRAIN_DEFAULTS.ownerName, onSubmit: (v) => {
|
|
19
19
|
setOwnerName(v.trim() || BRAIN_DEFAULTS.ownerName);
|
|
20
20
|
setStep('ai');
|
|
21
|
-
} })] })), step === 'ai' && (_jsxs(Box, { flexDirection: "column", children: [
|
|
21
|
+
} })] })), step === 'ai' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["3. \u0E2D\u0E22\u0E32\u0E01\u0E43\u0E2B\u0E49 AI \u0E40\u0E23\u0E35\u0E22\u0E01\u0E15\u0E31\u0E27\u0E40\u0E2D\u0E07\u0E27\u0E48\u0E32\u0E2D\u0E30\u0E44\u0E23? ", _jsxs(Text, { color: "gray", children: ["(Enter \u0E40\u0E1B\u0E25\u0E48\u0E32 = \"", BRAIN_DEFAULTS.aiName, "\")"] })] }), _jsx(TextInput, { placeholder: BRAIN_DEFAULTS.aiName, onSubmit: (v) => {
|
|
22
22
|
setAiName(v.trim() || BRAIN_DEFAULTS.aiName);
|
|
23
23
|
setStep('autonomy');
|
|
24
24
|
} })] })), step === 'autonomy' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "4. \u0E43\u0E2B\u0E49 AI \u0E17\u0E33\u0E07\u0E32\u0E19\u0E41\u0E1A\u0E1A\u0E44\u0E2B\u0E19?" }), _jsx(Select, { options: [
|
package/dist/ui/history.js
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, appendFileSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { appHomePath, persistenceEnabled } from '../brand.js';
|
|
3
3
|
// prompt history แบบ persist ข้าม session (เลียน shell history) — เก็บที่ ~/.sanook/history
|
|
4
4
|
const HISTORY_PATH = appHomePath('history');
|
|
5
5
|
const MAX_ENTRIES = 500;
|
|
6
|
+
function historyLines() {
|
|
7
|
+
return readFileSync(HISTORY_PATH, 'utf8')
|
|
8
|
+
.split('\n')
|
|
9
|
+
.map((line) => line.trim())
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
function trimHistoryFile() {
|
|
13
|
+
const lines = historyLines();
|
|
14
|
+
if (lines.length <= MAX_ENTRIES)
|
|
15
|
+
return;
|
|
16
|
+
writeFileSync(HISTORY_PATH, `${lines.slice(-MAX_ENTRIES).join('\n')}\n`, { mode: 0o600 });
|
|
17
|
+
}
|
|
18
|
+
function lastPersistedPrompt() {
|
|
19
|
+
try {
|
|
20
|
+
return historyLines().at(-1);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function persistedPrompt(prompt) {
|
|
27
|
+
return prompt.replace(/[\r\n]+/g, ' ');
|
|
28
|
+
}
|
|
29
|
+
function samePersistedPrompt(persisted, last) {
|
|
30
|
+
return last !== undefined && persisted === persistedPrompt(last.trim());
|
|
31
|
+
}
|
|
6
32
|
/** โหลด prompt เก่า (เก่า→ใหม่) สำหรับ Up/Down navigation ใน REPL */
|
|
7
33
|
export function loadHistory() {
|
|
34
|
+
if (!persistenceEnabled())
|
|
35
|
+
return [];
|
|
8
36
|
try {
|
|
9
|
-
|
|
10
|
-
return lines.slice(-MAX_ENTRIES);
|
|
37
|
+
return historyLines().slice(-MAX_ENTRIES);
|
|
11
38
|
}
|
|
12
39
|
catch {
|
|
13
40
|
return [];
|
|
@@ -18,11 +45,16 @@ export function appendHistory(prompt, last) {
|
|
|
18
45
|
if (!persistenceEnabled())
|
|
19
46
|
return;
|
|
20
47
|
const p = prompt.trim();
|
|
21
|
-
if (!p || p
|
|
48
|
+
if (!p || p.startsWith('/'))
|
|
49
|
+
return;
|
|
50
|
+
const persisted = persistedPrompt(p);
|
|
51
|
+
if (samePersistedPrompt(persisted, last) || persisted === lastPersistedPrompt())
|
|
22
52
|
return;
|
|
23
53
|
try {
|
|
24
54
|
mkdirSync(appHomePath(), { recursive: true });
|
|
25
|
-
appendFileSync(HISTORY_PATH, `${
|
|
55
|
+
appendFileSync(HISTORY_PATH, `${persisted}\n`, { mode: 0o600 });
|
|
56
|
+
trimHistoryFile();
|
|
57
|
+
chmodSync(HISTORY_PATH, 0o600);
|
|
26
58
|
}
|
|
27
59
|
catch {
|
|
28
60
|
/* เขียนไม่ได้ = ไม่เป็นไร (history เป็น nice-to-have) */
|
package/dist/ui/mentions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
3
|
import { checkReadPath } from '../tools/permission.js';
|
|
4
|
+
import { resolveAgentPath } from '../tools/util.js';
|
|
4
5
|
// @-file mentions: "@path" ใน prompt → inline เนื้อหาไฟล์ (text) หรือแนบเป็น image (รูป)
|
|
5
6
|
// ลด tool round-trip (agent ไม่ต้อง read_file เอง) + เปิดทาง vision input
|
|
6
7
|
const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']);
|
|
@@ -15,7 +16,7 @@ export async function expandMentions(input) {
|
|
|
15
16
|
const errors = [];
|
|
16
17
|
const inlined = [];
|
|
17
18
|
for (const rel of [...new Set(mentions)]) {
|
|
18
|
-
const abs =
|
|
19
|
+
const abs = resolveAgentPath(rel);
|
|
19
20
|
// canonicalize ก่อนเช็ก extension → symlink ที่ชื่อไม่มีนามสกุลแต่ชี้ไปรูป ก็จับเป็น image ถูก
|
|
20
21
|
const real = await realpath(abs).catch(() => abs);
|
|
21
22
|
if (IMAGE_EXT.has(extname(real).toLowerCase())) {
|
package/dist/ui/render.js
CHANGED
|
@@ -1,32 +1,72 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { render } from 'ink';
|
|
3
4
|
import { App } from './app.js';
|
|
4
5
|
import { SetupWizard } from './setup.js';
|
|
5
6
|
import { BrainWizard } from './brain-wizard.js';
|
|
6
7
|
import { saveKey, saveGlobalConfig, saveBrainPath } from '../config.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Root — โฮสต์ setup wizard → brain wizard → REPL ใน **Ink render เดียว**
|
|
10
|
+
*
|
|
11
|
+
* ก่อนหน้านี้แยกเป็น render(SetupWizard) → unmount → render(App) = 2 Ink instances ต่อกัน
|
|
12
|
+
* พอ instance แรก unmount, stdin raw-mode/keypress listener ไม่ reattach กับ instance ที่ 2
|
|
13
|
+
* → พิมพ์ในช่องแชทไม่ได้. รวมเป็น tree เดียว (React สลับ component ภายใน) stdin ต่อเนื่องไม่หลุด.
|
|
14
|
+
*/
|
|
15
|
+
export function Root({ needsSetup, appProps }) {
|
|
16
|
+
const [phase, setPhase] = useState(needsSetup ? 'setup' : 'app');
|
|
17
|
+
const [model, setModel] = useState(appProps.initialModel);
|
|
18
|
+
const [brainNote, setBrainNote] = useState(undefined);
|
|
19
|
+
if (phase === 'setup') {
|
|
14
20
|
const onComplete = (r) => {
|
|
15
21
|
void (async () => {
|
|
16
22
|
if (r.key)
|
|
17
23
|
await saveKey(r.envVar, r.key);
|
|
18
24
|
await saveGlobalConfig({ model: r.model, provider: r.provider });
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
await startBrainSetup(); // ถาม identity + path จริง แล้ว scaffold
|
|
22
|
-
resolve(r);
|
|
25
|
+
setModel(r.model);
|
|
26
|
+
setPhase(r.createBrain ? 'brain' : 'app');
|
|
23
27
|
})();
|
|
24
28
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
return _jsx(SetupWizard, { onComplete: onComplete });
|
|
30
|
+
}
|
|
31
|
+
if (phase === 'brain') {
|
|
32
|
+
const onComplete = (a) => {
|
|
33
|
+
void (async () => {
|
|
34
|
+
const { scaffoldBrain, BRAIN_DEFAULTS, expandHome, wireBrainMcp } = await import('../brain.js');
|
|
35
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
36
|
+
const target = expandHome(a.path);
|
|
37
|
+
try {
|
|
38
|
+
const res = await scaffoldBrain(target, {
|
|
39
|
+
...BRAIN_DEFAULTS,
|
|
40
|
+
ownerName: a.ownerName,
|
|
41
|
+
aiName: a.aiName,
|
|
42
|
+
autonomy: a.autonomy,
|
|
43
|
+
today,
|
|
44
|
+
});
|
|
45
|
+
await saveBrainPath(target);
|
|
46
|
+
const wired = await wireBrainMcp(target).catch(() => 'skip');
|
|
47
|
+
setBrainNote(`✅ second-brain — ${target} · สร้าง ${res.created.length} ไฟล์ · ` +
|
|
48
|
+
`${wired === 'added' ? 'wire filesystem MCP เข้า vault แล้ว' : 'MCP เดิมอยู่แล้ว (ไม่ทับ)'} · เปิดใน Obsidian: Open folder as vault`);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
setBrainNote(`⚠ สร้าง second-brain ไม่สำเร็จ: ${e.message} — ลองใหม่ด้วย ${'`'}sanook brain init${'`'}`);
|
|
52
|
+
}
|
|
53
|
+
setPhase('app');
|
|
54
|
+
})();
|
|
55
|
+
};
|
|
56
|
+
return _jsx(BrainWizard, { onComplete: onComplete });
|
|
57
|
+
}
|
|
58
|
+
// App mount สดตอน phase = 'app' → useState(initialModel) หยิบ model ที่เลือกจาก wizard ถูกต้อง
|
|
59
|
+
return _jsx(App, { ...appProps, initialModel: model, initialNote: brainNote ?? appProps.initialNote });
|
|
60
|
+
}
|
|
61
|
+
/** เปิดแอป: wizard (ถ้า first-run) → REPL — Ink render ครั้งเดียว (fix: พิมพ์ในช่องแชทไม่ได้) */
|
|
62
|
+
export function startApp(props) {
|
|
63
|
+
render(_jsx(Root, { ...props }));
|
|
64
|
+
}
|
|
65
|
+
/** เปิด REPL ตรงๆ (ไม่ผ่าน wizard) — เก็บไว้เผื่อ caller อื่น */
|
|
66
|
+
export function startRepl(appProps) {
|
|
67
|
+
render(_jsx(App, { ...appProps }));
|
|
28
68
|
}
|
|
29
|
-
/** standalone
|
|
69
|
+
/** standalone `sanook brain init` (interactive): ถาม path + ตัวตน → scaffold + wire MCP — single render, จบแล้ว process ออก */
|
|
30
70
|
export function startBrainSetup() {
|
|
31
71
|
return new Promise((resolve) => {
|
|
32
72
|
let unmount = () => { };
|
package/dist/ui/setup.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { Select, PasswordInput } from '@inkjs/ui';
|
|
5
|
-
import { PROVIDERS, consoleUrl } from '../providers/registry.js';
|
|
5
|
+
import { PROVIDERS, consoleUrl, hasUsableEnvKey } from '../providers/registry.js';
|
|
6
|
+
import { resolveKeyFromEnv, assertDirectApiKey } from '../providers/keys.js';
|
|
6
7
|
import { listRemoteModels, mergeModelOptions } from '../providers/models.js';
|
|
8
|
+
import { detectCodex } from '../providers/codex.js';
|
|
7
9
|
import { BRAND } from '../brand.js';
|
|
8
|
-
|
|
10
|
+
// จัดลำดับ provider ในเมนู: cloud ยอดนิยม → cloud อื่น → local → ChatGPT-plan (codex) ท้ายสุด
|
|
11
|
+
const PROVIDER_ORDER = ['anthropic', 'openai', 'google', 'xai', 'mistral', 'groq', 'ollama', 'lmstudio', 'codex'];
|
|
12
|
+
/** label + hint ต่อ provider: เจอ key ใน env / local / ChatGPT-login / ต้องมี key — ให้เลือกง่ายขึ้น */
|
|
13
|
+
export function providerOption(id) {
|
|
14
|
+
const p = PROVIDERS[id];
|
|
15
|
+
let hint;
|
|
16
|
+
if (p.kind === 'delegate')
|
|
17
|
+
hint = 'login ChatGPT · ไม่ใช้ API key';
|
|
18
|
+
else if (!p.requiresKey)
|
|
19
|
+
hint = 'local · ไม่ต้อง key';
|
|
20
|
+
else if (hasUsableEnvKey(id))
|
|
21
|
+
hint = '✓ key ใน env ใช้ได้';
|
|
22
|
+
else if (resolveKeyFromEnv(p.envVar, p.envFallbacks))
|
|
23
|
+
hint = 'key ใน env ใช้ไม่ได้';
|
|
24
|
+
else
|
|
25
|
+
hint = 'ต้องมี API key';
|
|
26
|
+
return { label: `${p.label} — ${hint}`, value: p.id };
|
|
27
|
+
}
|
|
28
|
+
/** first-run setup wizard: เลือก provider → (codex login | API key) → เลือก model → เสนอสร้าง second-brain */
|
|
9
29
|
export function SetupWizard({ onComplete }) {
|
|
10
30
|
const [step, setStep] = useState('provider');
|
|
11
31
|
const [provider, setProvider] = useState('');
|
|
@@ -13,8 +33,33 @@ export function SetupWizard({ onComplete }) {
|
|
|
13
33
|
const [model, setModel] = useState('');
|
|
14
34
|
const [remote, setRemote] = useState([]);
|
|
15
35
|
const [loadingModels, setLoadingModels] = useState(false);
|
|
36
|
+
const [codexStatus, setCodexStatus] = useState(null);
|
|
37
|
+
const [recheck, setRecheck] = useState(0);
|
|
38
|
+
const [keyDraft, setKeyDraft] = useState('');
|
|
39
|
+
const [keyError, setKeyError] = useState('');
|
|
16
40
|
const cfg = provider ? PROVIDERS[provider] : undefined;
|
|
17
|
-
const providerOptions =
|
|
41
|
+
const providerOptions = PROVIDER_ORDER.filter((id) => PROVIDERS[id]).map(providerOption);
|
|
42
|
+
// codex-auth: เช็ก codex CLI ติดตั้ง + login ChatGPT (re-run เมื่อกด "เช็กใหม่")
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (step !== 'codex-auth')
|
|
45
|
+
return;
|
|
46
|
+
let alive = true;
|
|
47
|
+
setCodexStatus(null);
|
|
48
|
+
void detectCodex().then((s) => {
|
|
49
|
+
if (!alive)
|
|
50
|
+
return;
|
|
51
|
+
setCodexStatus(s);
|
|
52
|
+
if (s.installed && s.loggedIn) {
|
|
53
|
+
// login แล้ว → ใช้ default model ของ codex (ChatGPT-plan เลือก model เอง) ข้ามขั้นเลือก key/model
|
|
54
|
+
setModel(`codex:${PROVIDERS.codex.models.default}`);
|
|
55
|
+
setStep('brain-offer');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return () => {
|
|
59
|
+
alive = false;
|
|
60
|
+
};
|
|
61
|
+
}, [step, recheck]);
|
|
62
|
+
// ดึงรายชื่อ model จริงจาก provider (เฉพาะ provider แบบ SDK ที่ต้อง/ไม่ต้อง key)
|
|
18
63
|
useEffect(() => {
|
|
19
64
|
if (step !== 'model' || !cfg)
|
|
20
65
|
return;
|
|
@@ -29,13 +74,65 @@ export function SetupWizard({ onComplete }) {
|
|
|
29
74
|
}, [step, cfg, key]);
|
|
30
75
|
const modelOptions = cfg ? mergeModelOptions(cfg, remote) : [];
|
|
31
76
|
const finish = (createBrain) => onComplete({ provider, model, envVar: cfg?.envVar ?? '', key, createBrain });
|
|
32
|
-
|
|
77
|
+
const backToProvider = () => {
|
|
78
|
+
setProvider('');
|
|
79
|
+
setCodexStatus(null);
|
|
80
|
+
setKeyError('');
|
|
81
|
+
setKey('');
|
|
82
|
+
setKeyDraft('');
|
|
83
|
+
setStep('provider');
|
|
84
|
+
};
|
|
85
|
+
// Esc บนทุก step (ยกเว้น provider) = ย้อนกลับไปเลือก provider — กัน dead-end ตอนเลือกผิด
|
|
86
|
+
// หรือ codex detect ค้าง (step codex-auth ตอน pending ไม่มีปุ่มอื่น แต่ Esc ออกได้เสมอ)
|
|
87
|
+
useInput((_input, key) => {
|
|
88
|
+
if (key.return && step === 'key' && !keyDraft.trim()) {
|
|
89
|
+
setKeyError('วาง API key ก่อนค่ะ (กด Enter ทั้งที่ว่างไม่ได้) · Esc = กลับไปเลือก provider');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (key.escape && step !== 'provider')
|
|
93
|
+
backToProvider();
|
|
94
|
+
});
|
|
95
|
+
// ตรวจ API key ในขั้นใส่ key — ว่าง = ไม่ผ่าน, OAuth/format ผิด = บอก error (กัน setup จบทั้งที่ key ใช้ไม่ได้)
|
|
96
|
+
const submitKey = (raw) => {
|
|
97
|
+
const k = raw.trim();
|
|
98
|
+
if (!k) {
|
|
99
|
+
setKeyError('วาง API key ก่อนค่ะ (กด Enter ทั้งที่ว่างไม่ได้) · Esc = กลับไปเลือก provider');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (cfg) {
|
|
103
|
+
try {
|
|
104
|
+
assertDirectApiKey(cfg, k); // reject OAuth/subscription token + format ผิด (เหมือน runtime)
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
setKeyError(e.message.split('\n')[0]);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
setKeyError('');
|
|
112
|
+
setKey(k);
|
|
113
|
+
setKeyDraft(k);
|
|
114
|
+
setStep('model');
|
|
115
|
+
};
|
|
116
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["\u2699 \u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32 ", BRAND.bannerTitle, " (\u0E04\u0E23\u0E31\u0E49\u0E07\u0E41\u0E23\u0E01)"] }), step === 'provider' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "1. \u0E40\u0E25\u0E37\u0E2D\u0E01 AI provider (\u2191\u2193 \u0E40\u0E25\u0E37\u0E2D\u0E01 \u00B7 Enter \u0E22\u0E37\u0E19\u0E22\u0E31\u0E19):" }), _jsx(Text, { color: "gray", children: " cloud = \u0E43\u0E2A\u0E48 API key \u00B7 local = \u0E1F\u0E23\u0E35\u0E1A\u0E19\u0E40\u0E04\u0E23\u0E37\u0E48\u0E2D\u0E07 \u00B7 Codex = login \u0E14\u0E49\u0E27\u0E22 ChatGPT" }), _jsx(Select, { options: providerOptions, onChange: (v) => {
|
|
33
117
|
setProvider(v);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
118
|
+
const p = PROVIDERS[v];
|
|
119
|
+
if (p.kind === 'delegate')
|
|
120
|
+
setStep('codex-auth');
|
|
121
|
+
else if (p.requiresKey)
|
|
122
|
+
setStep('key');
|
|
123
|
+
else
|
|
124
|
+
setStep('model');
|
|
125
|
+
} })] })), step === 'codex-auth' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "2. \u0E40\u0E0A\u0E37\u0E48\u0E2D\u0E21 OpenAI Codex (\u0E43\u0E0A\u0E49\u0E42\u0E04\u0E27\u0E15\u0E49\u0E32 ChatGPT plan \u2014 \u0E44\u0E21\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E21\u0E35 API key):" }), codexStatus === null ? (_jsx(Text, { color: "gray", children: " \u0E01\u0E33\u0E25\u0E31\u0E07\u0E40\u0E0A\u0E47\u0E01 codex CLI + \u0E2A\u0E16\u0E32\u0E19\u0E30 login\u2026" })) : !codexStatus.installed ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: " \u274C \u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E15\u0E34\u0E14\u0E15\u0E31\u0E49\u0E07 codex CLI" }), _jsxs(Text, { children: [' ', "\u0E15\u0E34\u0E14\u0E15\u0E31\u0E49\u0E07\u0E43\u0E19 terminal \u0E2D\u0E35\u0E01\u0E2B\u0E19\u0E49\u0E32\u0E15\u0E48\u0E32\u0E07: ", _jsx(Text, { color: "cyan", children: "npm i -g @openai/codex" })] }), _jsx(Select, { options: [
|
|
126
|
+
{ label: 'เช็กใหม่ (ติดตั้งเสร็จแล้ว)', value: 'recheck' },
|
|
127
|
+
{ label: '← กลับไปเลือก provider อื่น', value: 'back' },
|
|
128
|
+
], onChange: (v) => (v === 'recheck' ? setRecheck((n) => n + 1) : backToProvider()) })] })) : !codexStatus.loggedIn ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: " \u26A0 \u0E15\u0E34\u0E14\u0E15\u0E31\u0E49\u0E07\u0E41\u0E25\u0E49\u0E27 \u0E41\u0E15\u0E48\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49 login ChatGPT" }), _jsxs(Text, { children: [' ', "\u0E23\u0E31\u0E19\u0E43\u0E19 terminal \u0E2D\u0E35\u0E01\u0E2B\u0E19\u0E49\u0E32\u0E15\u0E48\u0E32\u0E07: ", _jsx(Text, { color: "cyan", children: "codex login" }), " ", _jsx(Text, { color: "gray", children: "(\u0E40\u0E1B\u0E34\u0E14 browser \u0E43\u0E2B\u0E49\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E14\u0E49\u0E27\u0E22\u0E1A\u0E31\u0E0D\u0E0A\u0E35 ChatGPT)" })] }), _jsx(Select, { options: [
|
|
129
|
+
{ label: 'เช็กใหม่ (login เสร็จแล้ว)', value: 'recheck' },
|
|
130
|
+
{ label: '← กลับไปเลือก provider อื่น', value: 'back' },
|
|
131
|
+
], onChange: (v) => (v === 'recheck' ? setRecheck((n) => n + 1) : backToProvider()) })] })) : (_jsx(Text, { color: "green", children: " \u2705 login ChatGPT \u0E41\u0E25\u0E49\u0E27 \u2014 \u0E01\u0E33\u0E25\u0E31\u0E07\u0E44\u0E1B\u0E15\u0E48\u0E2D\u2026" }))] })), step === 'key' && cfg && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["2. \u0E27\u0E32\u0E07 API key \u0E02\u0E2D\u0E07 ", cfg.label, ": ", _jsx(Text, { color: "gray", children: "(Esc = \u0E01\u0E25\u0E31\u0E1A)" })] }), consoleUrl(provider) ? _jsxs(Text, { color: "cyan", children: [" \u2192 \u0E40\u0E2D\u0E32 key \u0E17\u0E35\u0E48: ", consoleUrl(provider)] }) : null, cfg.keyExample ? _jsxs(Text, { color: "gray", children: [" \u0E23\u0E39\u0E1B\u0E41\u0E1A\u0E1A key: ", cfg.keyExample] }) : null, _jsx(Text, { color: "gray", children: " (API key \u0E15\u0E23\u0E07\u0E08\u0E32\u0E01 console \u2014 \u0E2B\u0E49\u0E32\u0E21 OAuth/subscription token \u00B7 key \u0E08\u0E30\u0E40\u0E01\u0E47\u0E1A\u0E41\u0E1A\u0E1A\u0E40\u0E02\u0E49\u0E32\u0E23\u0E2B\u0E31\u0E2A\u0E43\u0E19\u0E40\u0E04\u0E23\u0E37\u0E48\u0E2D\u0E07)" }), _jsx(PasswordInput, { placeholder: cfg.envVar, onChange: (v) => {
|
|
132
|
+
setKeyDraft(v);
|
|
133
|
+
if (keyError)
|
|
134
|
+
setKeyError('');
|
|
135
|
+
}, onSubmit: submitKey }), keyError ? _jsxs(Text, { color: "red", children: [" \u2717 ", keyError] }) : null] })), step === 'model' &&
|
|
39
136
|
cfg &&
|
|
40
137
|
(loadingModels ? (_jsxs(Text, { color: "gray", children: [" \u0E01\u0E33\u0E25\u0E31\u0E07\u0E14\u0E36\u0E07\u0E23\u0E32\u0E22\u0E0A\u0E37\u0E48\u0E2D model \u0E08\u0E32\u0E01 ", cfg.label, "\u2026"] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["3. \u0E40\u0E25\u0E37\u0E2D\u0E01 model \u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19", remote.length ? _jsxs(Text, { color: "gray", children: [" (", modelOptions.length, " \u0E15\u0E31\u0E27\u0E08\u0E32\u0E01 provider + alias)"] }) : null, ":"] }), _jsx(Select, { options: modelOptions, onChange: (v) => {
|
|
41
138
|
setModel(`${provider}:${v}`);
|
package/dist/update.js
CHANGED
|
@@ -6,11 +6,23 @@ function packageUrl(registry, packageName) {
|
|
|
6
6
|
const encoded = encodeURIComponent(packageName).replace(/^%40/, '@');
|
|
7
7
|
return `${base}/${encoded}`;
|
|
8
8
|
}
|
|
9
|
+
function normalizeNumericIdentifier(part) {
|
|
10
|
+
return /^\d+$/.test(part) ? part.replace(/^0+/, '') || '0' : undefined;
|
|
11
|
+
}
|
|
12
|
+
function compareNumericIdentifiers(a, b) {
|
|
13
|
+
if (a === b)
|
|
14
|
+
return 0;
|
|
15
|
+
if (a.length !== b.length)
|
|
16
|
+
return a.length > b.length ? 1 : -1;
|
|
17
|
+
return a > b ? 1 : -1;
|
|
18
|
+
}
|
|
9
19
|
function splitVersion(version) {
|
|
10
20
|
const [withoutBuild] = version.trim().replace(/^v/, '').split('+');
|
|
11
|
-
const
|
|
21
|
+
const prereleaseIndex = withoutBuild.indexOf('-');
|
|
22
|
+
const corePart = prereleaseIndex === -1 ? withoutBuild : withoutBuild.slice(0, prereleaseIndex);
|
|
23
|
+
const prereleasePart = prereleaseIndex === -1 ? '' : withoutBuild.slice(prereleaseIndex + 1);
|
|
12
24
|
return {
|
|
13
|
-
core: corePart.split('.').map((part) =>
|
|
25
|
+
core: corePart.split('.').map((part) => normalizeNumericIdentifier(part) ?? '0'),
|
|
14
26
|
prerelease: prereleasePart ? prereleasePart.split('.') : [],
|
|
15
27
|
};
|
|
16
28
|
}
|
|
@@ -29,12 +41,13 @@ function comparePrerelease(a, b) {
|
|
|
29
41
|
return -1;
|
|
30
42
|
if (pb === undefined)
|
|
31
43
|
return 1;
|
|
32
|
-
const na =
|
|
33
|
-
const nb =
|
|
34
|
-
if (
|
|
35
|
-
return na
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
const na = normalizeNumericIdentifier(pa);
|
|
45
|
+
const nb = normalizeNumericIdentifier(pb);
|
|
46
|
+
if (na !== undefined && nb !== undefined && na !== nb) {
|
|
47
|
+
return compareNumericIdentifiers(na, nb);
|
|
48
|
+
}
|
|
49
|
+
if ((na !== undefined) !== (nb !== undefined))
|
|
50
|
+
return na !== undefined ? -1 : 1;
|
|
38
51
|
if (pa !== pb)
|
|
39
52
|
return pa > pb ? 1 : -1;
|
|
40
53
|
}
|
|
@@ -45,10 +58,10 @@ export function compareVersions(a, b) {
|
|
|
45
58
|
const vb = splitVersion(b);
|
|
46
59
|
const len = Math.max(va.core.length, vb.core.length, 3);
|
|
47
60
|
for (let i = 0; i < len; i++) {
|
|
48
|
-
const na = va.core[i] ?? 0;
|
|
49
|
-
const nb = vb.core[i] ?? 0;
|
|
61
|
+
const na = va.core[i] ?? '0';
|
|
62
|
+
const nb = vb.core[i] ?? '0';
|
|
50
63
|
if (na !== nb)
|
|
51
|
-
return na
|
|
64
|
+
return compareNumericIdentifiers(na, nb);
|
|
52
65
|
}
|
|
53
66
|
return comparePrerelease(va.prerelease, vb.prerelease);
|
|
54
67
|
}
|