flash-story-app 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ FlashStory — proprietary software.
2
+
3
+ Copyright (c) 2026. All rights reserved.
4
+
5
+ This package is UNLICENSED: installing and running it (e.g. via `npx
6
+ flashstory`) is permitted; modification, copying and redistribution are NOT
7
+ granted. See the store listing / PRD for the product context.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # flash-story-app
2
+
3
+ One command runs the whole FlashStory shot editor — the B/S version (WEB.md):
4
+
5
+ ```bash
6
+ npx flash-story-app # starts the server on http://127.0.0.1:8787 (web UI + API)
7
+ ```
8
+
9
+ It is also a CLI for automation:
10
+
11
+ ```bash
12
+ npx flash-story-app health # server status
13
+ npx flash-story-app scene list # projects
14
+ npx flash-story-app scene get --project <id> # read a scene
15
+ npx flash-story-app entity add --project <id> --id c1 --name 老张 --color red
16
+ npx flash-story-app transform set --project <id> --id c1 --json '[{…}]'
17
+ npx flash-story-app export submit --project <id> # + status / result
18
+ ```
19
+
20
+ Everything it does goes through the same reducer validation as the editor UI.
21
+ For the full command reference and the Claude Code Skill workflow, see
22
+ `skills/flashstory/SKILL.md` in the repo (or the FlashStory Skill catalog).
23
+
24
+ Environment:
25
+
26
+ | var | default | meaning |
27
+ |---|---|---|
28
+ | `FLASHSTORY_DATA_DIR` | `~/.flashstory/data` | project + export + asset storage |
29
+ | `FLASHSTORY_PORT` | `8787` | HTTP port |
30
+ | `FLASHSTORY_HOST` | `127.0.0.1` | bind address (LAN requires explicit opt-in) |
31
+ | `FLASHSTORY_STATIC_DIR` | packaged `static/` | where the web UI is served from |
32
+ | `FLASHSTORY_ENV_FILE` | `./.env` | .env file path (see below; missing file = no-op) |
33
+ | `FLASHSTORY_OPENAI_API_KEY` | — | LLM gateway key (never reaches the browser) |
34
+ | `FLASHSTORY_OPENAI_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible endpoint |
35
+ | `FLASHSTORY_OPENAI_MODEL` | `gpt-4o-mini` | default model |
36
+ | `FLASHSTORY_OPENAI_MODELS` | = MODEL | **fallback** selectable model list (comma/space separated), used only when the endpoint's live `/v1/models` probe fails |
37
+ | `FLASHSTORY_SERVER` | `http://127.0.0.1:8787` | CLI client target (flag `--server` wins) |
38
+
39
+ Serve-mode CLI flags (override env / .env): `--port <n>` `--host <h>`
40
+ `--data-dir <dir>` `--open` (open the editor in the default browser after boot).
41
+
42
+ ### .env support
43
+
44
+ Both the server and the CLI client load a `.env` at startup. Lookup order:
45
+
46
+ 1. `FLASHSTORY_ENV_FILE` — explicit path
47
+ 2. `./.env` — the working directory (wherever you run it; a repo-root or
48
+ server `.env` is fine as long as you start from there)
49
+ 3. `~/.flashstory/.env` — machine-global config, so `npx flash-story-app` from
50
+ any directory still picks up your key
51
+
52
+ Process environment wins over .env — a shell `export` beats the file. The
53
+ startup banner prints which file was used and whether the LLM is configured:
54
+
55
+ ```
56
+ env file: ~/.flashstory/.env
57
+ data dir: /Users/me/.flashstory/data
58
+ LLM: configured · endpoint: https://api.deepseek.com/v1 · default model: deepseek-chat
59
+ ```
60
+
61
+ Example:
62
+
63
+ ```bash
64
+ FLASHSTORY_PORT=8787
65
+ FLASHSTORY_OPENAI_API_KEY=sk-…
66
+ FLASHSTORY_OPENAI_BASE_URL=https://api.deepseek.com/v1
67
+ FLASHSTORY_OPENAI_MODEL=deepseek-chat
68
+ FLASHSTORY_OPENAI_MODELS=deepseek-chat, deepseek-reasoner, gpt-4o-mini
69
+ ```
70
+
71
+ The settings dialog reads the model list from the server: a live probe of the
72
+ endpoint's `/v1/models` (cached 60s with a 2.5s timeout), falling back to
73
+ `FLASHSTORY_OPENAI_MODELS` (or the default model) when the probe fails.
74
+
75
+ ## Dev mode (vite)
76
+
77
+ In dev the frontend calls the backend **directly** (`http://127.0.0.1:8787` by
78
+ default) instead of going through vite's proxy — the proxy intermittently
79
+ truncates streaming SSE. The server answers with CORS for loopback origins
80
+ (`http://localhost:*` / `http://127.0.0.1:*`); override the allow-list with
81
+ `FLASHSTORY_CORS_ORIGINS` for non-loopback deploys. To point the dev frontend
82
+ at a different backend, set `VITE_FLASHSTORY_SERVER` (web/.env.development).
83
+ Production stays same-origin — no CORS needed.
84
+
85
+ ### Browser E2E (web/e2e)
86
+
87
+ `cd web && npx playwright test e2e/agent.spec.ts` runs the AI-session chain in
88
+ a real Chrome (`channel: 'chrome'`, uses your system Chrome, no download)
89
+ against a server on `FS_E2E_BASE` (default http://127.0.0.1:8787) and a real
90
+ LLM. It guards the two regressions that used to break AI chat: the server's
91
+ defaultModel fallback (a fresh browser sends no model) and the SSE delivery of
92
+ error `done` events — no more "event stream closed before completion".
93
+
94
+ License: UNLICENSED — see LICENSE.
package/dist/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{spawn as P}from"node:child_process";import{mkdirSync as F,writeFileSync as O,readFileSync as R}from"node:fs";import{join as S}from"node:path";import{homedir as C}from"node:os";import{l as q}from"./env.js";class w extends Error{}async function N(e,t,r,i){const d=await fetch(`${e}${r}`,{method:t,headers:i!==void 0?{"content-type":"application/json"}:void 0,body:i!==void 0?JSON.stringify(i):void 0}),c=await d.json().catch(()=>null);return{status:d.status,data:c}}async function A(e,t,r){const i=await fetch(`${e}${r}`,{method:t});return{status:i.status,text:await i.text()}}async function D(e,t){const r=await fetch(`${e}${t}`);return{status:r.status,bytes:new Uint8Array(await r.arrayBuffer())}}function k(e,t){if(e.status>=200&&e.status<300)return e.data;const r=e.data?.detail??e.data?.error??`HTTP ${e.status}`;throw new w(`${t} failed: ${r}`)}const G={get:N,post:N,binary:D,text:A};function H(e){const t=[],r={};for(let i=0;i<e.length;i++){const d=e[i];if(d.startsWith("--")){const c=d.slice(2),s=e[i+1];s!==void 0&&!s.startsWith("--")?(r[c]=s,i+=1):r[c]=!0}else t.push(d)}return{positionals:t,flags:r}}function o(e,t){const r=e[t];return typeof r=="string"?r:void 0}function J(e){const t=o(e,"project");if(!t)throw new w("--project <id> is required");return t}function g(e,t){if(!e)throw new w(`--json <${t}> is required`);try{return JSON.parse(e)}catch{throw new w(`--json <${t}> is not valid JSON`)}}async function h(e,t,r,i,d=!1){const c=await e.post(t,"POST",d?"/api/tools/batch":"/api/tools/execute",{projectId:r,...d?{actions:i}:{action:i}});if(c.status===404)return{data:{error:"project not found"},exit:1};const s=c.data;return s.ok?{data:{ok:!0,rev:s.rev},exit:0}:{data:{ok:!1,error:s.error??"rejected"},exit:1}}const y=e=>e().catch(t=>({data:{error:t instanceof Error?t.message:String(t)},exit:1}));async function B(e,t,r=G){const[i,...d]=e,{positionals:c,flags:s}=H(d),l=()=>J(s);switch(i){case"health":return y(async()=>{const u=await r.get(t,"GET","/health");return{data:k(u,"health"),exit:0}});case"scene":{const u=c[0];if(u==="list")return y(async()=>{const n=await r.get(t,"GET","/api/projects");return{data:k(n,"list projects"),exit:0}});if(u==="get")return y(async()=>{const n=await r.get(t,"GET",`/api/projects/${l()}`);if(n.status===404)throw new w("project not found");return{data:n.data,exit:0}});throw f("flashstory scene list|get --project <id>")}case"entity":{const u=c[0],n=l(),a=o(s,"id"),m=o(s,"name");switch(u){case"add":return h(r,t,n,{type:"addEntity",entity:{...a?{id:a}:{},name:m??"未命名",kind:o(s,"kind")??"character",...o(s,"color")?{color:o(s,"color")}:{}}});case"remove":if(!a)throw f("--id is required");return h(r,t,n,{type:"removeEntity",id:a});case"update":{if(!a)throw f("--id is required");const x={};return m&&(x.name=m),o(s,"color")&&(x.color=o(s,"color")),o(s,"heightCm")&&(x.heightCm=Number(o(s,"heightCm"))),h(r,t,n,{type:"updateEntity",id:a,patch:x})}default:throw f("flashstory entity add|remove|update --project <id> [--id] [--name]")}}case"transform":if(c[0]!=="set"||!o(s,"id"))throw f("flashstory transform set --project --id --json <keys>");return h(r,t,l(),{type:"setTransformTrack",id:o(s,"id"),keys:g(o(s,"json"),"keys")});case"pose":{const u=c[0];if(u==="list")return y(async()=>{const n=await r.get(t,"GET",`/api/projects/${l()}`);if(n.status===404)throw new w("project not found");return{data:{customPoses:n.data.doc.customPoses??{}},exit:0}});if(u==="set-keys"){if(!o(s,"id"))throw f("--id is required");return h(r,t,l(),{type:"setPoseTrack",id:o(s,"id"),keys:g(o(s,"json"),"keys")})}throw f("flashstory pose list|set-keys --project --id --json")}case"camera":if(c[0]!=="set-keys")throw f("flashstory camera set-keys --project --json");return h(r,t,l(),{type:"setCameraTrack",keys:g(o(s,"json"),"keys")});case"settings":return h(r,t,l(),{type:"setSettings",patch:g(o(s,"json"),"patch")});case"marker":{const u=Number(o(s,"t")??NaN);if(c[0]!=="add"||!Number.isFinite(u))throw f("flashstory marker add --project --t <sec> --label <text>");return h(r,t,l(),{type:"addMarker",t:u,label:o(s,"label")??""})}case"batch":return h(r,t,l(),g(o(s,"json"),"actions"),!0);case"screenshot":return y(async()=>{const u=await r.post(t,"POST","/api/tools/screenshot",{projectId:l(),t:Number(o(s,"t")??0),camera:o(s,"camera")??"edit"}),n=k(u,"screenshot"),a=o(s,"out");if(n.kind==="image"&&a){const m=n.dataUrl?.split(",")[1];return m&&O(a,Buffer.from(m,"base64")),{data:{kind:"image",saved:a},exit:0}}return{data:n,exit:0}});case"export":{const u=c[0],n=c[1];switch(u){case"submit":return y(async()=>{const a=await r.post(t,"POST","/api/export",{projectId:l(),includeVideo:s["no-video"]!==!0,includeStills:s.stills===!0});return{data:k(a,"export submit"),exit:0}});case"status":if(!n)throw f("usage: flashstory export status <exportId>");return y(async()=>{const a=await r.get(t,"GET",`/api/export/${n}`);if(a.status===404)throw new w("export not found");return{data:a.data,exit:0}});case"result":if(!n)throw f("usage: flashstory export result <exportId> --out <dir>");return y(async()=>{const a=o(s,"out")??"exports";F(a,{recursive:!0});const x=((await r.get(t,"GET",`/api/export/${n}/files`)).data?.files??[]).filter(Boolean),$=[];for(const j of x){const E=await r.binary(t,`/api/export/${n}/files/${encodeURIComponent(j)}`);E.status===200&&(O(S(a,j),E.bytes),$.push(S(a,j)))}return{data:{exportId:n,downloaded:$},exit:0}});case"cancel":if(!n)throw f("usage: flashstory export cancel <exportId>");return y(async()=>({data:(await r.post(t,"POST",`/api/export/${n}/cancel`,{})).data,exit:0}));default:throw f("flashstory export submit|status|result|cancel")}}default:throw f(`unknown command: ${i}`)}}function f(e){return new w(e)}const L="http://127.0.0.1:8787";function U(){return S(C(),".flashstory","config.json")}function _(){try{const e=R(U(),"utf8"),t=JSON.parse(e);return typeof t.server=="string"&&t.server?t.server:void 0}catch{return}}function V(e){return e??process.env.FLASHSTORY_SERVER??_()??L}q();const v=process.argv.slice(2),p={},T=[];for(let e=0;e<v.length;e++){const t=v[e];switch(t){case"--server":p.server=v[e+1],e+=1;break;case"--port":p.port=Number(v[e+1]),e+=1;break;case"--host":p.host=v[e+1],e+=1;break;case"--data-dir":p.dataDir=v[e+1],e+=1;break;case"--open":p.open=!0;break;default:T.push(t)}}const b=T[0];if(!b||b==="serve"){const{startServer:e}=await import("./serve.js"),t={};if(p.port!==void 0&&Number.isFinite(p.port)&&(t.port=p.port),p.host&&(t.hostname=p.host),p.dataDir&&(t.dataDir=p.dataDir),await e(t),p.open){const r=`http://${t.hostname??process.env.FLASHSTORY_HOST??"127.0.0.1"}:${t.port??process.env.FLASHSTORY_PORT??8787}`;Y(r)}}else{const e=V(p.server),{data:t,exit:r}=await B(T,e);console.log(JSON.stringify(t)),process.exit(r)}function Y(e){const t=process.platform,d=P(t==="darwin"?"open":t==="win32"?"cmd":"xdg-open",t==="win32"?["/c","start","",e]:[e],{stdio:"ignore",detached:!0});d.on("error",()=>{}),d.unref?.()}
package/dist/env.js ADDED
@@ -0,0 +1 @@
1
+ import{readFileSync as u,statSync as h}from"node:fs";function d(n){const s={};for(const e of n.split(/\r?\n/)){const i=e.trim();if(i===""||i.startsWith("#"))continue;const o=i.startsWith("export ")?i.slice(7).trim():i,r=o.indexOf("=");if(r<=0)continue;const l=o.slice(0,r).trim();if(!l)continue;let t=o.slice(r+1).trim();t.length>=2&&(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1));const a=t.search(/\s#/);a>=0&&(t=t.slice(0,a).trimEnd()),s[l]=t}return s}function p(n,s){let e=0;for(const[i,o]of Object.entries(n))s[i]===void 0&&(s[i]=o,e+=1);return e}let f=!1,c=null;function x(n){f||(f=!0,v())}function v(n){const s=[void 0,process.env.FLASHSTORY_ENV_FILE,".env",E()].filter(e=>!!e);for(const e of s)if(m(e)){try{p(d(u(e,"utf8")),process.env),c=e}catch{}return}c=null}function F(){return c}function E(){const n=process.env.HOME??"";return n?`${n}/.flashstory/.env`:"~/.flashstory/.env"}function m(n){try{return h(n).isFile()}catch{return!1}}export{F as a,x as l};
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{startServer as r}from"./serve.js";import{l as o}from"./env.js";import"@hono/node-server";import"node:module";import"hono";import"@hono/node-server/serve-static";import"node:fs";import"node:fs/promises";import"node:path";import"node:url";import"hono/streaming";import"stream";import"events";import"buffer";import"util";import"ws";import"node:os";o();await r();