borgmcp 2.2.0 → 2.4.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.
Files changed (106) hide show
  1. package/README.md +36 -0
  2. package/dist/assimilate-cmd.d.ts +6 -0
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +31 -7
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts.map +1 -1
  7. package/dist/assimilate-deps.js +2 -0
  8. package/dist/assimilate-deps.js.map +1 -1
  9. package/dist/claude.d.ts.map +1 -1
  10. package/dist/claude.js +14 -0
  11. package/dist/claude.js.map +1 -1
  12. package/dist/cli-help.d.ts +2 -0
  13. package/dist/cli-help.d.ts.map +1 -1
  14. package/dist/cli-help.js +19 -0
  15. package/dist/cli-help.js.map +1 -1
  16. package/dist/cli-platform.d.ts +1 -0
  17. package/dist/cli-platform.d.ts.map +1 -1
  18. package/dist/cli-platform.js +5 -1
  19. package/dist/cli-platform.js.map +1 -1
  20. package/dist/console-prefix.d.ts +9 -10
  21. package/dist/console-prefix.d.ts.map +1 -1
  22. package/dist/console-prefix.js +23 -20
  23. package/dist/console-prefix.js.map +1 -1
  24. package/dist/cubes.d.ts +11 -0
  25. package/dist/cubes.d.ts.map +1 -1
  26. package/dist/cubes.js +10 -0
  27. package/dist/cubes.js.map +1 -1
  28. package/dist/display-identity.d.ts +34 -0
  29. package/dist/display-identity.d.ts.map +1 -0
  30. package/dist/display-identity.js +88 -0
  31. package/dist/display-identity.js.map +1 -0
  32. package/dist/docs-sections.d.ts.map +1 -1
  33. package/dist/docs-sections.js +9 -0
  34. package/dist/docs-sections.js.map +1 -1
  35. package/dist/drone-management.d.ts.map +1 -1
  36. package/dist/drone-management.js +6 -0
  37. package/dist/drone-management.js.map +1 -1
  38. package/dist/inbox-monitor.d.ts.map +1 -1
  39. package/dist/inbox-monitor.js +12 -0
  40. package/dist/inbox-monitor.js.map +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +58 -16
  43. package/dist/index.js.map +1 -1
  44. package/dist/log-stream.d.ts.map +1 -1
  45. package/dist/log-stream.js +41 -15
  46. package/dist/log-stream.js.map +1 -1
  47. package/dist/parse-assimilate-args.d.ts +1 -1
  48. package/dist/parse-assimilate-args.d.ts.map +1 -1
  49. package/dist/parse-assimilate-args.js +5 -2
  50. package/dist/parse-assimilate-args.js.map +1 -1
  51. package/dist/regen-format.d.ts +2 -2
  52. package/dist/regen-format.d.ts.map +1 -1
  53. package/dist/regen-format.js +6 -5
  54. package/dist/regen-format.js.map +1 -1
  55. package/dist/regen.js +6 -2
  56. package/dist/regen.js.map +1 -1
  57. package/dist/remote-client.d.ts.map +1 -1
  58. package/dist/remote-client.js +8 -1
  59. package/dist/remote-client.js.map +1 -1
  60. package/dist/reset-local-seat-cmd.d.ts +8 -0
  61. package/dist/reset-local-seat-cmd.d.ts.map +1 -1
  62. package/dist/reset-local-seat-cmd.js +21 -2
  63. package/dist/reset-local-seat-cmd.js.map +1 -1
  64. package/dist/seat-reattach-guard.d.ts +21 -0
  65. package/dist/seat-reattach-guard.d.ts.map +1 -0
  66. package/dist/seat-reattach-guard.js +83 -0
  67. package/dist/seat-reattach-guard.js.map +1 -0
  68. package/dist/seats.d.ts +6 -2
  69. package/dist/seats.d.ts.map +1 -1
  70. package/dist/seats.js +37 -14
  71. package/dist/seats.js.map +1 -1
  72. package/dist/unknown-subcommand.d.ts +1 -1
  73. package/dist/unknown-subcommand.d.ts.map +1 -1
  74. package/dist/unknown-subcommand.js +1 -0
  75. package/dist/unknown-subcommand.js.map +1 -1
  76. package/dist/update-cmd.d.ts +58 -0
  77. package/dist/update-cmd.d.ts.map +1 -0
  78. package/dist/update-cmd.js +801 -0
  79. package/dist/update-cmd.js.map +1 -0
  80. package/docs/EXTRACTION_PROVENANCE.md +3 -3
  81. package/docs/LOCAL_SERVER.md +43 -0
  82. package/docs/RELEASING.md +145 -4
  83. package/docs/SEAT_LIFECYCLE.md +242 -0
  84. package/package.json +6 -3
  85. package/src/assimilate-cmd.ts +45 -7
  86. package/src/assimilate-deps.ts +2 -0
  87. package/src/claude.ts +15 -0
  88. package/src/cli-help.ts +22 -0
  89. package/src/cli-platform.ts +5 -2
  90. package/src/console-prefix.ts +25 -18
  91. package/src/cubes.ts +16 -0
  92. package/src/display-identity.ts +130 -0
  93. package/src/docs-sections.ts +9 -0
  94. package/src/drone-management.ts +6 -0
  95. package/src/inbox-monitor.ts +19 -0
  96. package/src/index.ts +66 -20
  97. package/src/log-stream.ts +38 -14
  98. package/src/parse-assimilate-args.ts +4 -2
  99. package/src/regen-format.ts +6 -5
  100. package/src/regen.ts +11 -2
  101. package/src/remote-client.ts +8 -0
  102. package/src/reset-local-seat-cmd.ts +26 -1
  103. package/src/seat-reattach-guard.ts +113 -0
  104. package/src/seats.ts +36 -14
  105. package/src/unknown-subcommand.ts +1 -0
  106. package/src/update-cmd.ts +1006 -0
