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 +11 -0
- package/dist/serve.js +111 -22
- package/package.json +1 -1
- package/studio-dist/assets/index-B46fXqON.js +108 -0
- package/studio-dist/index.html +1 -1
- package/studio-dist/assets/index-C-ACZ9Pw.js +0 -108
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
|
|
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 (
|
|
1800
|
-
const
|
|
1801
|
-
if (
|
|
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",
|