sanook-cli 0.5.8 → 0.5.10

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.10
4
+
5
+ - Patch release (npm republish guard — use this version after 0.5.9 is already on the registry).
6
+
7
+ ## 0.5.9
8
+
9
+ ### Codex (ChatGPT plan) + install
10
+
11
+ - **Codex models** — setup, `/model` picker, and config migration now only offer ChatGPT-plan-safe models (`gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`); legacy `gpt-5-codex` / `*-codex` ids auto-migrate instead of failing at runtime.
12
+ - **Codex errors** — surface JSONL API errors from `codex exec` (not just opaque exit codes).
13
+ - **Install** — README install section, GitHub Pages deploy script, npm publish guard (`scripts/publish-npm.sh`).
14
+
3
15
  ## 0.5.8
4
16
 
5
17
  ### Self-maintaining memory + reliability & security hardening
package/dist/commands.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { canonicalSpec, consoleUrl, hasUsableEnvKey, PROVIDERS, parseSpec } from './providers/registry.js';
4
+ import { mergeModelOptions } from './providers/models.js';
5
+ import { CODEX_CHATGPT_UNSUPPORTED_MODELS } from './providers/codex.js';
4
6
  import { appHomePath, BRAND } from './brand.js';
5
7
  import { parseFrontmatter } from './skills.js';
6
8
  import { projectConfigPathIfTrusted } from './trust.js';
@@ -84,15 +86,17 @@ function modelMenu(current) {
84
86
  const { provider } = parseSpec(current);
85
87
  const cfg = PROVIDERS[provider];
86
88
  const list = cfg
87
- ? Object.entries(cfg.models)
88
- .filter(([alias]) => alias !== 'default')
89
- .map(([alias, id]) => ` ${provider}:${alias} → ${id}`)
89
+ ? mergeModelOptions(cfg)
90
+ .map((o) => ` ${provider}:${o.value} (${o.label})`)
90
91
  .join('\n')
91
92
  : '';
93
+ const codexNote = provider === 'codex'
94
+ ? '\nChatGPT plan: ใช้ได้ gpt-5.5 · gpt-5.4 · gpt-5.4-mini (โมเดล *-codex เก่าไม่รองรับ)'
95
+ : '';
92
96
  return [
93
97
  `model ปัจจุบัน: ${current}`,
94
- cfg ? `\nเลือกของ ${cfg.label}:\n${list}` : '',
95
- `\nเปลี่ยน: /model <spec> (เช่น /model sonnet, /model openai:gpt-5.5)`,
98
+ cfg ? `\nเลือกของ ${cfg.label}:\n${list}${codexNote}` : '',
99
+ `\nเปลี่ยน: /model <spec> (เช่น /model sonnet, /model codex:5.5)`,
96
100
  `provider อื่น: ${Object.keys(PROVIDERS).join(' · ')}`,
97
101
  ]
98
102
  .filter(Boolean)
@@ -147,11 +151,15 @@ function modelChange(spec) {
147
151
  message: `model spec ไม่ครบ: "${spec}" — ใช้ /model <alias> หรือ /model <provider:model>`,
148
152
  };
149
153
  }
154
+ const rawModel = spec.includes(':') ? spec.slice(spec.indexOf(':') + 1).trim() : '';
155
+ const migratedNote = provider === 'codex' && rawModel && CODEX_CHATGPT_UNSUPPORTED_MODELS.has(rawModel)
156
+ ? `\n⚠ โมเดล ${rawModel} ไม่รองรับ ChatGPT plan → ใช้ ${canonical} แทน`
157
+ : '';
150
158
  const hint = missingKeyHint(provider);
151
159
  return {
152
160
  handled: true,
153
161
  modelChange: canonical,
154
- message: [`เปลี่ยน model → ${canonical}`, hint].filter(Boolean).join('\n'),
162
+ message: [`เปลี่ยน model → ${canonical}`, migratedNote, hint].filter(Boolean).join('\n'),
155
163
  };
156
164
  }