package/src/seats.ts CHANGED
@@ -98,6 +98,8 @@ function emptyStore(): SeatsFile {
98
98
 
99
99
  const ROLE_CLASSES = new Set(['queen', 'worker']);
100
100
  const OPERATION_KINDS = new Set(['seat', 'sibling']);
101
+ const SEAT_BEARER_RE = /^[A-Za-z0-9_-]{43,1024}$/;
102
+ const rejectedSeatRefs = new Set<string>();
101
103
 
102
104
  function isNonEmptyString(value: unknown): value is string {
103
105
  return typeof value === 'string' && value.length > 0;
@@ -117,10 +119,10 @@ function isValidOperation(value: unknown): value is SeatOperation {
117
119
  /**
118
120
  * CR#2: FULL per-entry validation. Every key/value/invariant of a seat record is
119
121
  * checked — the ref is well-formed and self-consistent (the map key equals the
120
- * record's derived ref), state ∈ {pending,active}, the credential is a non-empty
121
- * string, the operation is well-shaped, an ACTIVE record carries ALL its required
122
- * server + binding fields, and a PENDING record carries NO active-only session
123
- * id. A single invalid entry ⇒ the whole store is rejected
122
+ * record's derived ref), state ∈ {pending,active}, the credential is an exact
123
+ * 32-byte base64url bearer, the operation is well-shaped, an ACTIVE record carries
124
+ * ALL its required server + binding fields, and a PENDING record carries NO
125
+ * active-only session id. A single invalid entry ⇒ the whole store is rejected
124
126
  * (fail closed at the caller, bytes preserved) — never a silent cast.
125
127
  */
126
128
  function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
@@ -134,7 +136,8 @@ function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
134
136
  !isNonEmptyString(r.cubeId) ||
135
137
  !isNonEmptyString(r.roleId) ||
136
138
  !isValidOperation(r.operation) ||
137
- !isNonEmptyString(r.credential)
139
+ typeof r.credential !== 'string' ||
140
+ !SEAT_BEARER_RE.test(r.credential)
138
141
  ) {
139
142
  return false;
140
143
  }
@@ -204,7 +207,7 @@ function parseStore(raw: string): SeatsFile | null {
204
207
  !Array.isArray(replacement) &&
205
208
  Object.keys(replacement).length === 1 &&
206
209
  typeof (replacement as { credential?: unknown }).credential === 'string' &&
207
- /^[A-Za-z0-9_-]{43}$/.test((replacement as { credential: string }).credential);
210
+ SEAT_BEARER_RE.test((replacement as { credential: string }).credential);
208
211
  let canonicalOrigin = false;
209
212
  try {
210
213
  const origin = new URL(String(withoutReplacement.origin));
@@ -338,6 +341,7 @@ export async function mintPendingSeat(input: {
338
341
  };
339
342
  txn.data.seats[ref] = record;
340
343
  await txn.commit();
344
+ rejectedSeatRefs.delete(ref);
341
345
  return record;
342
346
  });
343
347
  }
@@ -430,6 +434,7 @@ export async function prepareSeat(input: {
430
434
  };
431
435
  txn.data.seats[ref] = record;
432
436
  await txn.commit();
437
+ rejectedSeatRefs.delete(ref);
433
438
  return { ok: true as const, record };
434
439
  });
435
440
  }
