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.
Files changed (146) hide show
  1. package/.env.example +161 -3
  2. package/CHANGELOG.md +83 -5
  3. package/README.md +240 -23
  4. package/README.th.md +87 -6
  5. package/dist/approval.js +6 -0
  6. package/dist/bin.js +3045 -210
  7. package/dist/brain-context.js +223 -0
  8. package/dist/brain-doctor.js +318 -0
  9. package/dist/brain-eval.js +186 -0
  10. package/dist/brain-final.js +371 -0
  11. package/dist/brain-review.js +382 -0
  12. package/dist/brain.js +12 -1
  13. package/dist/brand.js +1 -1
  14. package/dist/cli-args.js +152 -0
  15. package/dist/cli-option-values.js +16 -0
  16. package/dist/commands.js +172 -13
  17. package/dist/compaction.js +96 -11
  18. package/dist/config.js +118 -28
  19. package/dist/context-compression.js +191 -0
  20. package/dist/cost.js +49 -15
  21. package/dist/first-run.js +21 -0
  22. package/dist/gateway/auth.js +37 -8
  23. package/dist/gateway/bluebubbles.js +205 -0
  24. package/dist/gateway/config.js +929 -0
  25. package/dist/gateway/deliver.js +357 -0
  26. package/dist/gateway/discord.js +124 -0
  27. package/dist/gateway/email.js +472 -0
  28. package/dist/gateway/googlechat.js +207 -0
  29. package/dist/gateway/homeassistant.js +256 -0
  30. package/dist/gateway/ledger.js +18 -0
  31. package/dist/gateway/line.js +171 -0
  32. package/dist/gateway/lock.js +3 -1
  33. package/dist/gateway/matrix.js +366 -0
  34. package/dist/gateway/mattermost.js +322 -0
  35. package/dist/gateway/ntfy.js +218 -0
  36. package/dist/gateway/schedule.js +31 -4
  37. package/dist/gateway/serve.js +267 -7
  38. package/dist/gateway/server.js +253 -19
  39. package/dist/gateway/service.js +224 -0
  40. package/dist/gateway/session.js +343 -0
  41. package/dist/gateway/signal.js +351 -0
  42. package/dist/gateway/slack.js +124 -0
  43. package/dist/gateway/sms.js +169 -0
  44. package/dist/gateway/targets.js +576 -0
  45. package/dist/gateway/teams.js +106 -0
  46. package/dist/gateway/telegram.js +38 -15
  47. package/dist/gateway/webhooks.js +220 -0
  48. package/dist/gateway/whatsapp.js +230 -0
  49. package/dist/hooks.js +13 -2
  50. package/dist/insights-args.js +35 -0
  51. package/dist/insights.js +86 -0
  52. package/dist/loop.js +123 -24
  53. package/dist/lsp/index.js +23 -5
  54. package/dist/mcp-registry.js +350 -0
  55. package/dist/mcp-server.js +1 -1
  56. package/dist/mcp.js +44 -6
  57. package/dist/memory.js +100 -33
  58. package/dist/orchestrate.js +49 -19
  59. package/dist/personality.js +58 -0
  60. package/dist/providers/codex.js +86 -38
  61. package/dist/providers/keys.js +1 -1
  62. package/dist/providers/models.js +22 -6
  63. package/dist/providers/registry.js +38 -49
  64. package/dist/search/chunk.js +7 -8
  65. package/dist/search/cli.js +75 -0
  66. package/dist/search/embed-store.js +3 -0
  67. package/dist/search/indexer.js +44 -1
  68. package/dist/search/store.js +23 -1
  69. package/dist/session.js +93 -7
  70. package/dist/skill-install.js +29 -12
  71. package/dist/support-dump.js +175 -0
  72. package/dist/tools/edit.js +45 -15
  73. package/dist/tools/git.js +10 -5
  74. package/dist/tools/homeassistant.js +106 -0
  75. package/dist/tools/index.js +5 -0
  76. package/dist/tools/list.js +19 -6
  77. package/dist/tools/permission.js +923 -9
  78. package/dist/tools/read.js +16 -4
  79. package/dist/tools/schedule.js +19 -3
  80. package/dist/tools/search.js +217 -13
  81. package/dist/tools/task.js +18 -7
  82. package/dist/tools/timeout.js +21 -3
  83. package/dist/trust.js +11 -1
  84. package/dist/ui/app.js +57 -11
  85. package/dist/ui/brain-wizard.js +2 -2
  86. package/dist/ui/history.js +37 -5
  87. package/dist/ui/mentions.js +3 -2
  88. package/dist/ui/render.js +55 -15
  89. package/dist/ui/setup.js +107 -10
  90. package/dist/update.js +24 -11
  91. package/dist/worktree.js +175 -4
  92. package/package.json +4 -4
  93. package/second-brain/AGENTS.md +6 -4
  94. package/second-brain/CLAUDE.md +7 -1
  95. package/second-brain/Evals/_Index.md +10 -2
  96. package/second-brain/Evals/quality-ledger.md +9 -1
  97. package/second-brain/Evals/second-brain-benchmarks.md +62 -0
  98. package/second-brain/GEMINI.md +5 -4
  99. package/second-brain/Home.md +1 -1
  100. package/second-brain/Projects/_Index.md +3 -1
  101. package/second-brain/Projects/sanook-cli/_Index.md +26 -0
  102. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -0
  103. package/second-brain/README.md +1 -1
  104. package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
  105. package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
  106. package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
  107. package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
  108. package/second-brain/Research/_Index.md +6 -1
  109. package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
  110. package/second-brain/Reviews/_Index.md +1 -1
  111. package/second-brain/Runbooks/_Index.md +6 -1
  112. package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
  113. package/second-brain/SANOOK.md +45 -0
  114. package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
  115. package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
  116. package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
  117. package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
  118. package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
  119. package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
  120. package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
  121. package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
  122. package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
  123. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
  124. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
  125. package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
  126. package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
  127. package/second-brain/Sessions/_Index.md +15 -1
  128. package/second-brain/Shared/AI-Context-Index.md +22 -0
  129. package/second-brain/Shared/Context-Packs/_Index.md +9 -1
  130. package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
  131. package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
  132. package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
  133. package/second-brain/Shared/Operating-State/current-state.md +22 -3
  134. package/second-brain/Shared/Scripts/_Index.md +3 -1
  135. package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
  136. package/second-brain/Shared/Tech-Standards/_Index.md +4 -1
  137. package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
  138. package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
  139. package/second-brain/Shared/User-Memory/_Index.md +4 -1
  140. package/second-brain/Shared/User-Memory/response-examples.md +98 -0
  141. package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
  142. package/second-brain/Templates/_Index.md +9 -0
  143. package/second-brain/Templates/final-lite.md +111 -0
  144. package/second-brain/Templates/final.md +231 -0
  145. package/second-brain/Vault Structure Map.md +2 -1
  146. 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, useMemo } from 'react';
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(initialHistory?.length
24
- ? [{ id: -1, role: 'system', text: `↻ ต่อจาก session ก่อน (${initialHistory.length} ข้อความ)` }]
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: [banner, _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}` : ''] })] }));
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 }) {
@@ -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: [_jsx(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 = \u0E02\u0E49\u0E32\u0E21)" }), _jsx(TextInput, { defaultValue: BRAIN_DEFAULTS.ownerName, onSubmit: (v) => {
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: [_jsx(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?" }), _jsx(TextInput, { defaultValue: BRAIN_DEFAULTS.aiName, onSubmit: (v) => {
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: [
@@ -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
- const lines = readFileSync(HISTORY_PATH, 'utf8').split('\n').filter(Boolean);
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 === last)
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, `${p.replace(/\n/g, ' ')}\n`, { mode: 0o600 });
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) */
@@ -1,6 +1,7 @@
1
1
  import { readFile, realpath } from 'node:fs/promises';
2
- import { resolve, extname } from 'node:path';
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 = resolve(rel);
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
- export function startRepl(props) {
8
- render(_jsx(App, { ...props }));
9
- }
10
- /** render first-run wizardsave key+config → (ถ้าเลือก) ต่อ BrainWizard สร้าง second-brain → resolve */
11
- export function startSetup() {
12
- return new Promise((resolve) => {
13
- let unmount = () => { };
8
+ /**
9
+ * Root — โฮสต์ setup wizard → brain wizard → REPL ใน **Ink render เดียว**
10
+ *
11
+ * ก่อนหน้านี้แยกเป็น render(SetupWizard)unmountrender(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
- unmount();
20
- if (r.createBrain)
21
- await startBrainSetup(); // ถาม identity + path จริง แล้ว scaffold
22
- resolve(r);
25
+ setModel(r.model);
26
+ setPhase(r.createBrain ? 'brain' : 'app');
23
27
  })();
24
28
  };
25
- const instance = render(_jsx(SetupWizard, { onComplete: onComplete }));
26
- unmount = instance.unmount;
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 / first-run brain: ถาม path + ตัวตน → scaffold (personalized) + auto-wire filesystem MCP */
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
- /** first-run setup wizard: เลือก providerใส่ API key เลือก model เสนอสร้าง second-brain */
10
+ // จัดลำดับ provider ในเมนู: cloud ยอดนิยมcloud อื่นlocalChatGPT-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 = Object.values(PROVIDERS).map((p) => ({ label: p.label, value: p.id }));
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
- 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:" }), _jsx(Select, { options: providerOptions, onChange: (v) => {
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
- setStep(PROVIDERS[v].requiresKey ? 'key' : 'model');
35
- } })] })), step === 'key' && cfg && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["2. \u0E27\u0E32\u0E07 API key \u0E02\u0E2D\u0E07 ", cfg.label, ":"] }), consoleUrl(provider) ? (_jsxs(Text, { color: "cyan", children: [" \u2192 \u0E40\u0E2D\u0E32 key \u0E17\u0E35\u0E48: ", consoleUrl(provider)] })) : null, _jsx(Text, { color: "gray", children: " (API key \u0E15\u0E23\u0E07\u0E08\u0E32\u0E01 console \u2014 \u0E2B\u0E49\u0E32\u0E21 OAuth/subscription token)" }), _jsx(PasswordInput, { placeholder: cfg.envVar, onSubmit: (v) => {
36
- setKey(v.trim());
37
- setStep('model');
38
- } })] })), step === 'model' &&
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 [corePart, prereleasePart = ''] = withoutBuild.split('-', 2);
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) => Number.parseInt(part, 10)).map((n) => (Number.isFinite(n) ? n : 0)),
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 = /^\d+$/.test(pa) ? Number(pa) : Number.NaN;
33
- const nb = /^\d+$/.test(pb) ? Number(pb) : Number.NaN;
34
- if (Number.isFinite(na) && Number.isFinite(nb) && na !== nb)
35
- return na > nb ? 1 : -1;
36
- if (Number.isFinite(na) !== Number.isFinite(nb))
37
- return Number.isFinite(na) ? -1 : 1;
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 > nb ? 1 : -1;
64
+ return compareNumericIdentifiers(na, nb);
52
65
  }
53
66
  return comparePrerelease(va.prerelease, vb.prerelease);
54
67
  }