videodraft 0.15.0 → 0.15.1

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 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
- /** Forget the current token; the next call re-initialises. */
45
- reset(): void;
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
- /** Drop every stored connection session (all scopes) for this config dir. */
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.writeFileSync(lock, String(process.pid), { mode: 384, flag: "wx" });
301
- return true;
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") return false;
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 false;
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, String(process.pid), {
311
- mode: 384,
312
- flag: "wx"
313
- });
314
- return true;
299
+ fs2.writeFileSync(lock, owner, { mode: 384, flag: "wx" });
300
+ return owner;
315
301
  } catch {
316
- return false;
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.rmSync(`${file}.lock`, { force: true });
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
- if (acquireReclaimLock(file)) {
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 && current.token === record.token) {
352
- writeRecord(file, {
353
- ...current,
354
- lastUsedAt: new Date(now()).toISOString()
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
- } catch {
388
+ return current.token;
358
389
  } finally {
359
- releaseReclaimLock(file);
390
+ releaseReclaimLock(file, owner);
360
391
  }
361
392
  }
362
- return record.token;
393
+ return null;
363
394
  },
364
395
  save(token) {
365
396
  try {
366
- if (claimRecord(file, fresh(token))) return token;
367
- const winner = readRecord(file);
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())) return current.token;
383
- sleepSync(RECLAIM_POLL_MS);
384
- }
385
- return token;
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 token;
411
+ return null;
388
412
  }
389
413
  },
