scenri 0.20.3 → 0.20.4

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.20.4](https://github.com/tonygorb/Scenri/compare/v0.20.3...v0.20.4) (2026-09-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **codex:** run on the Codex default when the plan lacks gpt-6-sol ([e7dc36d](https://github.com/tonygorb/Scenri/commit/e7dc36df2698e4ce22de66c8a997252af04c42f0))
9
+ * **codex:** run on the Codex default when the plan lacks gpt-6-sol, keep only drawn pictures ([7851c59](https://github.com/tonygorb/Scenri/commit/7851c5944c5e4cbfd17ef7ac12cd6068f57e84b7))
10
+ * **codex:** store only a picture the image tool drew ([4573de7](https://github.com/tonygorb/Scenri/commit/4573de7944a479b0585af1fe62ec9cd9bce470b2))
11
+ * **studio:** reload a tab whose server came back as another version ([22c5b6d](https://github.com/tonygorb/Scenri/commit/22c5b6d7f730d37e8435923feec1266fb3fe11ca))
12
+ * **studio:** reload a tab whose server came back as another version ([06629f0](https://github.com/tonygorb/Scenri/commit/06629f09dcb5b47551ae18ce2a88ed3985e3579d))
13
+
3
14
  ## [0.20.3](https://github.com/tonygorb/Scenri/compare/v0.20.2...v0.20.3) (2026-09-27)
4
15
 
5
16
 
package/dist/serve.js CHANGED
@@ -967,6 +967,9 @@ function codexFailureDetail(stderr, stdout) {
967
967
  return tailOf(marked) || tailOf(body) || tailOf(stdout.trim()) || tailOf(stderr.trim());
968
968
  }
969
969
  var MODEL_NEEDS_NEWER_CODEX = /The '([^']+)' model requires a newer version of Codex/;
970
+ var MODEL_NOT_ON_PLAN = /The '([^']+)' model is not supported when using Codex with a ChatGPT account/;
971
+ var ModelNotOnPlan = class extends Error {
972
+ };
970
973
  var BAD_KEY_401 = /incorrect api key provided|invalid_api_key|invalid api key/i;
971
974
  function tooOldForModel(version, model) {
972
975
  return `Codex CLI ${version ?? "on this computer"} is too old for the model it is set to, ${model}.`;
@@ -1020,18 +1023,33 @@ function createRunner(opts = {}) {
1020
1023
  }
1021
1024
  let knownVersion = null;
1022
1025
  let tooOldFor = null;
1026
+ const offPlan = /* @__PURE__ */ new Set();
1027
+ const withoutOffPlan = (rest) => {
1028
+ const i = rest.indexOf("-m");
1029
+ return i >= 0 && offPlan.has(rest[i + 1]) ? [...rest.slice(0, i), ...rest.slice(i + 2)] : rest;
1030
+ };
1023
1031
  const winArg = (a) => `"${a.replace(/[\r\n]+/g, " ").replace(/"/g, "'").replace(/%/g, " percent ")}"`;
1024
1032
  const spawnCodex = (exe, args, stdinOpen) => {
1025
1033
  const stdio = [stdinOpen ? "pipe" : "ignore", "pipe", "pipe"];
1026
1034
  const env = buildChildEnv(parentEnv, [...ignoreEnvKeys(), ...SIBLING_PROVIDER_KEYS]);
1027
- if (args[0] === "exec") args = ["exec", ...extensionsOffArgs(parentEnv), ...args.slice(1)];
1035
+ if (args[0] === "exec") args = ["exec", ...extensionsOffArgs(parentEnv), ...withoutOffPlan(args.slice(1))];
1028
1036
  return exe.direct ? spawnImpl(exe.command, args, { stdio, env, ...platform !== "win32" ? { detached: true } : {} }) : spawnImpl([exe.command, ...args.map(winArg)].join(" "), [], { stdio, env, shell: true });
1029
1037
  };
1030
1038
  async function run4(args, signal, io) {
1039
+ try {
1040
+ await runOnce(args, signal, io);
1041
+ } catch (err) {
1042
+ if (!(err instanceof ModelNotOnPlan)) throw err;
1043
+ await runOnce(args, signal, io);
1044
+ }
1045
+ }
1046
+ async function runOnce(args, signal, io) {
1031
1047
  const exe = await resolution();
1032
1048
  const timeoutMs = io?.timeoutMs ?? defaultTimeoutMs;
1033
1049
  const launchedAs = args[0] === "exec" ? fingerprintFor(exe, knownVersion) : null;
1034
1050
  return new Promise((resolve, reject) => {
1051
+ const sent = withoutOffPlan(args.slice(1));
1052
+ const sentModel = sent.includes("-m") ? sent[sent.indexOf("-m") + 1] : null;
1035
1053
  let child;
1036
1054
  try {
1037
1055
  child = spawnCodex(exe, args, io?.stdin != null);
@@ -1145,6 +1163,26 @@ function createRunner(opts = {}) {
1145
1163
  );
1146
1164
  return;
1147
1165
  }
1166
+ const refused = MODEL_NOT_ON_PLAN.exec(stderr);
1167
+ if (refused) {
1168
+ const model = refused[1];
1169
+ if (sentModel === model) {
1170
+ if (!offPlan.has(model))
1171
+ console.warn(`codex: ${model} is not on this ChatGPT plan, running on the Codex default`);
1172
+ offPlan.add(model);
1173
+ finish("model-not-on-plan", () => reject(new ModelNotOnPlan(`${model} is not on this ChatGPT plan`)));
1174
+ return;
1175
+ }
1176
+ finish(
1177
+ `exit-${code ?? "unknown"}`,
1178
+ () => reject(
1179
+ new Error(
1180
+ `Your ChatGPT plan cannot run ${model} in Codex. Choose another model in Codex, then run this again.`
1181
+ )
1182
+ )
1183
+ );
1184
+ return;
1185
+ }
1148
1186
  const conflicting = presentConflictKeys(parentEnv, ignoreEnvKeys());
1149
1187
  if (conflicting.length > 0 && /\b401\b|unauthorized/i.test(stderr) && BAD_KEY_401.test(stderr)) {
1150
1188
  noteConnection("refused", {
@@ -1780,14 +1818,57 @@ function createCodexEngine(opts) {
1780
1818
  const withWorkDir = runner.withWorkDir;
1781
1819
  const generatedImagesDir = () => join(process.env.CODEX_HOME || join(homedir(), ".codex"), "generated_images");
1782
1820
  async function snapshotGenerated() {
1783
- if (platform !== "win32") return null;
1784
1821
  try {
1785
1822
  return new Set(await readdir(generatedImagesDir()));
1786
1823
  } catch {
1787
1824
  return /* @__PURE__ */ new Set();
1788
1825
  }
1789
1826
  }
1790
- async function collectImages(dir, before = null, claimed) {
1827
+ async function drawnSince(before, claimed) {
1828
+ const home = generatedImagesDir();
1829
+ let names;
1830
+ try {
1831
+ names = await readdir(home);
1832
+ } catch {
1833
+ return [];
1834
+ }
1835
+ const found = [];
1836
+ const consider = async (path) => {
1837
+ const st = await lstat(path);
1838
+ if (st.isFile() && /\.png$/i.test(path) && !claimed.has(path)) found.push({ path, mtime: st.mtimeMs });
1839
+ };
1840
+ for (const name of names) {
1841
+ if (before.has(name)) continue;
1842
+ const at = join(home, name);
1843
+ try {
1844
+ const st = await lstat(at);
1845
+ if (st.isDirectory()) for (const file of await readdir(at)) await consider(join(at, file));
1846
+ else await consider(at);
1847
+ } catch {
1848
+ }
1849
+ }
1850
+ return found.sort((a, b) => b.mtime - a.mtime).map((f) => f.path);
1851
+ }
1852
+ async function claimCopy(drawn, buf, claimed) {
1853
+ for (const path of drawn) {
1854
+ try {
1855
+ if ((await stat(path)).size !== buf.length || !(await readFile(path)).equals(buf)) continue;
1856
+ } catch {
1857
+ continue;
1858
+ }
1859
+ claimed.add(path);
1860
+ return true;
1861
+ }
1862
+ return false;
1863
+ }
1864
+ async function takeDrawn(path, claimed) {
1865
+ claimed.add(path);
1866
+ const buf = await readLeftFile(path, basename(path));
1867
+ const hash = saveImage(buf);
1868
+ await forgetGenerated(buf);
1869
+ return hash;
1870
+ }
1871
+ async function collectImages(dir, before, claimed = /* @__PURE__ */ new Set()) {
1791
1872
  const entries = await readdir(dir);
1792
1873
  const outFiles = entries.filter((name) => /^out-.*\.png$/.test(name)).sort((a, b) => {
1793
1874
  const na = Number(/^out-(\d+)\.png$/.exec(a)?.[1] ?? NaN);
@@ -1796,9 +1877,12 @@ function createCodexEngine(opts) {
1796
1877
  return a.localeCompare(b);
1797
1878
  });
1798
1879
  if (outFiles.length === 0) {
1799
- if (before) {
1800
- const recovered = await recoverFromGenerated(before, claimed);
1801
- if (recovered) return [recovered];
1880
+ if (platform === "win32") {
1881
+ const [newest] = await drawnSince(before, claimed);
1882
+ if (newest) {
1883
+ console.warn(`codex: workdir empty, recovered ${newest}`);
1884
+ return [await takeDrawn(newest, claimed)];
1885
+ }
1802
1886
  }
1803
1887
  throw new Error("Codex finished but produced no images");
1804
1888
  }
@@ -1806,6 +1890,12 @@ function createCodexEngine(opts) {
1806
1890
  for (const name of outFiles) {
1807
1891
  const buf = await readLeftFile(join(dir, name), name);
1808
1892
  if (buf.length === 0) throw new Error(`codex: ${name} is empty`);
1893
+ const drawn = platform === "win32" ? [] : await drawnSince(before, claimed);
1894
+ if (drawn.length > 0 && !await claimCopy(drawn, buf, claimed)) {
1895
+ console.warn(`codex: ${name} is not a picture the image tool drew, taking the tool's own file`);
1896
+ hashes.push(await takeDrawn(drawn[0], claimed));
1897
+ continue;
1898
+ }
1809
1899
  hashes.push(saveImage(buf));
1810
1900
  await forgetGenerated(buf);
1811
1901
  }
@@ -1844,22 +1934,6 @@ function createCodexEngine(opts) {
1844
1934
  }
1845
1935
  }
1846
1936
  }
1847
- async function recoverFromGenerated(before, claimed) {
1848
- const home = generatedImagesDir();
1849
- let names;
1850
- try {
1851
- names = (await readdir(home)).filter((n) => !before.has(n) && !claimed?.has(n));
1852
- } catch {
1853
- return null;
1854
- }
1855
- if (!names.length) return null;
1856
- const stamped = await Promise.all(names.map(async (n) => ({ n, mtime: (await stat(join(home, n))).mtimeMs })));
1857
- stamped.sort((a, b) => b.mtime - a.mtime);
1858
- const pick2 = stamped[0].n;
1859
- claimed?.add(pick2);
1860
- console.warn(`codex: workdir empty, recovered ${pick2} from ${home}`);
1861
- return saveImage(await readLeftFile(join(home, pick2), pick2));
1862
- }
1863
1937
  return {
1864
1938
  capabilities() {
1865
1939
  return {
@@ -16312,6 +16386,21 @@ function registerImageRoutes(app, deps) {
16312
16386
  var HEADLINES_KEPT = 5;
16313
16387
  var FIRST_PUBLIC = "0.2.0";
16314
16388
  var RELEASES = [
16389
+ {
16390
+ version: "0.20.4",
16391
+ date: "2026-09-28",
16392
+ title: "Shots made with Codex work on every ChatGPT plan",
16393
+ sections: [
16394
+ {
16395
+ heading: "Create",
16396
+ body: "When your ChatGPT plan does not offer the model Scenri asks Codex for, the shot now runs on the model your Codex is set to, with every reference picture still attached."
16397
+ },
16398
+ {
16399
+ heading: "Fixes",
16400
+ body: "On Mac and Linux a shot keeps only the picture Codex drew, never a stray file. An open tab reloads itself when Scenri restarts on a new version."
16401
+ }
16402
+ ]
16403
+ },
16315
16404
  {
16316
16405
  version: "0.20.3",
16317
16406
  date: "2026-09-27",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.20.3",
3
+ "version": "0.20.4",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",