ossclip 0.1.10 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ossclip",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Local-first CLI video producer: cuts silence and fillers, word-timed captions, face-aware framing, and LLM-planned code-rendered graphics — transcription and rendering never leave your machine",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,9 +36,9 @@
36
36
  "commander": "^12.1.0",
37
37
  "tsx": "^4.19.0",
38
38
  "zod": "^3.25.76",
39
- "@ossclip/core": "0.1.10",
40
- "@ossclip/renderer": "0.1.10",
41
- "@ossclip/scenes": "0.1.10"
39
+ "@ossclip/core": "0.1.11",
40
+ "@ossclip/renderer": "0.1.11",
41
+ "@ossclip/scenes": "0.1.11"
42
42
  },
43
43
  "homepage": "https://github.com/AhsanAyaz/ossclip#readme",
44
44
  "bugs": {
package/src/edit.ts CHANGED
@@ -390,11 +390,22 @@ export async function startEditServer(
390
390
  );
391
391
  if (!parsed.success) return send(500, { error: `command.json is not valid: ${parsed.error.message}` });
392
392
  const cmd = parsed.data;
393
+ // §129: heal legacy records at replay. Before the fix, wizard and
394
+ // bare-path runs recorded process.argv — the ORIGINAL invocation,
395
+ // missing the `produce` literal the re-entered parse actually ran —
396
+ // so replaying them verbatim dies at commander's front door with
397
+ // "error: unknown option '--llm'". produce is the ONLY command that
398
+ // ever writes command.json, so an args array not starting with
399
+ // "produce" can only be that bug: prepend the literal to
400
+ // reconstruct the command that ran. A modern record — and a legacy
401
+ // directly-typed `ossclip produce …` — already starts with it and
402
+ // is untouched.
403
+ const args = cmd.args[0] === "produce" ? cmd.args : ["produce", ...cmd.args];
393
404
  renderLines = [];
394
405
  renderExit = null;
395
406
  renderStartedAt = Date.now();
396
407
  renderCancelled = false;
397
- const child = spawn(cmd.execPath, [...cmd.execArgv, cmd.script, ...cmd.args], {
408
+ const child = spawn(cmd.execPath, [...cmd.execArgv, cmd.script, ...args], {
398
409
  cwd: cmd.cwd,
399
410
  stdio: ["ignore", "pipe", "pipe"],
400
411
  });
package/src/produce.ts CHANGED
@@ -98,6 +98,7 @@ import {
98
98
  } from "@ossclip/core";
99
99
  import { recordRecentProject } from "./edit";
100
100
  import { editHint } from "./interactive/edit-hint";
101
+ import { recordedProduceArgs } from "./replay-argv";
101
102
  import { renderCover, renderProduction } from "@ossclip/renderer";
102
103
  import {
103
104
  coverTextRect,
@@ -2092,18 +2093,24 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
2092
2093
  // provider (R16 §75). Pin the RESOLVED choice into the recorded args —
2093
2094
  // never the key itself; secrets stay out of the workdir — so a replay
2094
2095
  // uses the same configuration or fails loudly asking for it.
2095
- const recordedArgs = process.argv.slice(2);
2096
- if (provider && !recordedArgs.includes("--llm")) {
2097
- recordedArgs.push("--llm", providerName);
2098
- }
2099
2096
  // §93g: pin the RESOLVED window, exactly as §75 pinned the provider. The
2100
2097
  // editor's Render replays this argv; if replay re-asked the model and got a
2101
2098
  // slightly different window, every saved override — anchored to scene ids
2102
2099
  // and word indices — would land on the wrong words. The word range, not
2103
2100
  // just `--clip 60`, is what makes replay deterministic with zero LLM calls.
2104
- if (clipWindow && !recordedArgs.includes("--clip-window")) {
2105
- recordedArgs.push("--clip-window", `${clipWindow.startWord}:${clipWindow.endWord}`);
2106
- }
2101
+ //
2102
+ // §129: NOT process.argv. A wizard or bare-path run re-enters commander
2103
+ // with a BUILT argv while process.argv still holds the original invocation
2104
+ // (`ossclip <path>`, no `produce` literal, none of the wizard's answers) —
2105
+ // recording process.argv shipped a command that replays as
2106
+ // `ossclip <path> --llm …` and dies on "unknown option '--llm'".
2107
+ // recordedProduceArgs prefers the argv the re-entry stashed and falls back
2108
+ // to process.argv for a directly typed `ossclip produce …`, which stays
2109
+ // byte-identical to what was always recorded.
2110
+ const recordedArgs = recordedProduceArgs({
2111
+ llm: provider ? providerName : undefined,
2112
+ clipWindow: clipWindow ? `${clipWindow.startWord}:${clipWindow.endWord}` : undefined,
2113
+ });
2107
2114
  await writeFile(
2108
2115
  join(work, "command.json"),
2109
2116
  JSON.stringify(
package/src/program.ts CHANGED
@@ -7,6 +7,7 @@ import { CleanupLevelSchema, SceneComponentIdSchema } from "@ossclip/core";
7
7
  import { STUDIO_ENTRY } from "@ossclip/renderer";
8
8
  import { loadEnvFiles } from "./env";
9
9
  import { produce } from "./produce";
10
+ import { setReplayArgv } from "./replay-argv";
10
11
 
11
12
  // Before anything reads a provider key (R16 §77) — including the auto-detect
12
13
  // order in `defaultProviderName`, which decides which model runs.
@@ -95,6 +96,11 @@ export function buildProgram(): Command {
95
96
  // IS supplied — a piped `ossclip <path>` is `ossclip produce <path>`.
96
97
  const direct = ["produce", path];
97
98
  console.log(`\n▸ running:\n ${renderCommand(direct)}\n`);
99
+ // §129: THIS argv — not process.argv, which still says `ossclip
100
+ // <path>` with no `produce` literal — is the invocation
101
+ // command.json must record for the editor's Render to replay.
102
+ // Every parseAsync re-entry below stashes for the same reason.
103
+ setReplayArgv(direct);
98
104
  await program.parseAsync(["node", "ossclip", ...direct]);
99
105
  return;
100
106
  }
@@ -102,6 +108,7 @@ export function buildProgram(): Command {
102
108
  const { loadConfig } = await import("@ossclip/core");
103
109
  const argv = await produceWizard({ speaker: loadConfig().speaker, input: path });
104
110
  console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
111
+ setReplayArgv(argv); // §129
105
112
  await program.parseAsync(["node", "ossclip", ...argv]);
106
113
  return;
107
114
  }
@@ -119,6 +126,11 @@ export function buildProgram(): Command {
119
126
  // menu is also how you learn the flags", and three of the four entries
120
127
  // printed nothing. The menu's whole pedagogical point is this line.
121
128
  console.log(`\n▸ running:\n ${renderCommand(direct)}\n`);
129
+ // §129: stashed even for non-produce choices — the invariant is that
130
+ // the stash always mirrors the parse being entered, and only
131
+ // produce's recording ever reads it (consume-on-read keeps a
132
+ // non-produce stash from leaking past this parse).
133
+ setReplayArgv(direct);
122
134
  await program.parseAsync(["node", "ossclip", ...direct]);
123
135
  return;
124
136
  }
@@ -126,6 +138,7 @@ export function buildProgram(): Command {
126
138
  const { loadConfig } = await import("@ossclip/core");
127
139
  const argv = await produceWizard({ speaker: loadConfig().speaker });
128
140
  console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
141
+ setReplayArgv(argv); // §129
129
142
  await program.parseAsync(["node", "ossclip", ...argv]);
130
143
  });
131
144
 
@@ -263,6 +276,7 @@ export function buildProgram(): Command {
263
276
  console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
264
277
  // Re-entering the SAME parse the flags take: the zod checks below run
265
278
  // on wizard output exactly as they do on a typed command line.
279
+ setReplayArgv(argv); // §129
266
280
  await program.parseAsync(["node", "ossclip", ...argv]);
267
281
  return;
268
282
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * What command.json must record: the argv of the parse that ACTUALLY ran
3
+ * (§129).
4
+ *
5
+ * `produce` records its invocation into the workdir so the editor's Render
6
+ * button can replay it byte for byte. It used to record `process.argv`,
7
+ * which is the truth for exactly one entry point — a directly typed
8
+ * `ossclip produce …`. The wizard has always BUILT a produce argv and
9
+ * re-entered `program.parseAsync(["node", "ossclip", ...argv])`, and the
10
+ * bare-path route does the same; in both, process.argv still holds the
11
+ * ORIGINAL invocation — no `produce` literal, none of the wizard's answers —
12
+ * so every re-entered run recorded a command that replays as
13
+ * `ossclip <path> --llm …` and dies at commander's front door with
14
+ * "error: unknown option '--llm'" (§129's field artifact). The re-entry
15
+ * sites in program.ts stash the argv they are about to parse here; the
16
+ * recording prefers the stash and falls back to process.argv, which keeps
17
+ * the direct path byte-identical to what it always wrote.
18
+ */
19
+
20
+ let stashed: string[] | null = null;
21
+
22
+ /**
23
+ * Called by every parseAsync re-entry in program.ts, immediately before the
24
+ * parse, with the argv minus its ["node", "ossclip"] prefix. Copied so a
25
+ * caller reusing its array cannot retroactively edit the record.
26
+ */
27
+ export function setReplayArgv(argv: string[]): void {
28
+ stashed = [...argv];
29
+ }
30
+
31
+ /**
32
+ * Consume-on-read (§129): commander 12 keeps option state across parseAsync
33
+ * calls (see the bare-`produce` refusal in program.ts), and a stash kept
34
+ * across parses would be the same trap one layer up — a menu choice that
35
+ * never reaches `produce` must not leave its argv behind for a later
36
+ * recording in the same process to mistake for its own.
37
+ */
38
+ export function consumeReplayArgv(): string[] | null {
39
+ const argv = stashed;
40
+ stashed = null;
41
+ return argv;
42
+ }
43
+
44
+ /**
45
+ * The args `produce` writes into command.json: the argv of the parse that
46
+ * ran (stash for a re-entered wizard/bare-path run, process.argv for a
47
+ * directly typed one), plus the §75/§93g pins. The pins guard on
48
+ * `includes` so a flag the user actually typed — or a pin recorded by the
49
+ * run a replay is re-running — is never appended twice.
50
+ */
51
+ export function recordedProduceArgs(pins: { llm?: string; clipWindow?: string }): string[] {
52
+ const args = consumeReplayArgv() ?? process.argv.slice(2);
53
+ if (pins.llm !== undefined && !args.includes("--llm")) {
54
+ args.push("--llm", pins.llm);
55
+ }
56
+ if (pins.clipWindow !== undefined && !args.includes("--clip-window")) {
57
+ args.push("--clip-window", pins.clipWindow);
58
+ }
59
+ return args;
60
+ }