@zhiman_innies/innies-codex 0.122.60 → 0.122.61

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.
@@ -231,7 +231,8 @@
231
231
  "pro",
232
232
  "team"
233
233
  ],
234
- "supports_reasoning_summaries": true
234
+ "supports_reasoning_summaries": true,
235
+ "hidden": true
235
236
  },
236
237
  {
237
238
  "support_verbosity": true,
@@ -305,7 +306,8 @@
305
306
  "pro",
306
307
  "team"
307
308
  ],
308
- "supports_reasoning_summaries": true
309
+ "supports_reasoning_summaries": true,
310
+ "hidden": true
309
311
  },
310
312
  {
311
313
  "support_verbosity": false,
@@ -380,7 +382,8 @@
380
382
  "pro",
381
383
  "team"
382
384
  ],
383
- "supports_reasoning_summaries": true
385
+ "supports_reasoning_summaries": true,
386
+ "hidden": true
384
387
  },
385
388
  {
386
389
  "support_verbosity": false,
@@ -448,7 +451,8 @@
448
451
  "pro",
449
452
  "team"
450
453
  ],
451
- "supports_reasoning_summaries": true
454
+ "supports_reasoning_summaries": true,
455
+ "hidden": true
452
456
  },
453
457
  {
454
458
  "support_verbosity": false,
@@ -512,7 +516,8 @@
512
516
  "pro",
513
517
  "team"
514
518
  ],
515
- "supports_reasoning_summaries": true
519
+ "supports_reasoning_summaries": true,
520
+ "hidden": true
516
521
  },
517
522
  {
518
523
  "support_verbosity": true,
@@ -580,7 +585,8 @@
580
585
  "pro",
581
586
  "team"
582
587
  ],
583
- "supports_reasoning_summaries": true
588
+ "supports_reasoning_summaries": true,
589
+ "hidden": true
584
590
  },
585
591
  {
586
592
  "support_verbosity": true,
@@ -644,7 +650,8 @@
644
650
  "pro",
645
651
  "team"
646
652
  ],
647
- "supports_reasoning_summaries": true
653
+ "supports_reasoning_summaries": true,
654
+ "hidden": true
648
655
  },
649
656
  {
650
657
  "support_verbosity": false,
@@ -708,7 +715,8 @@
708
715
  "pro",
709
716
  "team"
710
717
  ],
711
- "supports_reasoning_summaries": true
718
+ "supports_reasoning_summaries": true,
719
+ "hidden": true
712
720
  },
713
721
  {
714
722
  "support_verbosity": true,
@@ -776,7 +784,8 @@
776
784
  "pro",
777
785
  "team"
778
786
  ],
779
- "supports_reasoning_summaries": true
787
+ "supports_reasoning_summaries": true,
788
+ "hidden": true
780
789
  },
781
790
  {
782
791
  "support_verbosity": true,
@@ -836,7 +845,8 @@
836
845
  "pro",
837
846
  "team"
838
847
  ],
839
- "supports_reasoning_summaries": true
848
+ "supports_reasoning_summaries": true,
849
+ "hidden": true
840
850
  },
841
851
  {
842
852
  "support_verbosity": true,
@@ -896,7 +906,8 @@
896
906
  "pro",
897
907
  "team"
898
908
  ],
899
- "supports_reasoning_summaries": true
909
+ "supports_reasoning_summaries": true,
910
+ "hidden": true
900
911
  },
901
912
  {
902
913
  "support_verbosity": false,
@@ -956,7 +967,8 @@
956
967
  "pro",
957
968
  "team"
958
969
  ],
959
- "supports_reasoning_summaries": true
970
+ "supports_reasoning_summaries": true,
971
+ "hidden": true
960
972
  },
961
973
  {
962
974
  "support_verbosity": false,
@@ -1016,7 +1028,8 @@
1016
1028
  "pro",
1017
1029
  "team"
1018
1030
  ],
1019
- "supports_reasoning_summaries": true
1031
+ "supports_reasoning_summaries": true,
1032
+ "hidden": true
1020
1033
  }
1021
1034
  ]
1022
1035
  }
@@ -61,6 +61,15 @@ export function resolveInniesHome() {
61
61
  }
