scenri 0.20.2 → 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 +21 -0
- package/README.md +34 -32
- package/dist/{chunk-CMLJGPYC.js → chunk-GTDE2BS2.js} +15 -3
- package/dist/index.js +1 -1
- package/dist/{offer-W6NJXMWT.js → offer-FT2W2SQ7.js} +4 -4
- package/dist/serve.js +128 -24
- package/package.json +1 -1
- package/studio-dist/assets/index-B46fXqON.js +108 -0
- package/studio-dist/index.html +1 -1
- package/templates/previews/showcase/court-72-checker-sit.jpg +0 -0
- package/templates/showcase/court-72-checker-sit.json +1 -1
- package/studio-dist/assets/index-C-ACZ9Pw.js +0 -108
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
14
|
+
## [0.20.3](https://github.com/tonygorb/Scenri/compare/v0.20.2...v0.20.3) (2026-09-27)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **cli:** a skipped install script says how to allow it and clear npx's copy ([7afece2](https://github.com/tonygorb/Scenri/commit/7afece286e58e61ead28cac5186559781a368096))
|
|
20
|
+
* **cli:** the desktop offer names Settings > Local access ([1ea3992](https://github.com/tonygorb/Scenri/commit/1ea399235c21eb87127605ba834afed607be946e))
|
|
21
|
+
* **content:** the court sneakers tile, redrawn from words that agree with themselves ([1686d0d](https://github.com/tonygorb/Scenri/commit/1686d0df176b8745d0c4e76cfd6b91bc279d5eaf))
|
|
22
|
+
* public pages, films and first-run text describe Scenri as it is ([563f35e](https://github.com/tonygorb/Scenri/commit/563f35eab31f725df25057e6022cb1cf01eecf8f))
|
|
23
|
+
|
|
3
24
|
## [0.20.2](https://github.com/tonygorb/Scenri/compare/v0.20.1...v0.20.2) (2026-09-27)
|
|
4
25
|
|
|
5
26
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<br><br>
|
|
8
8
|
|
|
9
9
|
**The open studio for brand-consistent AI visuals.**<br>
|
|
10
|
-
Define a client's brand once. Then
|
|
10
|
+
Define a client's brand once. Then put its products and people into scenes, art-direct each shot in plain words, and refine it step by step. Open source, running entirely on your own machine and your own AI accounts.
|
|
11
11
|
|
|
12
12
|
[](https://github.com/tonygorb/scenri/actions/workflows/ci.yml)
|
|
13
13
|
[](https://www.npmjs.com/package/scenri)
|
|
@@ -21,9 +21,9 @@ npx scenri
|
|
|
21
21
|
|
|
22
22
|
<sub>Needs [Node.js](https://nodejs.org) 22 or newer. New to any of this? The [install guide](https://github.com/tonygorb/scenri/blob/main/docs/INSTALL.md) walks through every step.</sub>
|
|
23
23
|
|
|
24
|
-
<img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/demo.gif" alt="
|
|
24
|
+
<img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/demo.gif" alt="Scenri's Create page: in the composer a dollar sign picks the product Cropped Puffer, an at sign picks the presenter Kwame, a slash picks the Container Blue scene, and a line of written direction follows the three chips. Generate turns a new card into a dithered swirl where the picture is being made, the swirl develops into a man in an orange puffer jacket leaning on a cobalt shipping container, and the shot opens beside the prompt that made it" width="820">
|
|
25
25
|
|
|
26
|
-
<sub>Pick a product, pick a presenter, pick a scene, then write the direction. That is the
|
|
26
|
+
<sub>Pick a product, pick a presenter, pick a scene, then write the direction. That is the prompt.</sub>
|
|
27
27
|
|
|
28
28
|
</div>
|
|
29
29
|
|
|
@@ -33,27 +33,27 @@ npx scenri
|
|
|
33
33
|
|
|
34
34
|
Most AI image tools give you a prompt box and a slot machine. Scenri gives you the part that actually takes the time: **art direction**.
|
|
35
35
|
|
|
36
|
-
Every shot keeps its
|
|
36
|
+
Every shot keeps its history. Open it, say what to change, and each refinement joins a trail under the picture, so the original and every step stay one click apart. Refine any step again, or use **Reuse setup** to start a new shot from the same prompt and chips.
|
|
37
37
|
|
|
38
38
|
It runs as a local server on your own computer. Your brands, your images, and your keys stay on your disk. To use it from a phone or tablet on the same Wi-Fi, open Settings, Local access, and scan the QR code.
|
|
39
39
|
|
|
40
|
-
## What a
|
|
40
|
+
## What a prompt produces
|
|
41
41
|
|
|
42
|
-
The film above ends on the
|
|
42
|
+
The film above ends on one of the examples from the Home wall: the Cropped Puffer product, the presenter Kwame, the Container Blue scene, and a short paragraph of written direction, of which the film types the first line. The chips carry the identity; the words do the art direction. Here are eight more shots from the Home wall, each built the same way from chips and written direction:
|
|
43
43
|
|
|
44
44
|
<!-- One paragraph, no whitespace between the tags: a newline here renders as
|
|
45
45
|
a fixed word-space, and 4 x 24.5% plus three spaces overflows a phone screen
|
|
46
|
-
and wraps 3+1. With zero gaps the rows stay four wide at every width. --><p align="center"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-paper-garden.jpg" title="Collagen in a paper garden" alt="A lilac Tanner Fielding collagen pouch standing among giant cut-paper flowers in coral, yellow and violet" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-foil-jump.jpg" title="Mid-air in the foil tent" alt="A woman in an orange puffer jacket jumping inside a tent of crinkled silver foil" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-ruby-pour.jpg" title="Aperitivo
|
|
46
|
+
and wraps 3+1. With zero gaps the rows stay four wide at every width. --><p align="center"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-paper-garden.jpg" title="Collagen in a paper garden" alt="A lilac Tanner Fielding collagen pouch standing among giant cut-paper flowers in coral, yellow and violet" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-foil-jump.jpg" title="Mid-air in the foil tent" alt="A woman in an orange puffer jacket jumping inside a tent of crinkled silver foil" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-ruby-pour.jpg" title="Aperitivo under a ruby pour" alt="A Fenner Ross aperitivo bottle on a pale pink ledge while a thick ruby syrup pours into a glass of ice beside it and drips over the edge" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-pink-smoke.jpg" title="Gold hoops in pink smoke" alt="A woman in profile with a low bun and a chunky gold hoop earring, wrapped in clouds of pink smoke" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-travertine-chair.jpg" title="Oak lounge chair in travertine light" alt="An oak lounge chair with cream boucle cushions beside a travertine block, window light striping the wall" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-balloon-knot.jpg" title="Headphones in the balloon knot" alt="A man with his eyes closed in pale blue over-ear headphones, sunk in a tangle of pink, orange and blue balloons" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-boulder-runner.jpg" title="Trail shoe on lichened granite" alt="A white and orange trail running shoe resting on lichen-covered granite high above a mountain valley" width="24.5%"><img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/readme-arch-lean.jpg" title="Leaning into the green arch" alt="A woman in cobalt trousers leaning inside a lime green arch on a sky blue set, holding a violet can" width="24.5%"></p>
|
|
47
47
|
|
|
48
|
-
All
|
|
48
|
+
All 110 examples on the Home wall work the same way. Open one and it loads back into the composer as the prompt that made it, ready to change.
|
|
49
49
|
|
|
50
50
|
## What you get out of the box
|
|
51
51
|
|
|
52
|
-
<img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/library.gif" alt="
|
|
52
|
+
<img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/library.gif" alt="Scenri's Home for a new brand: the four ways to start and the example tabs over the wall of finished shots, a slow scroll down the wall, then the Presenters library and the Scenes library, each opening with its offer to make your own above the catalog" width="820">
|
|
53
53
|
|
|
54
|
-
A library of scenes, presenters and demo products, so the app is useful before you have uploaded anything of your own. Filter by category
|
|
54
|
+
A library of 42 scenes, 19 presenters and 30 demo products, so the app is useful before you have uploaded anything of your own. Filter by category and star what fits the brand. Add your own products from their photos, and make your own presenters and scenes in a short conversation: describe a person or a place, or start from photos, and approve what Scenri draws before it is saved.
|
|
55
55
|
|
|
56
|
-
The brand kit
|
|
56
|
+
The brand kit lives in Settings and is the part that keeps output consistent. Name, mark, palette and rules, set once and carried into every shot that asks for them.
|
|
57
57
|
|
|
58
58
|
## Run it
|
|
59
59
|
|
|
@@ -67,11 +67,11 @@ That is the whole install. npm asks once whether to proceed, downloads the curre
|
|
|
67
67
|
|
|
68
68
|
On Windows, if PowerShell answers `npx.ps1 cannot be loaded because running scripts is disabled on this system`, that is a stock Windows setting, not a fault: type `npx.cmd scenri` instead, or use Command Prompt. Still no administrator required.
|
|
69
69
|
|
|
70
|
-
On macOS and Windows, Scenri then asks once whether to put a **Scenri icon on your desktop**. Say yes and from then on a double-click starts Scenri and opens it in your browser, no terminal needed; the icon works offline and keeps working across updates. Said no? Settings, then
|
|
70
|
+
On macOS and Windows, Scenri then asks once whether to put a **Scenri icon on your desktop**. Say yes and from then on a double-click starts Scenri and opens it in your browser, no terminal needed; the icon works offline and keeps working across updates. Said no? Settings, then Local access, then **Add to desktop**, or `npx scenri desktop` in a terminal. Scenri stays what it is, a local server and your browser: the icon is a launcher, not an app.
|
|
71
71
|
|
|
72
|
-
Generation runs on **Codex CLI**, an official helper from OpenAI that draws on your own ChatGPT plan. No API key to paste, and Scenri never charges you. Each image
|
|
72
|
+
Generation runs on **Codex CLI**, an official helper from OpenAI that draws on your own paid ChatGPT plan (Codex image generation is not part of the Free plan). No API key to paste, and Scenri never charges you. Each image spends some of your plan's Codex usage. You do not have to set it up by hand: if it is missing, Scenri offers to install it and to sign you in, both from the app. No ChatGPT plan? Add your own key from an image provider in Settings instead, see [Engines](#engines).
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
One dependency, `better-sqlite3`, runs an install script that fetches its native binary. The npm that ships with current Node releases (npm 11) runs it without asking. npm 12 blocks dependency install scripts by default, so if `npm --version` prints 12, allow this one once with `npm config set allow-scripts=better-sqlite3 --location=user` before you run `npx scenri`. Something not starting? See [troubleshooting](https://github.com/tonygorb/scenri/blob/main/docs/INSTALL.md#troubleshooting).
|
|
75
75
|
|
|
76
76
|
<details>
|
|
77
77
|
<summary>Run from source</summary>
|
|
@@ -88,35 +88,37 @@ pnpm dev # starts the server on 127.0.0.1:4747
|
|
|
88
88
|
|
|
89
89
|
## First five minutes
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
Scenri offers to walk you through your first shot, one step at a time, and Learn holds the rest. The short version:
|
|
92
|
+
|
|
93
|
+
1. **Paste a website URL.** Scenri reads the public pages and drafts the kit: name, logo, palette. If the site sells things, it offers to add the products too.
|
|
94
|
+
2. **Describe a shot.** In the composer, `$` reaches for a product, `@` for a presenter, `/` for a scene, and `#` for a colour. Everything between them is your own words.
|
|
95
|
+
3. **Generate on Codex CLI.** Runs on your own paid ChatGPT plan, so there is no key and no per-image charge from us.
|
|
96
|
+
4. **Refine it.** Open the shot and say what to change. Each refinement joins the trail under the picture.
|
|
97
|
+
5. **Add your own key** in Settings, Providers to run on OpenRouter, Replicate or fal instead.
|
|
96
98
|
|
|
97
99
|
## Why it is built this way
|
|
98
100
|
|
|
99
|
-
- **Iteration is the product.**
|
|
101
|
+
- **Iteration is the product.** Not a prompt box: every shot keeps its trail of refinements, and the ones you star collect in Keepers.
|
|
100
102
|
- **Your brands are files, not hostages.** `.brand` is an open, documented format under a permissive license. Email one to a client. Any tool can adopt it.
|
|
101
|
-
- **Your AI, your cost.** Bring your own Codex CLI
|
|
102
|
-
- **Local first, and it means it.** No account, no telemetry,
|
|
103
|
+
- **Your AI, your cost.** Bring your own ChatGPT plan through Codex CLI, or an API key. You pay the provider directly, at its own price. Scenri sells no credits and adds no markup.
|
|
104
|
+
- **Local first, and it means it.** No account, no telemetry, nothing uploaded to us. Scenri runs on this computer, and answers other devices on your own network only when they bring its six-digit code (Settings, Local access). It makes exactly two requests on its own behalf: a version-number check against npm every six hours so updates can announce themselves (and, when one is found, the download of that release from npm, staged locally until you choose to restart), and the download of the library imagery archive from GitHub, once for each library version a release pins. Nothing about you or your work is sent in either, and both turn off: in Settings or `SCENRI_NO_UPDATE_CHECK=1` for the first, `SCENRI_NO_CONTENT_FETCH=1` for the second ([how updates work](https://github.com/tonygorb/scenri/blob/main/docs/updates.md), [what goes where](https://github.com/tonygorb/scenri/blob/main/PRIVACY.md)).
|
|
103
105
|
|
|
104
106
|
## Engines
|
|
105
107
|
|
|
106
108
|
| Engine | What it costs you | Needs | Carries a Product or Presenter |
|
|
107
109
|
|---|---|---|---|
|
|
108
|
-
| **Codex CLI** | your ChatGPT plan | a ChatGPT
|
|
109
|
-
| OpenRouter | about $0.04
|
|
110
|
-
| Replicate | about $0.003
|
|
111
|
-
| fal | about $0.
|
|
110
|
+
| **Codex CLI** | your ChatGPT plan's Codex usage | a paid ChatGPT plan; the app installs Codex and signs you in | yes, up to 5 references |
|
|
111
|
+
| OpenRouter | about $0.04 an image | API key | yes, up to 4 references |
|
|
112
|
+
| Replicate | about $0.003 an image | API token | no |
|
|
113
|
+
| fal | about $0.006 an image (fal bills per megapixel) | API key | no |
|
|
112
114
|
|
|
113
115
|
Codex CLI is the default because it needs no key and because it carries the most reference images: a shot that has to keep both a product and a person accurate needs the room.
|
|
114
116
|
|
|
115
|
-
It is not free.
|
|
117
|
+
It is not free. Paid ChatGPT plans include Codex usage, and each image spends some of it: OpenAI says image generation uses it three to five times faster than a plain turn. OpenAI meters that, not Scenri, so spend caps do not apply to this engine.
|
|
116
118
|
|
|
117
|
-
**Without a ChatGPT plan**, use your own provider key. OpenRouter is the one to pick if your shots name a Product or a Presenter. Replicate and fal take no reference images, so Scenri refuses those
|
|
119
|
+
**Without a paid ChatGPT plan**, use your own provider key. OpenRouter is the one to pick if your shots name a Product or a Presenter. Replicate and fal take no reference images, so Scenri refuses those shots on them rather than generating something that only looks right. Edits on Replicate and fal cost more than a new image.
|
|
118
120
|
|
|
119
|
-
Keys are stored in your local library folder, sent only to that provider, and never returned by the API. Set a monthly spend cap per engine in Settings.
|
|
121
|
+
Keys are stored in your local library folder, sent only to that provider, and never returned by the API. Set a monthly spend cap per engine in Settings, Providers.
|
|
120
122
|
|
|
121
123
|
Your Codex session is yours: Scenri runs the official `codex` commands on your machine and never reads, copies or stores the credential. That is also why Scenri never pools user plans: a plan is licensed to the person who pays for it, not to a service reselling it to other people. Any hosted version of Scenri, if one ever exists, would run API-priced engines only.
|
|
122
124
|
|
|
@@ -136,7 +138,7 @@ Your Codex session is yours: Scenri runs the official `codex` commands on your m
|
|
|
136
138
|
|
|
137
139
|
`OPENROUTER_API_KEY`, `REPLICATE_API_TOKEN` and `FAL_KEY` are read from the environment as an alternative to entering them in Settings.
|
|
138
140
|
|
|
139
|
-
**Phones and tablets.** Scenri also answers on this computer's Wi-Fi or Ethernet address, so a phone on the same network can open it: Settings, Local access shows a QR code. Scenri has no accounts, so every device other than this computer has to bring a six-digit code, which rides inside the QR code and the link; typing the bare address asks for it, wrong guesses
|
|
141
|
+
**Phones and tablets.** Scenri also answers on this computer's Wi-Fi or Ethernet address, so a phone on the same network can open it: Settings, Local access shows a QR code. Scenri has no accounts, so every device other than this computer has to bring a six-digit code, which rides inside the QR code and the link; typing the bare address asks for it, ten wrong guesses from one device lock it out for ten minutes, and **New code** signs every device out. The first time, macOS or Windows may ask whether Node may accept incoming connections: allow it, or phones cannot reach Scenri. Setting `SCENRI_HOST=127.0.0.1` keeps Scenri to this computer alone. Over plain http the code travels unencrypted, like everything else, so treat phone access as convenience on a network you trust.
|
|
140
142
|
|
|
141
143
|
## Layout
|
|
142
144
|
|
|
@@ -146,7 +148,7 @@ Your Codex session is yours: Scenri runs the official `codex` commands on your m
|
|
|
146
148
|
| `packages/brand-spec` | the `.brand` schema, validator, and URL auto-builder |
|
|
147
149
|
| `packages/core` | brands, shot history, the version tree, image store, cost ledger (SQLite) |
|
|
148
150
|
| `packages/catalog` | product catalog import: Shopify, WooCommerce, Webflow, generic |
|
|
149
|
-
| `packages/engines/*` | engine adapters: `codex`, `openrouter`, `replicate`, `fal`, `demo
|
|
151
|
+
| `packages/engines/*` | engine adapters: `codex`, `openrouter`, `replicate`, `fal`, and `demo`, a placeholder used by the tests |
|
|
150
152
|
| `apps/studio` | the React studio the CLI serves |
|
|
151
153
|
|
|
152
154
|
One package publishes to npm: **`scenri`**, the CLI, which bundles everything else. The rest are internal.
|
|
@@ -155,7 +157,7 @@ One package publishes to npm: **`scenri`**, the CLI, which bundles everything el
|
|
|
155
157
|
|
|
156
158
|
Start with [CONTRIBUTING.md](https://github.com/tonygorb/scenri/blob/main/CONTRIBUTING.md). Engine adapters are the friendliest surface: one file, one interface, well covered by tests.
|
|
157
159
|
|
|
158
|
-
Accessibility is tracked in [docs/A11Y-BACKLOG.md](https://github.com/tonygorb/scenri/blob/main/docs/A11Y-BACKLOG.md).
|
|
160
|
+
Accessibility is tracked in [docs/A11Y-BACKLOG.md](https://github.com/tonygorb/scenri/blob/main/docs/A11Y-BACKLOG.md). Biome's recommended accessibility rules run at `error`, so the backlog is currently empty and a new defect fails CI rather than joining a list.
|
|
159
161
|
|
|
160
162
|
Found a security problem? Please report it privately. See [SECURITY.md](https://github.com/tonygorb/scenri/blob/main/SECURITY.md).
|
|
161
163
|
|
|
@@ -3,7 +3,8 @@ import { resolve } from 'path';
|
|
|
3
3
|
|
|
4
4
|
// src/bootError.ts
|
|
5
5
|
var INSTALL_GUIDE = "https://github.com/tonygorb/scenri/blob/main/docs/INSTALL.md";
|
|
6
|
-
var NATIVE_MARKERS = ["NODE_MODULE_VERSION", "
|
|
6
|
+
var NATIVE_MARKERS = ["NODE_MODULE_VERSION", "ERR_DLOPEN_FAILED"];
|
|
7
|
+
var NEVER_BUILT = "Could not locate the bindings file";
|
|
7
8
|
function portBusyLines(port) {
|
|
8
9
|
const next = port + 1;
|
|
9
10
|
return [
|
|
@@ -18,6 +19,17 @@ function bootErrorLines(err) {
|
|
|
18
19
|
const first = raw.split("\n")[0];
|
|
19
20
|
const code = err?.code;
|
|
20
21
|
const haystack = `${typeof code === "string" ? code : ""} ${raw}`;
|
|
22
|
+
if (haystack.includes(NEVER_BUILT)) {
|
|
23
|
+
return [
|
|
24
|
+
"Scenri could not start: a native component failed to load.",
|
|
25
|
+
`(${first})`,
|
|
26
|
+
"Its install script did not run: npm 12 skips install scripts until they are allowed.",
|
|
27
|
+
"Fix: allow it once, clear the copy npx kept, then start again:",
|
|
28
|
+
" npm config set allow-scripts=better-sqlite3 --location=user",
|
|
29
|
+
" npm cache npx rm --force",
|
|
30
|
+
" npx scenri"
|
|
31
|
+
];
|
|
32
|
+
}
|
|
21
33
|
if (NATIVE_MARKERS.some((marker) => haystack.includes(marker))) {
|
|
22
34
|
return [
|
|
23
35
|
"Scenri could not start: a native component failed to load.",
|
|
@@ -54,5 +66,5 @@ function anotherScenriLines(port, theirs, ours) {
|
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
export { anotherScenriLines, bootErrorLines, portBusyLines, shouldAdoptRunning };
|
|
57
|
-
//# sourceMappingURL=chunk-
|
|
58
|
-
//# sourceMappingURL=chunk-
|
|
69
|
+
//# sourceMappingURL=chunk-GTDE2BS2.js.map
|
|
70
|
+
//# sourceMappingURL=chunk-GTDE2BS2.js.map
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,8 @@ function shouldOfferDesktop(i) {
|
|
|
13
13
|
return true;
|
|
14
14
|
}
|
|
15
15
|
var OFFER_QUESTION = " Add Scenri to your desktop? Then you can open it without a terminal. [Y/n] ";
|
|
16
|
-
var LATER_LINE = " Not now. Add it later with: npx scenri desktop, or from Settings >
|
|
17
|
-
var STILL_RUNNING_LINE = " Scenri is running anyway. Add the icon later from Settings >
|
|
16
|
+
var LATER_LINE = " Not now. Add it later with: npx scenri desktop, or from Settings > Local access.";
|
|
17
|
+
var STILL_RUNNING_LINE = " Scenri is running anyway. Add the icon later from Settings > Local access.";
|
|
18
18
|
var ASK_TIMEOUT_MS = 12e4;
|
|
19
19
|
function parseOfferAnswer(raw) {
|
|
20
20
|
const answer = raw.trim().toLowerCase();
|
|
@@ -67,5 +67,5 @@ function askOnTerminal(question, timeoutMs = ASK_TIMEOUT_MS, io = {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export { OFFER_QUESTION, askOnTerminal, offerDesktop, parseOfferAnswer, shouldOfferDesktop };
|
|
70
|
-
//# sourceMappingURL=offer-
|
|
71
|
-
//# sourceMappingURL=offer-
|
|
70
|
+
//# sourceMappingURL=offer-FT2W2SQ7.js.map
|
|
71
|
+
//# sourceMappingURL=offer-FT2W2SQ7.js.map
|
package/dist/serve.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createUpdateChecker, classify, isReleaseTriplet, findNpm, stageVersion } from './chunk-YK37H6DM.js';
|
|
2
|
-
import { shouldAdoptRunning, anotherScenriLines, portBusyLines } from './chunk-
|
|
2
|
+
import { shouldAdoptRunning, anotherScenriLines, portBusyLines } from './chunk-GTDE2BS2.js';
|
|
3
3
|
import { SchemaTooNewError, defaultHome, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, nameMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-2EKTEMJY.js';
|
|
4
4
|
import { detectInstallKind } from './chunk-PAIAXRAC.js';
|
|
5
5
|
export { detectInstallKind } from './chunk-PAIAXRAC.js';
|
|
@@ -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,36 @@ 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
|
+
},
|
|
16404
|
+
{
|
|
16405
|
+
version: "0.20.3",
|
|
16406
|
+
date: "2026-09-27",
|
|
16407
|
+
title: "A clear fix when npm 12 stops Scenri from starting",
|
|
16408
|
+
sections: [
|
|
16409
|
+
{
|
|
16410
|
+
heading: "Install",
|
|
16411
|
+
body: "If npm 12 skipped the install script Scenri's database needs, the terminal now says so and prints the three commands that fix it, instead of blaming a Node update."
|
|
16412
|
+
},
|
|
16413
|
+
{
|
|
16414
|
+
heading: "Fixes",
|
|
16415
|
+
body: "The Court sneakers in the cobalt room example on Home is redrawn, and the desktop icon offer in the terminal now points to Settings > Local access."
|
|
16416
|
+
}
|
|
16417
|
+
]
|
|
16418
|
+
},
|
|
16315
16419
|
{
|
|
16316
16420
|
version: "0.20.2",
|
|
16317
16421
|
date: "2026-09-27",
|
|
@@ -20126,7 +20230,7 @@ async function run3() {
|
|
|
20126
20230
|
const ownEntry = fileURLToPath(import.meta.url);
|
|
20127
20231
|
const { addToDesktop, installDeps } = await import('./cli-PXPCJR7V.js');
|
|
20128
20232
|
const { refreshLauncher } = await import('./refresh-5HBDLLLB.js');
|
|
20129
|
-
const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-
|
|
20233
|
+
const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-FT2W2SQ7.js');
|
|
20130
20234
|
const { launcherInstalled } = await import('./paths-6ZXRZUHY.js');
|
|
20131
20235
|
const meta = readMeta();
|
|
20132
20236
|
void refreshLauncher({ ...installDeps(ownEntry), ownEntry, installKind, pkg: meta.name }).then((r) => {
|