@higgsfield/video-editor-mcp 1.0.4 → 1.0.6

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.
@@ -4,13 +4,6 @@ import { fileURLToPath as __higgseditPath } from "node:url";
4
4
  const require = __higgseditRequire(import.meta.url);
5
5
  const __filename = __higgseditPath(import.meta.url);
6
6
  const __dirname = __filename.slice(0, __filename.lastIndexOf("/"));
7
- import {
8
- frame,
9
- render,
10
- sheet,
11
- verify
12
- } from "./chunk-4D6AXU4B.mjs";
13
- import "./chunk-QLPCYN6K.mjs";
14
7
  import {
15
8
  flag,
16
9
  parseArgs
@@ -18,21 +11,6 @@ import {
18
11
  import {
19
12
  CLI_MANIFEST
20
13
  } from "./chunk-FZ5U36SQ.mjs";
21
- import {
22
- check
23
- } from "./chunk-MK6PL7N4.mjs";
24
- import {
25
- stampProject
26
- } from "./chunk-VI6IV6JP.mjs";
27
- import {
28
- assertProject
29
- } from "./chunk-SIMO6I3D.mjs";
30
- import "./chunk-6BETMESC.mjs";
31
- import "./chunk-WZR5PZM7.mjs";
32
- import "./chunk-F7YUKEI5.mjs";
33
- import "./chunk-EKFC4S4M.mjs";
34
- import "./chunk-2VKQHJMM.mjs";
35
- import "./chunk-75WO2NOF.mjs";
36
14
 
37
15
  // src/main.ts
38
16
  import { resolve } from "node:path";
@@ -114,11 +92,13 @@ async function main(argv) {
114
92
  print(r, () => [r.written.length ? `created ${target}` : `${target}: nothing to create`, ...r.written.map((f) => ` ${f}`), ...r.skipped.map((f) => ` skipped (exists): ${f}`), `next: ${cd}npm install && npm run dev`].join("\n"));
115
93
  return 0;
116
94
  }
95
+ const { assertProject } = await import("./chunk-SIMO6I3D.mjs");
117
96
  const dir = assertProject(dirArg);
118
97
  const depth = flag(args, "depth") === "10" ? 10 : 8;
119
98
  const allowFontFallback = args.flags["allow-font-fallback"] === true;
120
99
  switch (command) {
121
100
  case "check": {
101
+ const { check } = await import("./chunk-MK6PL7N4.mjs");
122
102
  const r = await check(dir, { allowFontFallback });
123
103
  print(
124
104
  r,
@@ -137,6 +117,7 @@ async function main(argv) {
137
117
  return r.ok ? 0 : 1;
138
118
  }
139
119
  case "stamp": {
120
+ const { stampProject } = await import("./chunk-VI6IV6JP.mjs");
140
121
  const r = stampProject(dir);
141
122
  print(r, () => [`stamped ${r.stamped.length} element(s) in ${r.written.length} file(s)`, ...r.stamped.map((s) => ` ${s.file}: <${s.tag}> id="${s.id}"`), ...r.skipped.map((s) => ` skipped ${s.file}: ${s.reason}`)].join("\n"));
142
123
  return r.skipped.length ? 1 : 0;
@@ -145,6 +126,7 @@ async function main(argv) {
145
126
  const at = args.positional[1];
146
127
  const out = flag(args, "o", "out");
147
128
  if (!at || !out) throw new Error("usage: higgsedit frame <dir> <t> -o x.png");
129
+ const { frame } = await import("./chunk-4D6AXU4B.mjs");
148
130
  const r = await frame(dir, at, out, depth, allowFontFallback);
149
131
  print(r, () => `wrote ${r.out} (frame ${r.frame}, ${r.ms.toFixed(0)} ms)`);
150
132
  return 0;
@@ -153,6 +135,7 @@ async function main(argv) {
153
135
  const times = flag(args, "times")?.split(",").map((s) => s.trim()).filter(Boolean);
154
136
  const out = flag(args, "o", "out");
155
137
  if (!times?.length || !out) throw new Error("usage: higgsedit sheet <dir> --times 0,1,2 -o sheet.png");
138
+ const { sheet } = await import("./chunk-4D6AXU4B.mjs");
156
139
  const r = await sheet(dir, times, out, Number(flag(args, "columns") ?? Math.min(4, times.length)), Number(flag(args, "width") ?? 480), allowFontFallback);
157
140
  print(r, () => `wrote ${r.out} (frames ${r.frames.join(", ")})`);
158
141
  return 0;
@@ -163,6 +146,7 @@ async function main(argv) {
163
146
  const encoder = flag(args, "encoder");
164
147
  if (encoder && !ENCODERS.includes(encoder)) throw new Error(`unknown --encoder ${encoder}; use ${ENCODERS.join(", ")}`);
165
148
  const crf = flag(args, "crf");
149
+ const { render } = await import("./chunk-4D6AXU4B.mjs");
166
150
  const r = await render(dir, {
167
151
  out,
168
152
  depth,
@@ -188,6 +172,7 @@ async function main(argv) {
188
172
  return 0;
189
173
  }
190
174
  case "verify": {
175
+ const { verify } = await import("./chunk-4D6AXU4B.mjs");
191
176
  const r = await verify(dir, Number(flag(args, "count") ?? 12), depth, allowFontFallback);
192
177
  print(r, () => [`${r.identical ? "identical" : "DIFFERENT"}: ${r.frames.length} frames, ${r.depth}-bit, shuffled order ${r.shuffledOrder.join(",")}`, ...r.frames.map((f, i) => ` ${String(f).padStart(6)} ${r.forward[i]} ${r.shuffled[i] === r.forward[i] ? "=" : "\u2260"}`)].join("\n"));
193
178
  return r.identical ? 0 : 1;
@@ -213,13 +198,18 @@ async function main(argv) {
213
198
  return 1;
214
199
  }
215
200
  }
201
+ function exitAfterOutput(code) {
202
+ let pending = 2;
203
+ const flushed = () => { if (--pending === 0) process.exit(code); };
204
+ for (const stream of [process.stdout, process.stderr]) stream.write("", flushed);
205
+ }
216
206
  patientConnects();
217
207
  main(process.argv.slice(2)).then(
218
- (code) => process.exit(code),
208
+ exitAfterOutput,
219
209
  (error) => {
220
210
  const hint = error instanceof Error && error.name === "FontLoadError" ? " Pass --allow-font-fallback to render anyway." : "";
221
211
  console.error(`higgsedit: ${error instanceof Error ? error.message : String(error)}${hint}`);
222
- process.exit(1);
212
+ exitAfterOutput(1);
223
213
  }
224
214
  );
225
215
  var CLI_ROOT = resolve(import.meta.dirname, "..");
@@ -0,0 +1,295 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ const MAX_BYTES = 16 * 1024 * 1024;
5
+ const MAX_ENTRIES = 100;
6
+ const STALE = "edit history changed in another editor; reopen the project before editing or undoing";
7
+ const message = (error) => error instanceof Error ? error.message : String(error);
8
+ const missing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
9
+ const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10
+ const safePath = (path) => !/[\\\0]/.test(path) && path.split("/").every((part) => part !== "" && part !== "." && part !== ".." && part !== ".higgsedit-cache") && !/^[A-Za-z]:/.test(path);
11
+ const identity = (stat) => `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
12
+ function encode(undo, redo, revision, text = (entry) => JSON.stringify(entryValue(entry))) {
13
+ return `{"version":1,"revision":${JSON.stringify(revision)},"undo":[${undo.map(text).join(",")}],"redo":[${redo.map(text).join(",")}]}`;
14
+ }
15
+ const entryValue = ({ name, files }) => ({ name, files: [...files].map(([path, texts]) => ({ path, ...texts })) });
16
+ function decode(text) {
17
+ let value;
18
+ try {
19
+ value = JSON.parse(text);
20
+ } catch {
21
+ return;
22
+ }
23
+ if (!record(value) || value.version !== 1 || typeof value.revision !== "string" || !Array.isArray(value.undo) || !Array.isArray(value.redo) || value.undo.length + value.redo.length > MAX_ENTRIES) return;
24
+ const entries = (stack) => {
25
+ const result = [];
26
+ for (const entry of stack) {
27
+ if (!record(entry) || typeof entry.name !== "string" || !Array.isArray(entry.files) || !entry.files.length) return;
28
+ const files = /* @__PURE__ */ new Map();
29
+ for (const file of entry.files) {
30
+ if (!record(file) || typeof file.path !== "string" || !safePath(file.path) || typeof file.before !== "string" || typeof file.after !== "string" || files.has(file.path)) return;
31
+ files.set(file.path, { before: file.before, after: file.after });
32
+ }
33
+ result.push({ name: entry.name, files });
34
+ }
35
+ return result;
36
+ };
37
+ const undo = entries(value.undo), redo = entries(value.redo);
38
+ return undo && redo ? { undo, redo } : void 0;
39
+ }
40
+ class EditHistory {
41
+ constructor(dir, onWrite, limits = {}) {
42
+ this.dir = dir;
43
+ this.onWrite = onWrite;
44
+ this.maxBytes = Math.min(MAX_BYTES, limits.maxBytes ?? MAX_BYTES);
45
+ this.maxEntries = Math.min(MAX_ENTRIES, limits.maxEntries ?? MAX_ENTRIES);
46
+ if (!Number.isSafeInteger(this.maxBytes) || this.maxBytes < 100 || !Number.isSafeInteger(this.maxEntries) || this.maxEntries < 1) throw new Error("invalid edit history limits");
47
+ try {
48
+ const saved = this.readJournal();
49
+ this.revision = saved.revision;
50
+ const parsed = saved.text === void 0 ? void 0 : decode(saved.text);
51
+ if (parsed) {
52
+ const bounded = this.bound(parsed.undo, parsed.redo);
53
+ this.undoStack = bounded.undo;
54
+ this.redoStack = bounded.redo;
55
+ }
56
+ } catch {
57
+ }
58
+ }
59
+ dir;
60
+ onWrite;
61
+ undoStack = [];
62
+ redoStack = [];
63
+ revision = "missing";
64
+ maxBytes;
65
+ maxEntries;
66
+ // Committed entries never change. Weak keys release evicted history and abandoned transactions.
67
+ encoded = /* @__PURE__ */ new WeakMap();
68
+ begin(name) {
69
+ if (typeof name !== "string") throw new Error("edit history name must be a string");
70
+ this.assertCurrent();
71
+ const entry = { name, files: /* @__PURE__ */ new Map() };
72
+ const previousWrites = /* @__PURE__ */ new Map();
73
+ const fileBytes = /* @__PURE__ */ new Map();
74
+ let entryBytes = Buffer.byteLength(encode([entry], [], randomUUID()));
75
+ let finished = false;
76
+ return {
77
+ onWrite: (path, text) => {
78
+ if (finished) throw new Error("edit history transaction already finished");
79
+ const prior = entry.files.get(path);
80
+ const current = this.readSource(path);
81
+ if (prior && current !== prior.after) throw new Error(`${path} changed outside the editor since ${entry.name}`);
82
+ const texts = { before: prior?.before ?? current, after: text };
83
+ const bytes = Buffer.byteLength(JSON.stringify({ path, ...texts }));
84
+ const nextBytes = entryBytes - (fileBytes.get(path) ?? 0) + bytes + (!prior && entry.files.size ? 1 : 0);
85
+ if (nextBytes > this.maxBytes) throw new Error(`edit history size limit exceeded (${this.maxBytes} bytes); split the edit or reduce the affected source files`);
86
+ entryBytes = nextBytes;
87
+ fileBytes.set(path, bytes);
88
+ previousWrites.set(path, current);
89
+ entry.files.set(path, texts);
90
+ this.encoded.delete(entry);
91
+ this.onWrite?.(path, text);
92
+ },
93
+ commit: () => {
94
+ if (finished) throw new Error("edit history transaction already finished");
95
+ if (entry.files.size) {
96
+ this.checkSources(entry, "after");
97
+ this.publish([...this.undoStack, entry], []);
98
+ }
99
+ finished = true;
100
+ },
101
+ rollback: () => {
102
+ if (finished) return;
103
+ this.restore(entry, "after", "before", previousWrites);
104
+ finished = true;
105
+ }
106
+ };
107
+ }
108
+ state() {
109
+ return {
110
+ canUndo: this.undoStack.length > 0,
111
+ canRedo: this.redoStack.length > 0,
112
+ undoName: this.undoStack.at(-1)?.name ?? null,
113
+ redoName: this.redoStack.at(-1)?.name ?? null
114
+ };
115
+ }
116
+ undo() {
117
+ return this.step(this.undoStack, this.redoStack, "after", "before");
118
+ }
119
+ redo() {
120
+ return this.step(this.redoStack, this.undoStack, "before", "after");
121
+ }
122
+ step(from, to, expect, write) {
123
+ const entry = from.at(-1);
124
+ if (!entry) return { ok: false, reason: `nothing to ${write === "before" ? "undo" : "redo"}` };
125
+ try {
126
+ this.assertCurrent();
127
+ this.checkSources(entry, expect);
128
+ } catch (error) {
129
+ return { ok: false, reason: message(error) };
130
+ }
131
+ try {
132
+ for (const [path, texts] of entry.files) this.write(path, texts[write]);
133
+ const remaining = from.slice(0, -1), moved = [...to, entry];
134
+ this.publish(write === "before" ? remaining : moved, write === "before" ? moved : remaining);
135
+ } catch (error) {
136
+ try {
137
+ this.restore(entry, write, expect);
138
+ } catch (rollbackError) {
139
+ return { ok: false, reason: `${message(error)}; rollback failed: ${message(rollbackError)}` };
140
+ }
141
+ return { ok: false, reason: message(error) };
142
+ }
143
+ return { ok: true, written: [...entry.files.keys()] };
144
+ }
145
+ checkSources(entry, expect) {
146
+ for (const [path, texts] of entry.files) {
147
+ if (this.readSource(path) !== texts[expect]) throw new Error(`${path} changed outside the editor since ${entry.name}`);
148
+ }
149
+ }
150
+ /** A failed write may still contain the original text. Never roll back someone else's version. */
151
+ restore(entry, expect, restore, previousWrites) {
152
+ const pending = [];
153
+ for (const [path, texts] of entry.files) {
154
+ const current = this.readSource(path);
155
+ if (current === texts[restore]) continue;
156
+ if (current !== texts[expect] && current !== previousWrites?.get(path)) throw new Error(`${path} changed outside the editor since ${entry.name}`);
157
+ pending.push([path, texts[restore]]);
158
+ }
159
+ for (const [path, text] of pending) this.write(path, text);
160
+ }
161
+ readSource(path) {
162
+ if (!safePath(path)) throw new Error("unsafe edit history source path");
163
+ let target = this.dir;
164
+ for (const part of path.split("/")) {
165
+ target = join(target, part);
166
+ try {
167
+ if (lstatSync(target).isSymbolicLink()) throw new Error(`${path} is a symlink; cannot restore edit history`);
168
+ } catch (error) {
169
+ if (missing(error)) throw new Error(`${path} no longer exists`, { cause: error });
170
+ throw error;
171
+ }
172
+ }
173
+ return readFileSync(target, "utf8");
174
+ }
175
+ write(path, text) {
176
+ const target = join(this.dir, path);
177
+ this.onWrite?.(path, text);
178
+ this.atomicWrite(target, text, lstatSync(target).mode & 511);
179
+ }
180
+ cacheDir() {
181
+ const cache = join(this.dir, ".higgsedit-cache");
182
+ try {
183
+ const stat = lstatSync(cache);
184
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe edit history cache directory");
185
+ } catch (error) {
186
+ if (!missing(error)) throw error;
187
+ }
188
+ return cache;
189
+ }
190
+ readJournal() {
191
+ const file = join(this.cacheDir(), "history.json");
192
+ let fd;
193
+ try {
194
+ if (!lstatSync(file).isFile()) throw new Error("edit history journal is not a regular file");
195
+ fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
196
+ } catch (error) {
197
+ if (missing(error)) return { revision: "missing" };
198
+ throw new Error(`cannot read edit history: ${message(error)}`, { cause: error });
199
+ }
200
+ try {
201
+ const stat = fstatSync(fd, { bigint: true });
202
+ if (!stat.isFile()) throw new Error("edit history journal is not a regular file");
203
+ const revision = identity(stat);
204
+ if (stat.size > BigInt(this.maxBytes)) return { revision };
205
+ const bytes = Buffer.alloc(Number(stat.size) + 1);
206
+ let length = 0, count = 0;
207
+ do {
208
+ count = readSync(fd, bytes, length, bytes.length - length, null);
209
+ length += count;
210
+ } while (count && length < bytes.length);
211
+ if (BigInt(length) !== stat.size || identity(fstatSync(fd, { bigint: true })) !== revision) throw new Error("edit history changed while reading");
212
+ const data = bytes.subarray(0, length);
213
+ return { revision, text: data.toString("utf8") };
214
+ } finally {
215
+ closeSync(fd);
216
+ }
217
+ }
218
+ assertCurrent() {
219
+ let revision;
220
+ try {
221
+ const stat = lstatSync(join(this.cacheDir(), "history.json"), { bigint: true });
222
+ if (!stat.isFile()) throw new Error("edit history journal is not a regular file");
223
+ revision = identity(stat);
224
+ } catch (error) {
225
+ if (!missing(error)) throw error;
226
+ revision = "missing";
227
+ }
228
+ if (revision !== this.revision) throw new Error(STALE);
229
+ }
230
+ serialized(entry) {
231
+ let saved = this.encoded.get(entry);
232
+ if (!saved) {
233
+ const text = JSON.stringify(entryValue(entry));
234
+ saved = { text, bytes: Buffer.byteLength(text) };
235
+ this.encoded.set(entry, saved);
236
+ }
237
+ return saved;
238
+ }
239
+ bound(undo, redo) {
240
+ const revision = randomUUID();
241
+ const sized = (stack) => stack.map((entry) => ({ entry, bytes: this.serialized(entry).bytes }));
242
+ const keepUndo = sized(undo), keepRedo = sized(redo);
243
+ let bytes = Buffer.byteLength(encode([], [], revision)) + [...keepUndo, ...keepRedo].reduce((sum, entry) => sum + entry.bytes, 0);
244
+ const separators = () => Math.max(0, keepUndo.length - 1) + Math.max(0, keepRedo.length - 1);
245
+ while (keepUndo.length + keepRedo.length > this.maxEntries || bytes + separators() > this.maxBytes) {
246
+ const removed = keepUndo.length > 1 || !keepRedo.length ? keepUndo.shift() : keepRedo.shift();
247
+ if (!removed) throw new Error("edit history size limit exceeded");
248
+ bytes -= removed.bytes;
249
+ }
250
+ undo = keepUndo.map(({ entry }) => entry);
251
+ redo = keepRedo.map(({ entry }) => entry);
252
+ return { undo, redo, text: encode(undo, redo, revision, (entry) => this.serialized(entry).text) };
253
+ }
254
+ publish(undo, redo) {
255
+ const next = this.bound(undo, redo);
256
+ this.assertCurrent();
257
+ const cache = this.cacheDir();
258
+ mkdirSync(cache, { recursive: true, mode: 448 });
259
+ const target = join(cache, "history.json");
260
+ try {
261
+ this.revision = this.atomicWrite(target, next.text, 384);
262
+ } catch (error) {
263
+ throw new Error(`cannot save edit history: ${message(error)}`, { cause: error });
264
+ }
265
+ this.undoStack = next.undo;
266
+ this.redoStack = next.redo;
267
+ }
268
+ atomicWrite(target, text, mode) {
269
+ const tmp = `${target}.higgsedit-${process.pid}-${randomUUID()}.tmp`;
270
+ let published = false;
271
+ let fd;
272
+ try {
273
+ fd = openSync(tmp, "wx", mode);
274
+ writeFileSync(fd, text);
275
+ fsyncSync(fd);
276
+ renameSync(tmp, target);
277
+ published = true;
278
+ return identity(fstatSync(fd, { bigint: true }));
279
+ } finally {
280
+ try {
281
+ if (fd !== void 0) closeSync(fd);
282
+ } finally {
283
+ if (!published) {
284
+ try {
285
+ unlinkSync(tmp);
286
+ } catch {
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ export {
294
+ EditHistory
295
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higgsfield/video-editor-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Higgsfield Video Editor as a local MCP server over stdio: edits TSX video projects on this machine with the higgsedit CLI and serves the editor panel.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "higgsfield-video-editor-local",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Edit and render video projects on your Mac with Higgsfield. Arrange clips, add text, audio and motion graphics, preview your timeline in the interactive editor, and render finished videos. Generate images and videos in the editor by connecting your Higgsfield account.",
5
5
  "author": {
6
6
  "name": "Higgsfield AI, Inc."
@@ -37,5 +37,5 @@
37
37
  }
38
38
  }
39
39
  },
40
- "version": "1.0.4"
40
+ "version": "1.0.6"
41
41
  }