157
165
  /** parse input — ถ้าขึ้นต้น / = slash command, ไม่งั้น handled=false (ส่งเข้า agent) */
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { appHomePath, appProjectPath, BRAND } from './brand.js';
5
5
  import { projectRoot, projectTrustStatus } from './trust.js';
6
6
  import { registerPricing } from './cost.js';
7
+ import { migrateDeprecatedCodexModel } from './providers/codex.js';
7
8
  export function configHomeDir() {
8
9
  return appHomePath();
9
10
  }
@@ -180,6 +181,11 @@ export async function loadConfig(overrides = {}, cwd = process.cwd()) {
180
181
  }
181
182
  const merged = { ...global, ...project, ...envConfig, ...cleanOverrides };
182
183
  const config = parseConfigGraceful(merged);
184
+ const migratedModel = migrateDeprecatedCodexModel(config.model);
185
+ if (migratedModel !== config.model) {
186
+ config.model = migratedModel;
187
+ void saveGlobalConfig({ model: migratedModel }).catch(() => { });
188
+ }
183
189
  // pricing override: config.pricing + env SANOOK_PRICING (JSON) → ลงทะเบียนเข้า cost table
184
190
  registerPricing(config.pricing);
185
191
  registerPricing(parseEnvPricing());
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',
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: model === PROVIDERS.codex.models.default ? undefined : model,
189
+ model: execModel,
181
190
  sandbox,
182
191
  cwd: opts.cwd, // worktree isolation ของ sub-agent
183
192
  signal: opts.signal,
@@ -1,4 +1,5 @@
1
1
  import { canonicalSpec, hasUsableEnvKey, PROVIDERS, parseSpec } from './providers/registry.js';