@@ -498,6 +503,7 @@ export async function activateAndBindSeat(input: {
498
503
  ...(input.isHumanSeat !== undefined ? { isHumanSeat: input.isHumanSeat } : {}),
499
504
  };
500
505
  await txn.commit();
506
+ rejectedSeatRefs.delete(ref);
501
507
  return 'activated';
502
508
  });
503
509
  }
@@ -567,16 +573,28 @@ export async function bindPendingSeatToWorktree(input: {
567
573
 
568
574
  // ─── Hydration / enumeration (scan by worktree) ──────────────────────────────
569
575
 
570
- /** The exact ACTIVE seat bound to `worktree`, or null. A pending record (no
571
- * worktree, or non-active) is NEVER surfaced as a live binding. */
576
+ /** The preferred ACTIVE seat bound to `worktree`, or null. A pending record (no
577
+ * worktree, or non-active) is NEVER surfaced as a live binding. Candidates use
578
+ * one total order across every process: unrejected before rejected, a sibling
579
+ * finalized into this worktree before an older in-place binding, then seat ref. */
572
580
  export async function getActiveSeatForWorktree(worktree: string): Promise<SeatRecord | null> {
573
581
  const store = await readStore();
574
- for (const [ref, record] of Object.entries(store.seats)) {
575
- if (record.state === 'active' && record.worktree === worktree && seatRef(record) === ref) {
576
- return record;
577
- }
578
- }
579
- return null;
582
+ const candidates = Object.entries(store.seats)
583
+ .filter(([ref, record]) =>
584
+ record.state === 'active' && record.worktree === worktree && seatRef(record) === ref)
585
+ .sort(([leftRef, left], [rightRef, right]) => {
586
+ const rejectionOrder = Number(rejectedSeatRefs.has(leftRef)) - Number(rejectedSeatRefs.has(rightRef));
587
+ if (rejectionOrder !== 0) return rejectionOrder;
588
+ const operationOrder = Number(left.operation.kind === 'seat') - Number(right.operation.kind === 'seat');
589
+ if (operationOrder !== 0) return operationOrder;
590
+ return leftRef < rightRef ? -1 : leftRef > rightRef ? 1 : 0;
591
+ });
592
+ return candidates[0]?.[1] ?? null;
593
+ }
594
+
595
+ /** Deprioritize an exact seat after a definitive server auth or eviction verdict. */
596
+ export function markSeatRejected(ref: string): void {
597
+ if (REF_RE.test(ref)) rejectedSeatRefs.add(ref);
580
598
  }
581
599
 
582
600
  /**
@@ -624,6 +642,8 @@ export async function findIncompleteSiblingAttempt(binding: {
624
642
  record.state === 'pending' &&
625
643
  record.worktree === undefined &&
626
644
  record.operation.kind === 'sibling' &&
645
+ record.operation.operationKey.startsWith('implicit-sibling:') &&
646
+ record.operation.operationKey.length > 'implicit-sibling:'.length &&
627
647
  record.origin === binding.origin &&
628
648
  record.trustIdentity === binding.trustIdentity &&
629
649
  record.cubeId === binding.cubeId &&
@@ -696,6 +716,7 @@ export async function resetSeatForWorktree(expected: {
696
716
  }
697
717
  delete txn.data.seats[expected.ref];
698
718
  await txn.commit();
719
+ rejectedSeatRefs.delete(expected.ref);
699
720
  return { outcome: 'reset' as const, ref: expected.ref };
700
721
  });
701
722
  }
@@ -726,6 +747,7 @@ export async function clearSeat(ref: string): Promise<void> {
726
747
  if (txn.data.seats[ref] !== undefined) {
727
748
  delete txn.data.seats[ref];
728
749
  await txn.commit();
750
+ rejectedSeatRefs.delete(ref);
729
751
  }
730
752
  });
731
753
  }
@@ -14,6 +14,7 @@
14
14
  /** The subcommands the claude.ts router dispatches on (lines 107-176). */
15
15
  export const KNOWN_SUBCOMMANDS = [
16
16
  'setup',
17
+ 'update',
17
18
  'assimilate',
18
19
  'reset-local-seat',
19
20
  'spawn',