62
62
 
63
63
  export function ensureInniesHomeDefaults(homeDir) {
64
+ // N20 (2026-06-15): self-create the home dir so the library
65
+ // contract is self-contained. Previously `innies.js:55` did the
66
+ // `fs.mkdirSync` before calling us; that meant any other caller
67
+ // (a script that `import`s this module, a unit test, a future
68
+ // sub-command) had to remember the same dance or hit ENOENT on
69
+ // the very first `writeFileSync` of catalog.json. The cost is
70
+ // one extra `mkdir` on the hot path; the benefit is the API
71
+ // matches the contract advertised in the JSDoc.
72
+ fs.mkdirSync(homeDir, { recursive: true });
64
73
  const state = loadInniesState(homeDir);
65
74
  const catalogPath = path.join(homeDir, DEFAULT_CATALOG_FILENAME);
66
75
  ensureInniesCatalog(catalogPath);
@@ -372,6 +381,60 @@ function defaultInniesConfig(catalogPath, managedDefault) {
372
381
  ].join("\n");
373
382
  }
374
383
 
384
+ // N20 (2026-06-15): detect-and-peel the FIRST-RUN warning header so
385
+ // `normalizeInniesConfig` can re-emit it at the very top, matching
386
+ // `defaultInniesConfig`'s byte ordering. Without this, init 1 (the
387
+ // fresh `defaultInniesConfig` output) places the header at the top
388
+ // and the managed lines below it; on init 2+ the managed lines are
389
+ // PREPENDED (because `stripManagedRootSettings` preserves comment
390
+ // lines verbatim) and the header drifts to position 2 — producing a
391
+ // stable-but-different md5 every other round-trip. Detecting the
392
+ // header by its leading magic line and peeling the full block off
393
+ // the unmanaged tail closes the gap: init 1 and init 2+ are now
394
+ // bytewise-equal whenever the user has not edited the header.
395
+ //
396
+ // The header is owned by the code (not the user) so re-emitting it
397
+ // unconditionally is correct; the only case where we DON'T emit it
398
+ // is when the source file does not have it (e.g. a user on a
399
+ // pre-N20 install who upgrades — their file lacks the header and we
400
+ // respect that).
401
+ const FIRST_RUN_HEADER_MAGIC = "# ⚠️ FIRST-RUN SETUP REQUIRED (N20 / 2026-06-15)";
402
+ const FIRST_RUN_HEADER_OPEN = "# ============================================================================";
403
+
404
+ function peelFirstRunHeader(unmanagedBody) {
405
+ const lines = unmanagedBody.split(/\r?\n/);
406
+ // The header always opens with `# ==========...` (line 0) and has
407
+ // the magic marker on line 1. We scan forward for the closing
408
+ // `# ==========...` (line N) which terminates the block. The
409
+ // magic-line check makes the detection robust to a user
410
+ // prepending their own comment block (which would shift the
411
+ // header's position); finding the closing delimiter makes it
412
+ // robust to future edits that add/remove filler lines.
413
+ if (lines.length < 3) {
414
+ return { header: null, body: unmanagedBody };
415
+ }
416
+ if (lines[0] !== FIRST_RUN_HEADER_OPEN) {
417
+ return { header: null, body: unmanagedBody };
418
+ }
419
+ if (!lines[1] || !lines[1].includes(FIRST_RUN_HEADER_MAGIC)) {
420
+ return { header: null, body: unmanagedBody };
421
+ }
422
+ // Find closing delimiter.
423
+ let closeIdx = -1;
424
+ for (let i = 2; i < lines.length; i++) {
425
+ if (lines[i] === FIRST_RUN_HEADER_OPEN) {
426
+ closeIdx = i;
427
+ break;
428
+ }
429
+ }
430
+ if (closeIdx < 0) {
431
+ return { header: null, body: unmanagedBody };
432
+ }
433
+ const header = lines.slice(0, closeIdx + 1).join("\n");
434
+ const body = lines.slice(closeIdx + 1).join("\n").replace(/^\n+/, "");
435
+ return { header, body };
436
+ }
437
+
375
438
  function normalizeInniesConfig(contents, catalogPath, state) {
376
439
  if (contents.trim() === "") {
377
440
  return defaultInniesConfig(catalogPath, managedDefaultModel());
@@ -382,15 +445,26 @@ function normalizeInniesConfig(contents, catalogPath, state) {
382
445
  const managedDefault = isUserSelected ? null : managedDefaultModel();
383
446
  const managedSettings = ROOT_MANAGED_SETTINGS;
384
447
  const unmanagedContents = stripManagedRootSettings(contents, managedSettings).trim();
448
+ // N20 (2026-06-15): peel the FIRST-RUN warning header off the
449
+ // unmanaged tail so we can re-emit it BEFORE the managed lines
450
+ // (matching `defaultInniesConfig`'s byte ordering). See
451
+ // `peelFirstRunHeader` for the detection rationale.
452
+ const { header: firstRunHeader, body: unmanagedBody } =
453
+ peelFirstRunHeader(unmanagedContents);
385
454
  const managedLines = isUserSelected
386
455
  ? preservedUserManagedLines(contents, catalogPath)
387
456
  : managedDefault
388
457
  ? managedDefaultLines(catalogPath, managedDefault)
389
458
  : catalogOnlyManagedLines(catalogPath);
390
- let updated = `${managedLines.join("\n")}\n`;
459
+ let updated = "";
460
+ if (firstRunHeader) {
461
+ updated = `${firstRunHeader}\n\n${managedLines.join("\n")}\n`;
462
+ } else {
463
+ updated = `${managedLines.join("\n")}\n`;
464
+ }
391
465
 
392
- if (unmanagedContents !== "") {
393
- updated = `${updated}\n${unmanagedContents}\n`;
466
+ if (unmanagedBody !== "") {
467
+ updated = `${updated}\n${unmanagedBody}\n`;
394
468
  } else {
395
469
  updated = `${updated}\n`;
396
470
  }
@@ -688,6 +762,19 @@ export function _enforceDashscopeEnvKeyForTest(contents) {
688
762
  return enforceDashscopeEnvKey(contents);
689
763
  }
690
764
 
765
+ // N20 (2026-06-15): exported for unit testing in
766
+ // scripts/test_normalize_byte_stable.cjs. Asserts that
767
+ // normalizeInniesConfig re-emits the FIRST-RUN warning header at
768
+ // the top of the file (matching defaultInniesConfig's byte
769
+ // ordering) so init 1 → init 2 produces a bytewise-equal config.
770
+ export function _normalizeInniesConfigForTest(contents, catalogPath, state) {
771
+ return normalizeInniesConfig(contents, catalogPath, state);
772
+ }
773
+
774
+ export function _peelFirstRunHeaderForTest(unmanagedBody) {
775
+ return peelFirstRunHeader(unmanagedBody);
776
+ }
777
+
691
778
  function normalizeProviderBlock(contents, provider) {
692
779
  const lines = contents.split(/\r?\n/);
693
780
  const updated = [];
package/bin/innies.js CHANGED
@@ -49,6 +49,24 @@ if (isVersionRequest(process.argv.slice(2))) {
49
49
  process.exit(0);
50
50
  }
51
51
 
52
+ // N20 fix Phase 3 (2026-06-15): parse `--model X` from argv at the
53
+ // top level so it's available before any `await` runs. Mirrors the
54
+ // slug -> provider_key mapping in `codex-rs/exec/src/lib.rs:387-392`
55
+ // (the headless-exec ingress that `--model` actually controls). We
56
+ // need this to scope the onboarding placeholder detection to the
57
+ // provider block the user *intends* to use — without it, onboarding
58
+ // warns about every unfilled block including ones the user never
59
+ // picked, which is the G1-NEW-1/4 root cause.
60
+ function parseModelArgFromArgv(argv) {
61
+ for (let i = 0; i < argv.length - 1; i++) {
62
+ if (argv[i] === "--model" || argv[i] === "-m") {
63
+ return argv[i + 1] ?? null;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ const MODEL_CLI_ARG = parseModelArgFromArgv(process.argv.slice(2));
69
+
52
70
  const codexHome = resolveInniesHome();
53
71
  process.env[INNIES_HOME_ENV_VAR] = codexHome;
54
72
  process.env.CODEX_HOME = codexHome;
@@ -158,10 +176,72 @@ function writeExternalRuntimeSmokeResult(runtimeVersion) {
158
176
  // warning — otherwise we'd block on readline forever. The
159
177
  // detection itself is purely synchronous and never throws.
160
178
 
161
- function detectPlaceholders(configPath) {
179
+ // N20 fix Phase 3 (2026-06-15): read the top-of-file `model_provider = "..."`
180
+ // pin (the value written by `defaultInniesConfig` in innies-config.js).
181
+ // We deliberately match the FIRST non-commented, non-section occurrence —
182
+ // the template writes `model_provider = "<DEFAULT_PROVIDER>"` at the top
183
+ // of the file (see `managedDefaultLines` and the `preservedModelProvider`
184
+ // fallback path in innies-config.js:514-525).
185
+ function resolveTopPinnedProvider(configPath) {
186
+ if (!fs.existsSync(configPath)) {
187
+ return null;
188
+ }
189
+ const lines = fs.readFileSync(configPath, "utf8").split(/\r?\n/);
190
+ for (const line of lines) {
191
+ const trimmed = line.trim();
192
+ if (trimmed.startsWith("#") || trimmed.startsWith("[")) {
193
+ continue;
194
+ }
195
+ const m = trimmed.match(/^model_provider\s*=\s*"([^"]+)"\s*$/);
196
+ if (m) {
197
+ return m[1];
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+
203
+ // N20 fix Phase 3 (2026-06-15): decide which provider block the user
204
+ // is *actually* about to dispatch to. Two inputs:
205
+ // 1. `--model X` from argv (wins if present — overrides the pin,
206
+ // matches `codex-rs/exec/src/lib.rs:387-392`)
207
+ // 2. top-of-file `model_provider = "..."` pin (the user's default)
208
+ //
209
+ // We apply the same slug -> provider_key static mapping that the
210
+ // Rust `--model` resolver uses so the two layers agree. Unknown
211
+ // slugs fall through to the top-of-file pin (the trust-the-pin
212
+ // path; the user is doing something custom).
213
+ function resolveSelectedProviderKey(configPath, modelCliArg) {
214
+ const topPin = resolveTopPinnedProvider(configPath);
215
+ if (!modelCliArg) {
216
+ return topPin;
217
+ }
218
+ const slugToKey = {
219
+ qwen35_35b: "zhiman_35b",
220
+ qwen36_27b: "zhiman_27b",
221
+ "qwen3.6-27b": "dashscope",
222
+ "qwen3-6-27b": "dashscope",
223
+ };
224
+ const mapped = Object.prototype.hasOwnProperty.call(slugToKey, modelCliArg)
225
+ ? slugToKey[modelCliArg]
226
+ : null;
227
+ // If we recognize the slug, return the mapped key. Otherwise trust
228
+ // the user's top-of-file pin (they may have a custom slug that
229
+ // happens to match a block name).
230
+ return mapped ?? topPin;
231
+ }
232
+
233
+ function detectPlaceholders(configPath, selectedKey) {
162
234
  if (!fs.existsSync(configPath)) {
163
235
  return [];
164
236
  }
237
+ // N20 fix Phase 3 (2026-06-15): when we cannot determine which
238
+ // block the user is about to dispatch to, return [] (conservative
239
+ // no-op). The user can still hit the N20 path inside the binary
240
+ // if their selection happens to be the unfilled one — but we
241
+ // don't want to spam warnings for blocks they're not using.
242
+ if (selectedKey === null || selectedKey === undefined) {
243
+ return [];
244
+ }
165
245
  const lines = fs.readFileSync(configPath, "utf8").split(/\r?\n/);
166
246
  const placeholders = [];
167
247
  let currentProviderHeader = null;
@@ -179,6 +259,13 @@ function detectPlaceholders(configPath) {
179
259
  const commentedBaseUrl = line.match(/^(\s*)#\s*base_url\s*=\s*"([^"]+)"\s*(#.*)?$/);
180
260
  if (commentedBaseUrl) {
181
261
  const [, indent, value, trailingComment] = commentedBaseUrl;
262
+ // N20 fix Phase 3 (2026-06-15): only flag placeholders inside
263
+ // the block the user actually selected. This is the G1-NEW-1
264
+ // root cause: previously, ALL unfilled blocks were warned
265
+ // about, including ones the user pinned-but-isn't-using.
266
+ if (currentProviderHeader !== selectedKey) {
267
+ continue;
268
+ }
182
269
  if (Object.prototype.hasOwnProperty.call(KNOWN_BASE_URL_PLACEHOLDERS, value)) {
183
270
  placeholders.push({
184
271
  lineNo: i,
@@ -207,11 +294,15 @@ async function promptOnce(rl, question) {
207
294
 
208
295
  async function maybeRunOnboardingFlow(codexHome) {
209
296
  const configPath = path.join(codexHome, "config.toml");
210
- const placeholders = detectPlaceholders(configPath);
297
+ // N20 fix Phase 3 (2026-06-15): narrow detection to the block the
298
+ // user is actually about to dispatch to. G1-NEW-1/4 root cause.
299
+ const selectedKey = resolveSelectedProviderKey(configPath, MODEL_CLI_ARG);
300
+ const placeholders = detectPlaceholders(configPath, selectedKey);
211
301
 
212
302
  if (placeholders.length === 0) {
213
- // No unfilled placeholders user already configured (or is on a
214
- // fresh install with env vars set). Nothing to do.
303
+ // No unfilled placeholders in the selected block user already
304
+ // configured it (or is on a fresh install with env vars set for
305
+ // exactly this block). Nothing to do.
215
306
  return { prompted: false, filled: 0 };
216
307
  }
217
308
 
@@ -225,6 +316,7 @@ async function maybeRunOnboardingFlow(codexHome) {
225
316
  console.warn(
226
317
  "[innies-onboarding] 检测到 config.toml 有未填的 base_url 占位符 " +
227
318
  "(N20 fresh-install 阻断)。" +
319
+ " (已自动识别选中 provider 块: \"" + selectedKey + "\" — 只检查此块)" +
228
320
  " stdin 不是 TTY,无法交互式引导 — 继续执行(可能 N20 fail)。" +
229
321
  " 三种解决方式: (1) 设置 INNIES_SKIP_ONBOARDING=1 显式跳过;" +
230
322
  " (2) 编辑 " + configPath + " 填值;" +
@@ -242,6 +334,7 @@ async function maybeRunOnboardingFlow(codexHome) {
242
334
  console.log("[innies-onboarding] 首次运行检测 (N20 fresh-install 阻断防护)");
243
335
  console.log("-----------------------------------------------------------------------------");
244
336
  console.log(`config.toml: ${configPath}`);
337
+ console.log(`(已自动识别选中 provider 块: "${selectedKey}" — 只检查此块)`);
245
338
  console.log("");
246
339
  console.log("检测到以下 provider 块的 base_url 仍是占位符(注释状态):");
247
340
  for (const p of placeholders) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhiman_innies/innies-codex",
3
- "version": "0.122.60",
3
+ "version": "0.122.61",
4
4
  "license": "Apache-2.0",
5
5
  "bin": {
6
6
  "innies": "bin/innies.js"
@@ -23,9 +23,9 @@
23
23
  "postinstall": "node bin/innies-init.js"
24
24
  },
25
25
  "optionalDependencies": {
26
- "@zhiman_innies/innies-codex-darwin-x64": "0.122.60-darwin-x64",
27
- "@zhiman_innies/innies-codex-darwin-arm64": "0.122.60-darwin-arm64",
28
- "@zhiman_innies/innies-codex-win32-x64": "0.122.60-win32-x64",
29
- "@zhiman_innies/innies-codex-win32-arm64": "0.122.60-win32-arm64"
26
+ "@zhiman_innies/innies-codex-darwin-x64": "0.122.61-darwin-x64",
27
+ "@zhiman_innies/innies-codex-darwin-arm64": "0.122.61-darwin-arm64",
28
+ "@zhiman_innies/innies-codex-win32-x64": "0.122.61-win32-x64",
29
+ "@zhiman_innies/innies-codex-win32-arm64": "0.122.61-win32-arm64"
30
30
  }
31
31
  }