2
+ import { isCodexChatGptSupportedModel } from './providers/codex.js';
2
3
  function statusFor(provider) {
3
4
  const cfg = PROVIDERS[provider];
4
5
  if (cfg.kind === 'delegate')
@@ -22,7 +23,9 @@ export function modelPickerOptions(current) {
22
23
  grouped.set(model, aliases);
23
24
  }
24
25
  const status = statusFor(provider);
25
- return [...grouped.entries()].map(([model, aliases]) => {
26
+ return [...grouped.entries()]
27
+ .filter(([model]) => provider !== 'codex' || isCodexChatGptSupportedModel(model))
28
+ .map(([model, aliases]) => {
26
29
  const nonDefaultAliases = aliases.filter((alias) => alias !== 'default');
27
30
  const displayAliases = nonDefaultAliases.length ? nonDefaultAliases.join('/') : 'default';
28
31
  const spec = `${provider}:${model}`;
@@ -47,6 +47,69 @@ export async function detectCodex() {
47
47
  }
48
48
  return { installed: true, loggedIn: false, reason: 'ยังไม่ได้ login — รัน: codex login' };
49
49
  }
50
+ /** Models rejected by official `codex exec` when auth is ChatGPT plan (not OpenAI API key). */
51
+ export const CODEX_CHATGPT_UNSUPPORTED_MODELS = new Set([
52
+ 'gpt-5-codex',
53
+ 'gpt-5.2-codex',
54
+ 'gpt-5.3-codex',
55
+ 'gpt-5.3-codex-spark',
56
+ ]);
57
+ /** Models verified to work with ChatGPT-plan auth via `codex exec` (Jun 2026). */
58
+ export const CODEX_CHATGPT_SUPPORTED_MODELS = ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini'];
59
+ /** Curated aliases for setup + /model picker — only ChatGPT-plan-safe ids. */
60
+ export const CODEX_CHATGPT_MODEL_ALIASES = {
61
+ default: 'gpt-5.5',
62
+ codex: 'gpt-5.5',
63
+ smart: 'gpt-5.5',
64
+ '5.5': 'gpt-5.5',
65
+ '5.4': 'gpt-5.4',
66
+ '5.4-mini': 'gpt-5.4-mini',
67
+ fast: 'gpt-5.4-mini',
68
+ };
69
+ export function isCodexChatGptSupportedModel(model) {
70
+ return CODEX_CHATGPT_SUPPORTED_MODELS.includes(model.trim());
71
+ }
72
+ /** Migrate saved config specs that still point at deprecated Codex CLI model ids. */
73
+ export function migrateDeprecatedCodexModel(spec) {
74
+ const idx = spec.indexOf(':');
75
+ if (idx === -1)
76
+ return spec;
77
+ const provider = spec.slice(0, idx);
78
+ if (provider !== 'codex')
79
+ return spec;
80
+ const { model, migratedFrom } = normalizeCodexChatGptModel(spec.slice(idx + 1));
81
+ return migratedFrom ? `${provider}:${model}` : spec;
82
+ }
83
+ /** Map legacy/unsupported Codex CLI model ids to a ChatGPT-plan-safe default. */
84
+ export function normalizeCodexChatGptModel(model) {
85
+ const trimmed = model.trim();
86
+ if (!trimmed || !CODEX_CHATGPT_UNSUPPORTED_MODELS.has(trimmed)) {
87
+ return { model: trimmed };
88
+ }
89
+ return { model: 'gpt-5.5', migratedFrom: trimmed };
90
+ }
91
+ function extractCodexErrorMessage(raw) {
92
+ const t = raw.trim();
93
+ if (!t)
94
+ return '';
95
+ try {
96
+ const outer = JSON.parse(t);
97
+ const nested = typeof outer.message === 'string' ? outer.message : '';
98
+ if (nested.startsWith('{')) {
99
+ try {
100
+ const inner = JSON.parse(nested);
101
+ return inner.error?.message ?? inner.message ?? nested;
102
+ }
103
+ catch {
104
+ return nested;
105
+ }
106
+ }
107
+ return outer.error?.message ?? outer.message ?? t;
108
+ }
109
+ catch {
110
+ return t;
111
+ }
112
+ }
50
113
  /**
51
114
  * รัน `codex exec` แบบ non-interactive — ส่ง prompt ทาง stdin, parse JSONL events
52
115
  * tolerant ต่อ malformed JSONL (codex bug #15451: --json ถูก ignore เมื่อมี tools active)
@@ -69,6 +132,7 @@ export async function runCodex(opts) {
69
132
  let threadId;
70
133
  let buf = '';
71
134
  let stderr = '';
135
+ let jsonlError = '';
72
136
  let aborted = false;
73
137
  const handleStdoutLine = (line) => {
74
138
  const t = line.trim();
@@ -93,6 +157,13 @@ export async function runCodex(opts) {
93
157
  else if (ev.type === 'turn.completed') {
94
158
  opts.onEvent?.({ type: 'usage', usage: ev.usage });
95
159
  }
160
+ else if (ev.type === 'error' || ev.type === 'turn.failed') {
161
+ const msg = extractCodexErrorMessage(ev.message ?? ev.error?.message ?? t);
162
+ if (msg) {
163
+ jsonlError = msg;
164
+ opts.onEvent?.({ type: 'error', message: msg });
165
+ }
166
+ }
96
167
  }
97
168
  catch {
98
169
  // malformed JSON line — ข้าม
@@ -139,8 +210,10 @@ export async function runCodex(opts) {
139
210
  reject(new Error(`codex exec ถูกยกเลิก${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
140
211
  else if (code === 0)
141
212
  resolve({ text: finalText.trim(), threadId });
142
- else
143
- reject(new Error(`codex exec จบด้วย exit code ${code}${stderr.trim() ? `: ${stderr.trim()}` : ''}`));
213
+ else {
214
+ const detail = jsonlError.trim() || stderr.trim();
215
+ reject(new Error(`codex exec จบด้วย exit code ${code}${detail ? `: ${detail}` : ''}`));
216
+ }
144
217
  });
145
218
  });
146
219
  }
@@ -1,3 +1,15 @@
1
+ import { isCodexChatGptSupportedModel, normalizeCodexChatGptModel } from './codex.js';
2
+ function curatedModels(cfg) {
3
+ if (cfg.id !== 'codex')
4
+ return cfg.models;
5
+ const out = {};
6
+ for (const [alias, id] of Object.entries(cfg.models)) {
7
+ const model = normalizeCodexChatGptModel(id).model;
8
+ if (isCodexChatGptSupportedModel(model))
9
+ out[alias] = model;
10
+ }
11
+ return out;
12
+ }
1
13
  /**
2
14
  * ดึงรายชื่อ model จริงจาก provider (GET /models) — "เลือกโมเดลที่เจ้าของมี" แบบ Hermes
3
15
  * - provider เป็นคน authoritative เรื่อง id (เราไม่ต้อง hardcode/เดา id ที่อาจ stale)
@@ -48,11 +60,12 @@ export async function listRemoteModels(cfg, key, timeoutMs = 6000) {
48
60
  * → ตัวเลือกโผล่ซ้ำ/หาย (bug "มีตัวเลือกสองตัวเลือกเป็น model เดียวกัน"). ใช้ทั้ง setup wizard และ /model picker
49
61
  */
50
62
  export function mergeModelOptions(cfg, remote = []) {
63
+ const models = curatedModels(cfg);
51
64
  // group alias ทั้งหมดตาม id (รวม 'default' ด้วย — กัน id ที่มีแต่ alias 'default' เช่น lmstudio:local-model,
52
65
  // ollama:llama3.3 หายไปจนเลือกไม่ได้/Select ว่าง). ตอนทำ label ค่อยซ่อนคำ "default" ถ้ามีชื่ออื่นอยู่แล้ว
53
66
  const aliasesById = new Map();
54
67
  const order = []; // คง first-seen order ของ id
55
- for (const [alias, id] of Object.entries(cfg.models)) {
68
+ for (const [alias, id] of Object.entries(models)) {
56
69
  if (!aliasesById.has(id)) {
57
70
  aliasesById.set(id, []);
58
71
  order.push(id);
@@ -66,6 +79,8 @@ export function mergeModelOptions(cfg, remote = []) {
66
79
  return { id, label: `${shown.join(' / ')} — ${id}` };
67
80
  });
68
81
  const seen = new Set(order);
69
- const extra = [...new Set(remote)].filter((id) => id && !seen.has(id)).map((id) => ({ id, label: id }));
82
+ const extra = cfg.id === 'codex'
83
+ ? []
84
+ : [...new Set(remote)].filter((id) => id && !seen.has(id)).map((id) => ({ id, label: id }));
70
85
  return [...curated, ...extra].map((o) => ({ label: o.label, value: o.id }));
71
86
  }
@@ -6,6 +6,7 @@ import { createMistral } from '@ai-sdk/mistral';
6
6
  import { createGroq } from '@ai-sdk/groq';
7
7
  import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
8
8
  import { resolveKeyFromEnv, assertDirectApiKey } from './keys.js';
9
+ import { CODEX_CHATGPT_MODEL_ALIASES, normalizeCodexChatGptModel } from './codex.js';
9
10
  import { BRAND } from '../brand.js';
10
11
  // ────────────────────────────────────────────────────────────────────────────
11
12
  // PROVIDER TABLE — เพิ่มค่าย = เพิ่ม 1 entry (loop/cost/keys ไม่ต้องแตะ)
@@ -134,21 +135,11 @@ export const PROVIDERS = {
134
135
  requiresKey: false,
135
136
  localPlaceholderKey: 'codex',
136
137
  keyFormat: null,
137
- models: {
138
- default: 'gpt-5.5',
139
- codex: 'gpt-5.5',
140
- '5.5': 'gpt-5.5',
141
- '5.4': 'gpt-5.4',
142
- '5.4-mini': 'gpt-5.4-mini',
143
- '5.3-codex': 'gpt-5.3-codex',
144
- '5.2-codex': 'gpt-5.2-codex',
145
- '5-codex': 'gpt-5-codex',
146
- spark: 'gpt-5.3-codex-spark',
147
- },
138
+ models: { ...CODEX_CHATGPT_MODEL_ALIASES },
148
139
  create: () => {
149
140
  throw new Error('codex เป็น delegate provider — ใช้ผ่าน codex subprocess ไม่ใช่ Vercel AI SDK');
150
141
  },
151
- note: 'ใช้ ChatGPT plan quota ผ่าน official codex CLI (ToS-safe, ไม่เก็บ credential). ต้อง codex login ก่อน',
142
+ note: 'ChatGPT plan ผ่าน codex CLI — รองรับ gpt-5.5 / gpt-5.4 / gpt-5.4-mini เท่านั้น (ต้อง codex login)',
152
143
  },
153
144
  };
154
145
  export const SUPPORTED_PROVIDERS = Object.keys(PROVIDERS);
@@ -177,7 +168,9 @@ export function parseSpec(spec) {
177
168
  const rest = spec.slice(idx + 1);
178
169
  const cfg = PROVIDERS[provider];
179
170
  // ถ้าเป็น alias ของ provider นั้น → map เป็น model id จริง, ไม่งั้นใช้ rest เป็น raw model id
180
- const model = cfg?.models[rest] ?? rest;
171
+ let model = cfg?.models[rest] ?? rest;
172
+ if (provider === 'codex')
173
+ model = normalizeCodexChatGptModel(model).model;
181
174
  return { provider, model };
182
175
  }
183
176
  const g = GLOBAL_ALIAS[spec];
package/dist/ui/setup.js CHANGED
@@ -35,11 +35,10 @@ export function SetupWizard({ onComplete }) {
35
35
  const providerOptions = setupProviderOptions();
36
36
  const providerMenuLines = setupProviderMenuLines();
37
37
  const advanceIfCodexReady = (status) => {
38
- if (!status.loggedIn)
38
+ if (!status.loggedIn || !status.installed)
39
39
  return;
40
40
  setModel(`codex:${PROVIDERS.codex.models.default}`);
41
- if (status.installed)
42
- setStep('agent');
41
+ setStep('model');
43
42
  };
44
43
  // codex-auth: เช็ก codex CLI ติดตั้ง + login ChatGPT (re-run เมื่อกด "เช็กใหม่")
45
44
  useEffect(() => {
@@ -220,7 +219,7 @@ export function SetupWizard({ onComplete }) {
220
219
  setKeyError('');
221
220
  }, onSubmit: submitKey }), keyError ? _jsxs(Text, { color: "red", children: [" \u2717 ", keyError] }) : null] })), step === 'model' &&
222
221
  cfg &&
223
- (loadingModels ? (_jsxs(Text, { color: "gray", children: [' ', m.modelLoading, " ", cfg.label, "\u2026"] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [m.stepModel, " \u2014 ", m.modelPick, remote.length ? _jsxs(Text, { color: "gray", children: [" (", modelOptions.length, ")"] }) : null, ":"] }), _jsx(Select, { options: modelOptions, onChange: (v) => {
222
+ (loadingModels ? (_jsxs(Text, { color: "gray", children: [' ', m.modelLoading, " ", cfg.label, "\u2026"] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [m.stepModel, " \u2014 ", m.modelPick, remote.length ? _jsxs(Text, { color: "gray", children: [" (", modelOptions.length, ")"] }) : null, ":"] }), provider === 'codex' ? _jsxs(Text, { color: "gray", children: [" ", m.codexModelHint] }) : null, _jsx(Select, { options: modelOptions, onChange: (v) => {
224
223
  setModel(`${provider}:${v}`);
225
224
  setStep('agent');
226
225
  } })] }))), step === 'agent' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.stepAgent }), _jsx(Text, { color: "gray", children: m.agentTitle }), _jsx(Select, { options: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanook-cli",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "A terminal AI coding agent — BYOK, 9 providers, MCP, cron gateway, skills, and git awareness. Built from scratch in TypeScript.",
5
5
  "type": "module",
6
6
  "bin": {