390
414
  replace(token) {
391
415
  try {
392
- writeRecord(file, fresh(token));
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
- fs2.rmSync(file, { force: true });
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
- fs2.rmSync(path2.join(dir, name), { force: true });
421
- removed += 1;
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
- const stored = this.session.load();
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;
package/dist/index.js CHANGED
@@ -24,8 +24,8 @@ function readVersionFromDisk() {
24
24
  }
25
25
  }
26
26
  function resolveVersion() {
27
- if ("0.15.0") {
28
- return "0.15.0";
27
+ if ("0.15.1") {
28
+ return "0.15.1";
29
29
  }
30
30
  return readVersionFromDisk();
31
31
  }
@@ -663,7 +663,7 @@ async function revokeToken(baseUrl, token, fetchImpl = fetch) {
663
663
  // src/core/session.ts
664
664
  import fs3 from "fs";
665
665
  import path2 from "path";
666
- import { createHash } from "crypto";
666
+ import { createHash, randomBytes } from "crypto";
667
667
  var SESSION_IDLE_MS = 12 * 60 * 60 * 1e3;
668
668
  var MCP_SESSION_HEADER = "Mcp-Session-Id";
669
669
  function sessionsDir(env) {
@@ -717,19 +717,6 @@ function writeRecord(file, record) {
717
717
  fs3.writeFileSync(tmp, JSON.stringify(record, null, 2), { mode: 384 });
718
718
  fs3.renameSync(tmp, file);
719
719
  }
720
- function claimRecord(file, record) {
721
- fs3.mkdirSync(path2.dirname(file), { recursive: true, mode: 448 });
722
- try {
723
- fs3.writeFileSync(file, JSON.stringify(record, null, 2), {
724
- mode: 384,
725
- flag: "wx"
726
- });
727
- return true;
728
- } catch (err) {
729
- if (err?.code === "EEXIST") return false;
730
- throw err;
731
- }
732
- }
733
720
  function isLive(record, nowMs) {
734
721
  const idle = nowMs - Date.parse(record.lastUsedAt);
735
722
  return Number.isFinite(idle) && idle <= SESSION_IDLE_MS;
@@ -745,33 +732,61 @@ function sleepSync(ms) {
745
732
  }
746
733
  function acquireReclaimLock(file) {
747
734
  const lock = `${file}.lock`;
735
+ const owner = `${process.pid}:${randomBytes(6).toString("hex")}`;
748
736
  try {
749
- fs3.writeFileSync(lock, String(process.pid), { mode: 384, flag: "wx" });
750
- return true;
737
+ fs3.mkdirSync(path2.dirname(lock), { recursive: true, mode: 448 });
738
+ fs3.writeFileSync(lock, owner, { mode: 384, flag: "wx" });
739
+ return owner;
751
740
  } catch (err) {
752
- if (err?.code !== "EEXIST") return false;
741
+ if (err?.code !== "EEXIST") throw err;
753
742
  try {
754
743
  const age = Date.now() - fs3.statSync(lock).mtimeMs;
755
- if (age <= RECLAIM_LOCK_STALE_MS) return false;
744
+ if (age <= RECLAIM_LOCK_STALE_MS) return null;
756
745
  const claimed = `${lock}.break.${process.pid}`;
757
746
  fs3.renameSync(lock, claimed);
758
747
  fs3.rmSync(claimed, { force: true });
759
- fs3.writeFileSync(lock, String(process.pid), {
760
- mode: 384,
761
- flag: "wx"
762
- });
763
- return true;
748
+ fs3.writeFileSync(lock, owner, { mode: 384, flag: "wx" });
749
+ return owner;
764
750
  } catch {
765
- return false;
751
+ return null;
766
752
  }
767
753
  }
768
754
  }
769
- function releaseReclaimLock(file) {
755
+ function releaseReclaimLock(file, owner) {
756
+ const lock = `${file}.lock`;
770
757
  try {
771
- fs3.rmSync(`${file}.lock`, { force: true });
758
+ if (fs3.readFileSync(lock, "utf8") !== owner) return;
759
+ fs3.rmSync(lock, { force: true });
772
760
  } catch {
773
761
  }
774
762
  }
763
+ function withScopeLock(file, fn) {
764
+ const deadline = Date.now() + RECLAIM_LOCK_STALE_MS + RECLAIM_WAIT_MS;
765
+ let owner;
766
+ try {
767
+ owner = acquireReclaimLock(file);
768
+ while (!owner && Date.now() < deadline) {
769
+ sleepSync(RECLAIM_POLL_MS);
770
+ owner = acquireReclaimLock(file);
771
+ }
772
+ } catch {
773
+ return false;
774
+ }
775
+ if (!owner) return false;
776
+ const lockPath = `${file}.lock`;
777
+ const stillOwner = () => {
778
+ try {
779
+ return fs3.readFileSync(lockPath, "utf8") === owner;
780
+ } catch {
781
+ return false;
782
+ }
783
+ };
784
+ try {
785
+ return fn(stillOwner);
786
+ } finally {
787
+ releaseReclaimLock(file, owner);
788
+ }
789
+ }
775
790
  function createConnectionSessionStore(input) {
776
791
  const env = input.env ?? process.env;
777
792
  const now = input.now ?? Date.now;
@@ -794,58 +809,76 @@ function createConnectionSessionStore(input) {
794
809
  load() {
795
810
  const record = readRecord(file);
796
811
  if (!record || !isLive(record, now())) return null;
797
- if (acquireReclaimLock(file)) {
812
+ const quickDeadline = Date.now() + 5 * RECLAIM_POLL_MS;
813
+ let owner;
814
+ try {
815
+ owner = acquireReclaimLock(file);
816
+ while (!owner && Date.now() < quickDeadline) {
817
+ sleepSync(RECLAIM_POLL_MS);
818
+ owner = acquireReclaimLock(file);
819
+ }
820
+ } catch {
821
+ return record.token;
822
+ }
823
+ if (owner) {
798
824
  try {
799
825
  const current = readRecord(file);
800
- if (current && current.token === record.token) {
801
- writeRecord(file, {
802
- ...current,
803
- lastUsedAt: new Date(now()).toISOString()
804
- });
826
+ if (!current || !isLive(current, now())) return null;
827
+ try {
828
+ const lockPath = `${file}.lock`;
829
+ if (fs3.readFileSync(lockPath, "utf8") === owner) {
830
+ writeRecord(file, {
831
+ ...current,
832
+ lastUsedAt: new Date(now()).toISOString()
833
+ });
834
+ }
835
+ } catch {
805
836
  }
806
- } catch {
837
+ return current.token;
807
838
  } finally {
808
- releaseReclaimLock(file);
839
+ releaseReclaimLock(file, owner);
809
840
  }
810
841
  }
811
- return record.token;
842
+ return null;
812
843
  },
813
844
  save(token) {
814
845
  try {
815
- if (claimRecord(file, fresh(token))) return token;
816
- const winner = readRecord(file);
817
- if (winner && isLive(winner, now())) return winner.token;
818
- if (acquireReclaimLock(file)) {
819
- try {
820
- const current = readRecord(file);
821
- if (current && isLive(current, now())) return current.token;
822
- writeRecord(file, fresh(token));
823
- return token;
824
- } finally {
825
- releaseReclaimLock(file);
826
- }
827
- }
828
- const deadline = Date.now() + RECLAIM_WAIT_MS;
829
- while (Date.now() < deadline) {
846
+ let result = null;
847
+ const ran = withScopeLock(file, (stillOwner) => {
830
848
  const current = readRecord(file);
831
- if (current && isLive(current, now())) return current.token;
832
- sleepSync(RECLAIM_POLL_MS);
833
- }
834
- return token;
849
+ if (current && isLive(current, now())) {
850
+ result = current.token;
851
+ return true;
852
+ }
853
+ if (!stillOwner()) return false;
854
+ writeRecord(file, fresh(token));
855
+ result = token;
856
+ return true;
857
+ });
858
+ return ran ? result : null;
835
859
  } catch {
836
- return token;
860
+ return null;
837
861
  }
838
862
  },
839
863
  replace(token) {
840
864
  try {
841
- writeRecord(file, fresh(token));
865
+ withScopeLock(file, (stillOwner) => {
866
+ if (!stillOwner()) return false;
867
+ writeRecord(file, fresh(token));
868
+ return true;
869
+ });
842
870
  } catch {
843
871
  }
844
872
  },
845
873
  reset() {
846
874
  try {
847
- fs3.rmSync(file, { force: true });
875
+ return withScopeLock(file, (stillOwner) => {
876
+ if (!stillOwner()) return false;
877
+ fs3.rmSync(file, { force: true });
878
+ return true;
879
+ });
848
880
  } catch {
881
+ return false;
849
882
  }
850
883
  },
851
884
  describe() {
@@ -866,8 +899,17 @@ function resetAllConnectionSessions(env = process.env) {
866
899
  try {
867
900
  for (const name of fs3.readdirSync(dir)) {
868
901
  if (!name.endsWith(".json")) continue;
869
- fs3.rmSync(path2.join(dir, name), { force: true });
870
- removed += 1;
902
+ const file = path2.join(dir, name);
903
+ try {
904
+ if (withScopeLock(file, (stillOwner) => {
905
+ if (!stillOwner()) return false;
906
+ fs3.rmSync(file, { force: true });
907
+ return true;
908
+ })) {
909
+ removed += 1;
910
+ }
911
+ } catch {
912
+ }
871
913
  }
872
914
  } catch {
873
915
  }
@@ -911,7 +953,12 @@ var VideoDraftClient = class {
911
953
  if (!this.session || this.sessionId !== void 0) return;
912
954
  if (!this.handshake) {
913
955
  this.handshake = (async () => {
914
- const stored = this.session.load();
956
+ let stored = null;
957
+ try {
958
+ stored = this.session.load();
959
+ } catch {
960
+ stored = null;
961
+ }
915
962
  if (stored) {
916
963
  this.sessionId = stored;
917
964
  return;
@@ -1199,6 +1246,9 @@ var VideoDraftClient = class {
1199
1246
  }
1200
1247
  };
1201
1248
 
1249
+ // src/cli/context.ts
1250
+ import { createHash as createHash2 } from "crypto";
1251
+
1202
1252
  // src/auth/token-provider.ts
1203
1253
  var EXPIRY_SKEW_MS = 6e4;
1204
1254
  function isExpiring(profile) {
@@ -1312,6 +1362,14 @@ function sessionArg(command, opts) {
1312
1362
  }
1313
1363
  return opts.session;
1314
1364
  }
1365
+ function sessionProfileKey(profileName, explicitToken) {
1366
+ if (profileName) return `profile:${profileName}`;
1367
+ if (explicitToken) {
1368
+ const fp = createHash2("sha256").update(explicitToken).digest("hex").slice(0, 16);
1369
+ return `token:${fp}`;
1370
+ }
1371
+ return "default";
1372
+ }
1315
1373
  function connectionSessionEnabled(env = process.env) {
1316
1374
  const v = env.VIDEODRAFT_NO_SESSION?.trim().toLowerCase();
1317
1375
  return !(v === "1" || v === "true" || v === "yes");
@@ -1342,7 +1400,10 @@ function buildContext(command) {
1342
1400
  });
1343
1401
  const session = connectionSessionEnabled() ? createConnectionSessionStore({
1344
1402
  baseUrl: auth.baseUrl,
1345
- profile: auth.profileName ?? "default"
1403
+ profile: sessionProfileKey(
1404
+ auth.profileName,
1405
+ flags.token ?? process.env.VIDEODRAFT_API_KEY
1406
+ )
1346
1407
  }) : void 0;
1347
1408
  const client = new VideoDraftClient({
1348
1409
  tokenProvider: auth.tokenProvider,
@@ -1887,25 +1948,33 @@ ${section.toUpperCase()}
1887
1948
  "Show the connection session this directory's standalone generations are filed under"
1888
1949
  ).action(async function() {
1889
1950
  const ctx = buildContext(this);
1890
- const info = ctx.session?.describe();
1891
- const record = info?.record ?? null;
1892
- const active = Boolean(record) && !info?.expired;
1893
- const sessionId = active ? info?.sessionId ?? null : null;
1894
- let created = false;
1895
- if (sessionId) {
1951
+ const knownIds = /* @__PURE__ */ new Set();
1952
+ let listed = false;
1953
+ if (ctx.session) {
1896
1954
  try {
1897
- const listed = await ctx.client.callTool(
1898
- "list_ai_studio_sessions",
1899
- { limit: 200 }
1900
- );
1901
- const rows = listed?.sessions ?? [];
1902
- created = rows.some(
1903
- (row) => (row?.id ?? row?.session_id) === sessionId
1904
- );
1955
+ for (let offset = 0; offset < 2e3; ) {
1956
+ const page = await ctx.client.callTool(
1957
+ "list_ai_studio_sessions",
1958
+ { limit: 200, offset }
1959
+ );
1960
+ const pageRows = page?.sessions ?? [];
1961
+ for (const row of pageRows) {
1962
+ const id = row?.id ?? row?.session_id;
1963
+ if (typeof id === "string") knownIds.add(id);
1964
+ }
1965
+ if (pageRows.length < 200) break;
1966
+ offset += pageRows.length;
1967
+ }
1968
+ listed = true;
1905
1969
  } catch {
1906
- created = false;
1970
+ listed = false;
1907
1971
  }
1908
1972
  }
1973
+ const info = ctx.session?.describe();
1974
+ const record = info?.record ?? null;
1975
+ const active = Boolean(record) && !info?.expired;
1976
+ const sessionId = active ? info?.sessionId ?? null : null;
1977
+ const created = Boolean(sessionId) && listed && knownIds.has(sessionId);
1909
1978
  const result = {
1910
1979
  enabled: Boolean(ctx.session),
1911
1980
  scope: info?.scope ?? null,
@@ -1936,21 +2005,24 @@ ${result.url ? `${result.url}
1936
2005
  const ctx = buildContext(this);
1937
2006
  const opts = this.opts();
1938
2007
  let removed = 0;
2008
+ let declined = false;
1939
2009
  if (opts.all) {
1940
2010
  removed = resetAllConnectionSessions();
1941
2011
  } else if (ctx.session) {
1942
2012
  const had = Boolean(ctx.session.describe().record);
1943
- ctx.session.reset();
1944
- removed = had ? 1 : 0;
2013
+ const ran = ctx.session.reset();
2014
+ removed = had && ran ? 1 : 0;
2015
+ declined = had && !ran;
1945
2016
  }
1946
2017
  const result = {
1947
2018
  reset: removed,
2019
+ declined,
1948
2020
  scope: opts.all ? "all" : ctx.session?.describe().scope ?? null
1949
2021
  };
1950
2022
  emit(ctx.out, result, () => {
1951
2023
  process.stdout.write(
1952
2024
  removed > 0 ? `Reset ${removed} connection session${removed === 1 ? "" : "s"}; the next generation starts a new AI Studio session.
1953
- ` : "Nothing to reset.\n"
2025
+ ` : declined ? "Could not reset: the session store is locked or unwritable; the session may still be in use.\n" : "Nothing to reset.\n"
1954
2026
  );
1955
2027
  });
1956
2028
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "videodraft",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Official VideoDraft CLI — create AI videos, images and audio from your terminal. Agent-friendly: --json everywhere, stable exit codes, async job polling.",
5
5
  "license": "MIT",
6
6
  "type": "module",