videodraft 0.15.0 → 0.16.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/dist/client.d.ts +18 -5
- package/dist/client.js +107 -60
- package/dist/index.js +304 -101
- package/package.json +1 -1
- package/skills/index.json +8 -8
- package/skills/videodraft/SKILL.md +7 -2
- package/skills/videodraft/references/examples.md +1 -1
- package/skills/videodraft/references/models.md +9 -7
- package/skills/videodraft/references/pipeline.md +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -36,13 +36,21 @@ interface ConnectionSessionStore {
|
|
|
36
36
|
* Persist a freshly minted token for this scope and return the token that
|
|
37
37
|
* now owns the scope. Two processes racing on an empty scope both mint;
|
|
38
38
|
* the first writer wins and the second gets the winner's token back, so
|
|
39
|
-
* one directory never ends up split across two sessions.
|
|
39
|
+
* one directory never ends up split across two sessions. Returns null
|
|
40
|
+
* when the token could NOT be persisted (read-only config dir): the
|
|
41
|
+
* caller must then go stateless — sending an unpersisted token would
|
|
42
|
+
* create a fresh AI Studio session per invocation, when the documented
|
|
43
|
+
* degraded mode is the shared server-side fallback.
|
|
40
44
|
*/
|
|
41
|
-
save(token: string): string;
|
|
45
|
+
save(token: string): string | null;
|
|
42
46
|
/** Replace the scope's token unconditionally (server re-mint). */
|
|
43
47
|
replace(token: string): void;
|
|
44
|
-
/**
|
|
45
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Forget the current token; the next call re-initialises. Returns whether
|
|
50
|
+
* the deletion actually ran — false means the scope lock could not be
|
|
51
|
+
* taken (or the filesystem refused) and the record may still exist.
|
|
52
|
+
*/
|
|
53
|
+
reset(): boolean;
|
|
46
54
|
/** Where this scope's record lives (for `sessions current`). */
|
|
47
55
|
describe(): {
|
|
48
56
|
scope: string;
|
|
@@ -64,7 +72,12 @@ interface SessionScopeInput {
|
|
|
64
72
|
/** Stable label for this scope; what the record is keyed by. */
|
|
65
73
|
declare function sessionScope(input: SessionScopeInput): string;
|
|
66
74
|
declare function createConnectionSessionStore(input: SessionScopeInput): ConnectionSessionStore;
|
|
67
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Drop every stored connection session (all scopes) for this config dir.
|
|
77
|
+
* Each record is removed under its per-scope lock — the same protocol as
|
|
78
|
+
* the single-scope reset — so a concurrent touch/replace that already read
|
|
79
|
+
* the old record cannot recreate it right after the sweep deletes it.
|
|
80
|
+
*/
|
|
68
81
|
declare function resetAllConnectionSessions(env?: NodeJS.ProcessEnv): number;
|
|
69
82
|
|
|
70
83
|
/**
|
package/dist/client.js
CHANGED
|
@@ -214,7 +214,7 @@ async function withLock(lockName, fn, env = process.env) {
|
|
|
214
214
|
// src/core/session.ts
|
|
215
215
|
import fs2 from "fs";
|
|
216
216
|
import path2 from "path";
|
|
217
|
-
import { createHash } from "crypto";
|
|
217
|
+
import { createHash, randomBytes } from "crypto";
|
|
218
218
|
var SESSION_IDLE_MS = 12 * 60 * 60 * 1e3;
|
|
219
219
|
var MCP_SESSION_HEADER = "Mcp-Session-Id";
|
|
220
220
|
function sessionsDir(env) {
|
|
@@ -268,19 +268,6 @@ function writeRecord(file, record) {
|
|
|
268
268
|
fs2.writeFileSync(tmp, JSON.stringify(record, null, 2), { mode: 384 });
|
|
269
269
|
fs2.renameSync(tmp, file);
|
|
270
270
|
}
|
|
271
|
-
function claimRecord(file, record) {
|
|
272
|
-
fs2.mkdirSync(path2.dirname(file), { recursive: true, mode: 448 });
|
|
273
|
-
try {
|
|
274
|
-
fs2.writeFileSync(file, JSON.stringify(record, null, 2), {
|
|
275
|
-
mode: 384,
|
|
276
|
-
flag: "wx"
|
|
277
|
-
});
|
|
278
|
-
return true;
|
|
279
|
-
} catch (err) {
|
|
280
|
-
if (err?.code === "EEXIST") return false;
|
|
281
|
-
throw err;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
271
|
function isLive(record, nowMs) {
|
|
285
272
|
const idle = nowMs - Date.parse(record.lastUsedAt);
|
|
286
273
|
return Number.isFinite(idle) && idle <= SESSION_IDLE_MS;
|
|
@@ -296,33 +283,61 @@ function sleepSync(ms) {
|
|
|
296
283
|
}
|
|
297
284
|
function acquireReclaimLock(file) {
|
|
298
285
|
const lock = `${file}.lock`;
|
|
286
|
+
const owner = `${process.pid}:${randomBytes(6).toString("hex")}`;
|
|
299
287
|
try {
|
|
300
|
-
fs2.
|
|
301
|
-
|
|
288
|
+
fs2.mkdirSync(path2.dirname(lock), { recursive: true, mode: 448 });
|
|
289
|
+
fs2.writeFileSync(lock, owner, { mode: 384, flag: "wx" });
|
|
290
|
+
return owner;
|
|
302
291
|
} catch (err) {
|
|
303
|
-
if (err?.code !== "EEXIST")
|
|
292
|
+
if (err?.code !== "EEXIST") throw err;
|
|
304
293
|
try {
|
|
305
294
|
const age = Date.now() - fs2.statSync(lock).mtimeMs;
|
|
306
|
-
if (age <= RECLAIM_LOCK_STALE_MS) return
|
|
295
|
+
if (age <= RECLAIM_LOCK_STALE_MS) return null;
|
|
307
296
|
const claimed = `${lock}.break.${process.pid}`;
|
|
308
297
|
fs2.renameSync(lock, claimed);
|
|
309
298
|
fs2.rmSync(claimed, { force: true });
|
|
310
|
-
fs2.writeFileSync(lock,
|
|
311
|
-
|
|
312
|
-
flag: "wx"
|
|
313
|
-
});
|
|
314
|
-
return true;
|
|
299
|
+
fs2.writeFileSync(lock, owner, { mode: 384, flag: "wx" });
|
|
300
|
+
return owner;
|
|
315
301
|
} catch {
|
|
316
|
-
return
|
|
302
|
+
return null;
|
|
317
303
|
}
|
|
318
304
|
}
|
|
319
305
|
}
|
|
320
|
-
function releaseReclaimLock(file) {
|
|
306
|
+
function releaseReclaimLock(file, owner) {
|
|
307
|
+
const lock = `${file}.lock`;
|
|
321
308
|
try {
|
|
322
|
-
fs2.
|
|
309
|
+
if (fs2.readFileSync(lock, "utf8") !== owner) return;
|
|
310
|
+
fs2.rmSync(lock, { force: true });
|
|
323
311
|
} catch {
|
|
324
312
|
}
|
|
325
313
|
}
|
|
314
|
+
function withScopeLock(file, fn) {
|
|
315
|
+
const deadline = Date.now() + RECLAIM_LOCK_STALE_MS + RECLAIM_WAIT_MS;
|
|
316
|
+
let owner;
|
|
317
|
+
try {
|
|
318
|
+
owner = acquireReclaimLock(file);
|
|
319
|
+
while (!owner && Date.now() < deadline) {
|
|
320
|
+
sleepSync(RECLAIM_POLL_MS);
|
|
321
|
+
owner = acquireReclaimLock(file);
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (!owner) return false;
|
|
327
|
+
const lockPath = `${file}.lock`;
|
|
328
|
+
const stillOwner = () => {
|
|
329
|
+
try {
|
|
330
|
+
return fs2.readFileSync(lockPath, "utf8") === owner;
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
try {
|
|
336
|
+
return fn(stillOwner);
|
|
337
|
+
} finally {
|
|
338
|
+
releaseReclaimLock(file, owner);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
326
341
|
function createConnectionSessionStore(input) {
|
|
327
342
|
const env = input.env ?? process.env;
|
|
328
343
|
const now = input.now ?? Date.now;
|
|
@@ -345,58 +360,76 @@ function createConnectionSessionStore(input) {
|
|
|
345
360
|
load() {
|
|
346
361
|
const record = readRecord(file);
|
|
347
362
|
if (!record || !isLive(record, now())) return null;
|
|
348
|
-
|
|
363
|
+
const quickDeadline = Date.now() + 5 * RECLAIM_POLL_MS;
|
|
364
|
+
let owner;
|
|
365
|
+
try {
|
|
366
|
+
owner = acquireReclaimLock(file);
|
|
367
|
+
while (!owner && Date.now() < quickDeadline) {
|
|
368
|
+
sleepSync(RECLAIM_POLL_MS);
|
|
369
|
+
owner = acquireReclaimLock(file);
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
return record.token;
|
|
373
|
+
}
|
|
374
|
+
if (owner) {
|
|
349
375
|
try {
|
|
350
376
|
const current = readRecord(file);
|
|
351
|
-
if (current
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
377
|
+
if (!current || !isLive(current, now())) return null;
|
|
378
|
+
try {
|
|
379
|
+
const lockPath = `${file}.lock`;
|
|
380
|
+
if (fs2.readFileSync(lockPath, "utf8") === owner) {
|
|
381
|
+
writeRecord(file, {
|
|
382
|
+
...current,
|
|
383
|
+
lastUsedAt: new Date(now()).toISOString()
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
356
387
|
}
|
|
357
|
-
|
|
388
|
+
return current.token;
|
|
358
389
|
} finally {
|
|
359
|
-
releaseReclaimLock(file);
|
|
390
|
+
releaseReclaimLock(file, owner);
|
|
360
391
|
}
|
|
361
392
|
}
|
|
362
|
-
return
|
|
393
|
+
return null;
|
|
363
394
|
},
|
|
364
395
|
save(token) {
|
|
365
396
|
try {
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
if (winner && isLive(winner, now())) return winner.token;
|
|
369
|
-
if (acquireReclaimLock(file)) {
|
|
370
|
-
try {
|
|
371
|
-
const current = readRecord(file);
|
|
372
|
-
if (current && isLive(current, now())) return current.token;
|
|
373
|
-
writeRecord(file, fresh(token));
|
|
374
|
-
return token;
|
|
375
|
-
} finally {
|
|
376
|
-
releaseReclaimLock(file);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
const deadline = Date.now() + RECLAIM_WAIT_MS;
|
|
380
|
-
while (Date.now() < deadline) {
|
|
397
|
+
let result = null;
|
|
398
|
+
const ran = withScopeLock(file, (stillOwner) => {
|
|
381
399
|
const current = readRecord(file);
|
|
382
|
-
if (current && isLive(current, now()))
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
400
|
+
if (current && isLive(current, now())) {
|
|
401
|
+
result = current.token;
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
if (!stillOwner()) return false;
|
|
405
|
+
writeRecord(file, fresh(token));
|
|
406
|
+
result = token;
|
|
407
|
+
return true;
|
|
408
|
+
});
|
|
409
|
+
return ran ? result : null;
|
|
386
410
|
} catch {
|
|
387
|
-
return
|
|
411
|
+
return null;
|
|
388
412
|
}
|
|
389
413
|
},
|
|
390
414
|
replace(token) {
|
|
391
415
|
try {
|
|
392
|
-
|
|
416
|
+
withScopeLock(file, (stillOwner) => {
|
|
417
|
+
if (!stillOwner()) return false;
|
|
418
|
+
writeRecord(file, fresh(token));
|
|
419
|
+
return true;
|
|
420
|
+
});
|
|
393
421
|
} catch {
|
|
394
422
|
}
|
|
395
423
|
},
|
|
396
424
|
reset() {
|
|
397
425
|
try {
|
|
398
|
-
|
|
426
|
+
return withScopeLock(file, (stillOwner) => {
|
|
427
|
+
if (!stillOwner()) return false;
|
|
428
|
+
fs2.rmSync(file, { force: true });
|
|
429
|
+
return true;
|
|
430
|
+
});
|
|
399
431
|
} catch {
|
|
432
|
+
return false;
|
|
400
433
|
}
|
|
401
434
|
},
|
|
402
435
|
describe() {
|
|
@@ -417,8 +450,17 @@ function resetAllConnectionSessions(env = process.env) {
|
|
|
417
450
|
try {
|
|
418
451
|
for (const name of fs2.readdirSync(dir)) {
|
|
419
452
|
if (!name.endsWith(".json")) continue;
|
|
420
|
-
|
|
421
|
-
|
|
453
|
+
const file = path2.join(dir, name);
|
|
454
|
+
try {
|
|
455
|
+
if (withScopeLock(file, (stillOwner) => {
|
|
456
|
+
if (!stillOwner()) return false;
|
|
457
|
+
fs2.rmSync(file, { force: true });
|
|
458
|
+
return true;
|
|
459
|
+
})) {
|
|
460
|
+
removed += 1;
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
422
464
|
}
|
|
423
465
|
} catch {
|
|
424
466
|
}
|
|
@@ -462,7 +504,12 @@ var VideoDraftClient = class {
|
|
|
462
504
|
if (!this.session || this.sessionId !== void 0) return;
|
|
463
505
|
if (!this.handshake) {
|
|
464
506
|
this.handshake = (async () => {
|
|
465
|
-
|
|
507
|
+
let stored = null;
|
|
508
|
+
try {
|
|
509
|
+
stored = this.session.load();
|
|
510
|
+
} catch {
|
|
511
|
+
stored = null;
|
|
512
|
+
}
|
|
466
513
|
if (stored) {
|
|
467
514
|
this.sessionId = stored;
|
|
468
515
|
return;
|