@orkestrel/terminal 0.0.1 → 0.0.2

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.
@@ -1,7 +1,8 @@
1
- import { isArray, isBoolean, isNonEmptyString, isNumber, isRecord, isString, literalOf, recordOf } from "@orkestrel/contract";
1
+ import { isArray, isBoolean, isNonEmptyString, isNumber, isRecord, isString, literalOf, rawShape, recordOf, stringShape } from "@orkestrel/contract";
2
2
  import { STATUS_ICONS, createStyler, stripControls } from "@orkestrel/console";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  import { createSSEParser } from "@orkestrel/sse";
5
+ import { createDatabase, createMemoryDriver } from "@orkestrel/database";
5
6
  //#region src/core/constants.ts
6
7
  /** Carriage return (`\r`, U+000D) — Enter on most terminals. */
7
8
  var RETURN = String.fromCharCode(13);
@@ -1033,8 +1034,10 @@ var isPendingPrompt = recordOf({
1033
1034
  message: isString,
1034
1035
  options: isRecord,
1035
1036
  status: isPendingPromptStatus,
1036
- time: isNumber
1037
- });
1037
+ time: isNumber,
1038
+ from: isString,
1039
+ to: isString
1040
+ }, ["from", "to"]);
1038
1041
  /**
1039
1042
  * Rebuild a wire-decoded `validate` payload into a {@link ValidationRules} bag — the inverse of
1040
1043
  * {@link serializeValidationRules}. Keeps only the primitive rule values (`boolean` / `number` /
@@ -1232,6 +1235,37 @@ function isInsecureRemote(url) {
1232
1235
  const hostname = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0] ?? "";
1233
1236
  return hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "[::1]";
1234
1237
  }
1238
+ /** Serialize a parked {@link PendingPrompt} into a {@link WireEvent} — event `'pending'`, `data` the JSON-stringified prompt, `id` the prompt's own id (an SSE bridge sets its frame `id:` from this). */
1239
+ function serializePending(prompt) {
1240
+ return {
1241
+ event: "pending",
1242
+ data: JSON.stringify(prompt),
1243
+ id: prompt.id
1244
+ };
1245
+ }
1246
+ /** Serialize a parked prompt's expiry into a {@link WireEvent} — event `'expire'`, `data` the JSON-stringified `{ id }` payload. */
1247
+ function serializeExpire(id) {
1248
+ return {
1249
+ event: "expire",
1250
+ data: JSON.stringify({ id })
1251
+ };
1252
+ }
1253
+ /** The {@link WireEvent} a broker/manager sends when it is going away — event `'shutdown'`, no payload. */
1254
+ function serializeShutdown() {
1255
+ return {
1256
+ event: "shutdown",
1257
+ data: ""
1258
+ };
1259
+ }
1260
+ /** Narrow an unknown wire payload to an answer POST body — a non-empty `id` string plus a `value` key (of any shape) present. */
1261
+ function isAnswerPayload(value) {
1262
+ return isRecord(value) && isNonEmptyString(value.id) && "value" in value;
1263
+ }
1264
+ /** Narrow an unknown value to a {@link TerminalSnapshot} — a non-empty `id` plus an optional numeric `timeout`. */
1265
+ var isTerminalSnapshot = recordOf({
1266
+ id: isNonEmptyString,
1267
+ timeout: isNumber
1268
+ }, ["timeout"]);
1235
1269
  //#endregion
1236
1270
  //#region src/core/Prompt.ts
1237
1271
  /**
@@ -1271,12 +1305,14 @@ function isInsecureRemote(url) {
1271
1305
  var Prompt = class {
1272
1306
  #timeout;
1273
1307
  #timer;
1308
+ #cap;
1274
1309
  #parked = /* @__PURE__ */ new Map();
1275
1310
  #emitter;
1276
1311
  #destroyed = false;
1277
1312
  constructor(options) {
1278
1313
  this.#timeout = options?.timeout ?? 3e5;
1279
1314
  this.#timer = options?.timer ?? defaultTimer;
1315
+ this.#cap = options?.cap;
1280
1316
  this.#emitter = new Emitter({
1281
1317
  on: options?.on,
1282
1318
  error: options?.error
@@ -1296,43 +1332,44 @@ var Prompt = class {
1296
1332
  }
1297
1333
  answer(id, value) {
1298
1334
  const parked = this.#parked.get(id);
1299
- if (parked === void 0) return false;
1335
+ if (parked === void 0) return {
1336
+ success: false,
1337
+ error: "unknown"
1338
+ };
1300
1339
  const accepted = parked.respond(value);
1301
- if (accepted === void 0) return false;
1340
+ if (accepted === void 0) return {
1341
+ success: false,
1342
+ error: "rejected"
1343
+ };
1302
1344
  parked.cancel();
1303
1345
  this.#emitter.emit("answer", id, accepted);
1304
1346
  this.#parked.delete(id);
1305
- return true;
1347
+ return {
1348
+ success: true,
1349
+ value: accepted
1350
+ };
1351
+ }
1352
+ park(request) {
1353
+ const gate = this.#gate(request.form, request.options);
1354
+ return this.#park(request.form, request.options.message, request.options, gate, request.from, request.to);
1306
1355
  }
1307
1356
  input(options) {
1308
- const validator = resolveValidation(options.validate);
1309
- return this.#park("input", options.message, options, (value) => isString(value) && validator(value) === true ? value : void 0);
1357
+ return this.#park("input", options.message, options, this.#gate("input", options)).value;
1310
1358
  }
1311
1359
  password(options) {
1312
- const validator = resolveValidation(options.validate);
1313
- return this.#park("password", options.message, options, (value) => isString(value) && validator(value) === true ? value : void 0);
1360
+ return this.#park("password", options.message, options, this.#gate("password", options)).value;
1314
1361
  }
1315
1362
  confirm(options) {
1316
- return this.#park("confirm", options.message, options, (value) => isBoolean(value) ? value : void 0);
1363
+ return this.#park("confirm", options.message, options, this.#gate("confirm", options)).value;
1317
1364
  }
1318
1365
  select(options) {
1319
- const values = new Set(options.choices.map(normalizeChoice).map((choice) => choice.value));
1320
- return this.#park("select", options.message, options, (value) => isString(value) && values.has(value) ? value : void 0);
1366
+ return this.#park("select", options.message, options, this.#gate("select", options)).value;
1321
1367
  }
1322
1368
  checkbox(options) {
1323
- const values = new Set(options.choices.map(normalizeCheckboxChoice).map((choice) => choice.value));
1324
- const { min, max } = options;
1325
- return this.#park("checkbox", options.message, options, (value) => {
1326
- if (!isArray(value) || !value.every(isString)) return void 0;
1327
- if (!value.every((item) => values.has(item))) return void 0;
1328
- if (min !== void 0 && value.length < min) return void 0;
1329
- if (max !== void 0 && value.length > max) return void 0;
1330
- return value;
1331
- });
1369
+ return this.#park("checkbox", options.message, options, this.#gate("checkbox", options)).value;
1332
1370
  }
1333
1371
  editor(options) {
1334
- const validator = resolveValidation(options.validate);
1335
- return this.#park("editor", options.message, options, (value) => isString(value) && validator(value) === true ? value : void 0);
1372
+ return this.#park("editor", options.message, options, this.#gate("editor", options)).value;
1336
1373
  }
1337
1374
  destroy() {
1338
1375
  if (this.#destroyed) return;
@@ -1341,8 +1378,15 @@ var Prompt = class {
1341
1378
  this.#parked.clear();
1342
1379
  this.#emitter.destroy();
1343
1380
  }
1344
- #park(form, message, options, gate) {
1345
- if (this.#destroyed) return Promise.reject(new TerminalError("EXPIRE", "broker destroyed"));
1381
+ #park(form, message, options, gate, from, to) {
1382
+ if (this.#destroyed) return {
1383
+ id: crypto.randomUUID(),
1384
+ value: Promise.reject(new TerminalError("EXPIRE", "broker destroyed"))
1385
+ };
1386
+ if (this.#cap !== void 0 && this.#parked.size >= this.#cap) return {
1387
+ id: crypto.randomUUID(),
1388
+ value: Promise.reject(new TerminalError("LIMIT", `parked-prompt cap (${this.#cap}) reached`, { cap: this.#cap }))
1389
+ };
1346
1390
  const id = crypto.randomUUID();
1347
1391
  const prompt = {
1348
1392
  id,
@@ -1350,35 +1394,83 @@ var Prompt = class {
1350
1394
  message,
1351
1395
  options: serializePromptOptions(options),
1352
1396
  status: "pending",
1353
- time: Date.now()
1397
+ time: Date.now(),
1398
+ ...from !== void 0 ? { from } : {},
1399
+ ...to !== void 0 ? { to } : {}
1354
1400
  };
1355
- return new Promise((resolve, reject) => {
1356
- const cancel = this.#timer(() => this.#expire(id), this.#timeout);
1357
- const respond = (value) => {
1358
- const accepted = gate(value);
1359
- if (accepted === void 0) return void 0;
1360
- const current = this.#parked.get(id);
1361
- if (current !== void 0) this.#parked.set(id, {
1362
- ...current,
1363
- prompt: {
1364
- ...current.prompt,
1365
- status: "answered"
1366
- }
1401
+ return {
1402
+ id,
1403
+ value: new Promise((resolve, reject) => {
1404
+ const cancel = this.#timer(() => this.#expire(id), this.#timeout);
1405
+ const respond = (answer) => {
1406
+ const accepted = gate(answer);
1407
+ if (accepted === void 0) return void 0;
1408
+ const current = this.#parked.get(id);
1409
+ if (current !== void 0) this.#parked.set(id, {
1410
+ ...current,
1411
+ prompt: {
1412
+ ...current.prompt,
1413
+ status: "answered"
1414
+ }
1415
+ });
1416
+ resolve(accepted);
1417
+ return accepted;
1418
+ };
1419
+ const expire = () => {
1420
+ reject(new TerminalError("EXPIRE", `prompt ${id} expired`, { id }));
1421
+ };
1422
+ this.#parked.set(id, {
1423
+ prompt,
1424
+ respond,
1425
+ expire,
1426
+ cancel
1367
1427
  });
1368
- resolve(accepted);
1369
- return accepted;
1370
- };
1371
- const expire = () => {
1372
- reject(new TerminalError("EXPIRE", `prompt ${id} expired`, { id }));
1373
- };
1374
- this.#parked.set(id, {
1375
- prompt,
1376
- respond,
1377
- expire,
1378
- cancel
1379
- });
1380
- this.#emitter.emit("pending", prompt);
1381
- });
1428
+ this.#emitter.emit("pending", prompt);
1429
+ })
1430
+ };
1431
+ }
1432
+ #gate(form, options) {
1433
+ switch (form) {
1434
+ case "input":
1435
+ case "password":
1436
+ case "editor": {
1437
+ if (!this.#isTextOptions(form, options)) return () => void 0;
1438
+ const validator = resolveValidation(options.validate);
1439
+ return (value) => isString(value) && validator(value) === true ? value : void 0;
1440
+ }
1441
+ case "confirm":
1442
+ if (!this.#isConfirmOptions(form, options)) return () => void 0;
1443
+ return (value) => isBoolean(value) ? value : void 0;
1444
+ case "select": {
1445
+ if (!this.#isSelectOptions(form, options)) return () => void 0;
1446
+ const values = new Set(options.choices.map(normalizeChoice).map((choice) => choice.value));
1447
+ return (value) => isString(value) && values.has(value) ? value : void 0;
1448
+ }
1449
+ case "checkbox": {
1450
+ if (!this.#isCheckboxOptions(form, options)) return () => void 0;
1451
+ const values = new Set(options.choices.map(normalizeCheckboxChoice).map((choice) => choice.value));
1452
+ const { min, max } = options;
1453
+ return (value) => {
1454
+ if (!isArray(value) || !value.every(isString)) return void 0;
1455
+ if (!value.every((item) => values.has(item))) return void 0;
1456
+ if (min !== void 0 && value.length < min) return void 0;
1457
+ if (max !== void 0 && value.length > max) return void 0;
1458
+ return value;
1459
+ };
1460
+ }
1461
+ }
1462
+ }
1463
+ #isTextOptions(form, _options) {
1464
+ return form === "input" || form === "password" || form === "editor";
1465
+ }
1466
+ #isConfirmOptions(form, _options) {
1467
+ return form === "confirm";
1468
+ }
1469
+ #isSelectOptions(form, _options) {
1470
+ return form === "select";
1471
+ }
1472
+ #isCheckboxOptions(form, _options) {
1473
+ return form === "checkbox";
1382
1474
  }
1383
1475
  #expire(id) {
1384
1476
  const parked = this.#parked.get(id);
@@ -1415,8 +1507,10 @@ var Prompt = class {
1415
1507
  * never an `as`), dispatched to `terminal`, and its resolved value POSTed back to `url`.
1416
1508
  * Dispatch is strictly SERIAL: the read loop drives the terminal for ONE prompt at a time and
1417
1509
  * only reads/dispatches the next event after the current prompt fully settles (its answer
1418
- * POSTed). A prompt id redelivered by the broker AFTER its prior dispatch has settled is
1419
- * dispatched again — the client does not dedupe across completion.
1510
+ * POSTed). A prompt id redelivered (e.g. after a reconnect) WHILE its prior dispatch is still
1511
+ * in flight is skipped (a `#seen` set tracks in-flight ids); once that dispatch settles (or the
1512
+ * broker sends `expire` for the id) the id is dropped from `#seen`, so a later redelivery of the
1513
+ * SAME id after completion is dispatched again — the client does not dedupe across completion.
1420
1514
  * - **Server signals.** An `expire` event (the broker dropped a parked prompt) emits `expire`; a
1421
1515
  * `shutdown` event calls {@link disconnect} (not {@link destroy}) — the client stops streaming
1422
1516
  * without auto-reconnect but stays reusable; a later {@link connect} recovers it.
@@ -1450,6 +1544,7 @@ var PromptClient = class {
1450
1544
  #connected = false;
1451
1545
  #destroyed = false;
1452
1546
  #warnedInsecureToken = false;
1547
+ #seen = /* @__PURE__ */ new Set();
1453
1548
  constructor(options) {
1454
1549
  this.url = options.url;
1455
1550
  this.#terminal = options.terminal;
@@ -1500,6 +1595,7 @@ var PromptClient = class {
1500
1595
  if (this.#destroyed) return;
1501
1596
  this.#destroyed = true;
1502
1597
  this.disconnect();
1598
+ this.#seen.clear();
1503
1599
  this.#emitter.destroy();
1504
1600
  }
1505
1601
  async #stream() {
@@ -1535,22 +1631,28 @@ var PromptClient = class {
1535
1631
  async #handle(event) {
1536
1632
  if (event.event === SSE_EVENTS.pending) {
1537
1633
  const parsed = parseWireJSON(event.data);
1538
- if (isPendingPrompt(parsed)) await this.#dispatch(parsed.id, parsed);
1634
+ if (isPendingPrompt(parsed) && !this.#seen.has(parsed.id)) await this.#dispatch(parsed.id, parsed);
1539
1635
  return;
1540
1636
  }
1541
1637
  if (event.event === SSE_EVENTS.expire) {
1542
1638
  const parsed = parseWireJSON(event.data);
1543
- if (isRecord(parsed) && isString(parsed.id)) this.#emitter.emit("expire", parsed.id);
1639
+ if (isRecord(parsed) && isString(parsed.id)) {
1640
+ this.#seen.delete(parsed.id);
1641
+ this.#emitter.emit("expire", parsed.id);
1642
+ }
1544
1643
  return;
1545
1644
  }
1546
1645
  if (event.event === SSE_EVENTS.shutdown) this.disconnect();
1547
1646
  }
1548
1647
  async #dispatch(id, pending) {
1648
+ this.#seen.add(id);
1549
1649
  try {
1550
1650
  const value = await dispatchPendingPrompt(this.#terminal, pending);
1551
1651
  await this.#post(id, value);
1552
1652
  } catch (error) {
1553
1653
  this.#emitter.emit("error", error);
1654
+ } finally {
1655
+ this.#seen.delete(id);
1554
1656
  }
1555
1657
  }
1556
1658
  async #post(id, value) {
@@ -1569,8 +1671,11 @@ var PromptClient = class {
1569
1671
  this.#emitter.emit("disconnect");
1570
1672
  }
1571
1673
  #headers(base) {
1572
- if (this.#token !== void 0) base[HEADER_TOKEN] = this.#token;
1573
- return base;
1674
+ if (this.#token !== void 0) return {
1675
+ ...base,
1676
+ [HEADER_TOKEN]: this.#token
1677
+ };
1678
+ return { ...base };
1574
1679
  }
1575
1680
  #wait(ms) {
1576
1681
  return new Promise((resolve) => {
@@ -1587,6 +1692,345 @@ var PromptClient = class {
1587
1692
  }
1588
1693
  };
1589
1694
  //#endregion
1695
+ //#region src/core/TerminalManager.ts
1696
+ /**
1697
+ * The multi-endpoint terminal MANAGER — a registry of named {@link PromptInterface} brokers (one
1698
+ * per endpoint), so several parties can `ask` prompts of each other by NAME with a `from` → `to`
1699
+ * attribution edge on every parked prompt, and a transitive DEADLOCK check across all in-flight
1700
+ * asks.
1701
+ *
1702
+ * @remarks
1703
+ * - **Registry.** `add(name, options?)` mints (or, if `name` is already mounted, returns the
1704
+ * EXISTING broker UNCHANGED — idempotent, never clobbers a live/parked endpoint). Every mounted
1705
+ * broker's `pending` / `answer` / `expire` events are re-emitted on the manager, attributed by
1706
+ * `name`.
1707
+ * - **`ask`.** The target must already be mounted via {@link add} — `ask` never auto-adds it;
1708
+ * rejects `TARGET` for an unknown `to` (listing the known names). Rejects `DEADLOCK` when parking
1709
+ * `from → to` would close a cycle over the CURRENT in-flight edge set (walked transitively);
1710
+ * otherwise parks through the target's broker and resolves with the ORIGINAL ticket Promise (edge
1711
+ * cleanup is attached via `.then`, never altering the value/rejection the caller observes).
1712
+ * - **Durable open / save.** `open(name)` restores an EMPTY broker from the `store` (parked
1713
+ * Promises are process-bound and never resurrected); `save(name)` persists the endpoint's
1714
+ * configured `timeout`.
1715
+ * - **Removal.** `remove` drops one endpoint or a batch (§9.2, array overload FIRST), destroying
1716
+ * each broker (which expires every prompt still parked on it). `clear` drops all; `destroy` is
1717
+ * idempotent.
1718
+ *
1719
+ * @example
1720
+ * ```ts
1721
+ * const manager = new TerminalManager()
1722
+ * manager.add('agent')
1723
+ * const name = manager.ask('user', 'agent', 'input', { message: 'Your name' })
1724
+ * manager.answer('agent', manager.pending('agent')[0].id, 'Ada')
1725
+ * await name // 'Ada'
1726
+ * ```
1727
+ */
1728
+ var TerminalManager = class {
1729
+ #terminals = /* @__PURE__ */ new Map();
1730
+ #config = /* @__PURE__ */ new Map();
1731
+ #listeners = /* @__PURE__ */ new Map();
1732
+ #edges = /* @__PURE__ */ new Map();
1733
+ #store;
1734
+ #timeout;
1735
+ #timer;
1736
+ #cap;
1737
+ #emitter;
1738
+ #destroyed = false;
1739
+ constructor(options) {
1740
+ this.#store = options?.store;
1741
+ this.#timeout = options?.timeout;
1742
+ this.#timer = options?.timer;
1743
+ this.#cap = options?.cap;
1744
+ this.#emitter = new Emitter({
1745
+ on: options?.on,
1746
+ error: options?.error
1747
+ });
1748
+ }
1749
+ get emitter() {
1750
+ return this.#emitter;
1751
+ }
1752
+ get count() {
1753
+ return this.#terminals.size;
1754
+ }
1755
+ terminal(name) {
1756
+ return this.#terminals.get(name);
1757
+ }
1758
+ terminals() {
1759
+ return [...this.#terminals.keys()];
1760
+ }
1761
+ add(name, options) {
1762
+ if (this.#destroyed) throw new TerminalError("DESTROYED", "manager destroyed");
1763
+ const existing = this.#terminals.get(name);
1764
+ if (existing !== void 0) return existing;
1765
+ const timeout = options?.timeout ?? this.#timeout;
1766
+ const timer = options?.timer ?? this.#timer;
1767
+ const cap = options?.cap ?? this.#cap;
1768
+ const broker = createPrompt({
1769
+ ...timeout !== void 0 ? { timeout } : {},
1770
+ ...timer !== void 0 ? { timer } : {},
1771
+ ...cap !== void 0 ? { cap } : {}
1772
+ });
1773
+ const listeners = {
1774
+ pending: (prompt) => this.#emitter.emit("pending", prompt),
1775
+ answer: (id, value) => this.#emitter.emit("answer", name, id, value),
1776
+ expire: (id) => this.#emitter.emit("expire", name, id)
1777
+ };
1778
+ broker.emitter.on("pending", listeners.pending);
1779
+ broker.emitter.on("answer", listeners.answer);
1780
+ broker.emitter.on("expire", listeners.expire);
1781
+ this.#terminals.set(name, broker);
1782
+ this.#config.set(name, options ?? {});
1783
+ this.#listeners.set(name, listeners);
1784
+ return broker;
1785
+ }
1786
+ ask(from, to, form, options) {
1787
+ const broker = this.#terminals.get(to);
1788
+ if (broker === void 0) {
1789
+ const known = this.terminals();
1790
+ return Promise.reject(new TerminalError("TARGET", `unknown terminal '${to}' (known: ${known.length > 0 ? known.join(", ") : "none"})`, {
1791
+ to,
1792
+ known
1793
+ }));
1794
+ }
1795
+ const cycle = this.#findCycle(from, to);
1796
+ if (cycle !== void 0) return Promise.reject(new TerminalError("DEADLOCK", `ask ${from} -> ${to} would deadlock: ${cycle.join(" -> ")}`, {
1797
+ from,
1798
+ to,
1799
+ path: cycle
1800
+ }));
1801
+ const ticket = broker.park({
1802
+ form,
1803
+ options,
1804
+ from,
1805
+ to
1806
+ });
1807
+ if (broker.pending(ticket.id) !== void 0) {
1808
+ this.#edges.set(ticket.id, {
1809
+ from,
1810
+ to
1811
+ });
1812
+ const clear = () => {
1813
+ this.#edges.delete(ticket.id);
1814
+ };
1815
+ ticket.value.then(clear, clear);
1816
+ }
1817
+ return ticket.value;
1818
+ }
1819
+ pending(to) {
1820
+ if (to !== void 0) {
1821
+ const broker = this.#terminals.get(to);
1822
+ return broker === void 0 ? [] : broker.pending();
1823
+ }
1824
+ const result = [];
1825
+ for (const broker of this.#terminals.values()) result.push(...broker.pending());
1826
+ return result;
1827
+ }
1828
+ answer(to, id, value) {
1829
+ const broker = this.#terminals.get(to);
1830
+ if (broker === void 0) return {
1831
+ success: false,
1832
+ error: "terminal"
1833
+ };
1834
+ return broker.answer(id, value);
1835
+ }
1836
+ async open(name) {
1837
+ if (this.#destroyed) throw new TerminalError("DESTROYED", "manager destroyed");
1838
+ const existing = this.#terminals.get(name);
1839
+ if (existing !== void 0) return existing;
1840
+ if (this.#store === void 0) return void 0;
1841
+ const snapshot = await this.#store.get(name);
1842
+ if (this.#destroyed) throw new TerminalError("DESTROYED", "manager destroyed");
1843
+ if (snapshot === void 0) return void 0;
1844
+ return this.add(name, snapshot.timeout !== void 0 ? { timeout: snapshot.timeout } : {});
1845
+ }
1846
+ async save(name) {
1847
+ const broker = this.#terminals.get(name);
1848
+ if (this.#store === void 0 || broker === void 0) return false;
1849
+ const config = this.#config.get(name);
1850
+ const snapshot = {
1851
+ id: name,
1852
+ ...config?.timeout !== void 0 ? { timeout: config.timeout } : {}
1853
+ };
1854
+ await this.#store.set(snapshot);
1855
+ return true;
1856
+ }
1857
+ remove(names) {
1858
+ if (isArray(names)) {
1859
+ let removed = false;
1860
+ for (const name of names) if (this.#removeOne(name)) removed = true;
1861
+ return removed;
1862
+ }
1863
+ return this.#removeOne(names);
1864
+ }
1865
+ clear() {
1866
+ for (const name of [...this.#terminals.keys()]) this.#removeOne(name);
1867
+ }
1868
+ destroy() {
1869
+ if (this.#destroyed) return;
1870
+ this.#destroyed = true;
1871
+ this.clear();
1872
+ this.#emitter.destroy();
1873
+ }
1874
+ #removeOne(name) {
1875
+ const broker = this.#terminals.get(name);
1876
+ if (broker === void 0) return false;
1877
+ broker.destroy();
1878
+ const listeners = this.#listeners.get(name);
1879
+ if (listeners !== void 0) {
1880
+ broker.emitter.off("pending", listeners.pending);
1881
+ broker.emitter.off("answer", listeners.answer);
1882
+ broker.emitter.off("expire", listeners.expire);
1883
+ }
1884
+ this.#terminals.delete(name);
1885
+ this.#config.delete(name);
1886
+ this.#listeners.delete(name);
1887
+ return true;
1888
+ }
1889
+ #findCycle(from, to) {
1890
+ if (from === to) return [from, to];
1891
+ const visited = /* @__PURE__ */ new Set([to]);
1892
+ const queue = [[to]];
1893
+ while (queue.length > 0) {
1894
+ const path = queue.shift();
1895
+ if (path === void 0) break;
1896
+ const last = path[path.length - 1];
1897
+ if (last === void 0) continue;
1898
+ for (const edge of this.#edges.values()) {
1899
+ if (edge.from !== last) continue;
1900
+ if (edge.to === from) return [
1901
+ from,
1902
+ ...path,
1903
+ from
1904
+ ];
1905
+ if (visited.has(edge.to)) continue;
1906
+ visited.add(edge.to);
1907
+ queue.push([...path, edge.to]);
1908
+ }
1909
+ }
1910
+ }
1911
+ };
1912
+ //#endregion
1913
+ //#region src/core/MemoryTerminalStore.ts
1914
+ /**
1915
+ * The in-memory {@link TerminalStoreInterface} — a process-lifetime `Map` of
1916
+ * {@link TerminalSnapshot}s keyed by endpoint id, the DEFAULT store
1917
+ * {@link import('./factories.js').createMemoryTerminalStore} builds. The EXACT twin of
1918
+ * {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore}.
1919
+ *
1920
+ * @remarks
1921
+ * A plain `Map<string, TerminalSnapshot>` (AGENTS §21 — the snapshot is already pure, self-contained
1922
+ * CONFIG-only JSON, so no encoding is needed for the memory tier). There is NO idle-TTL and NO
1923
+ * eviction: a persisted config lives until an explicit `delete`. A durable backend (JSON / SQLite /
1924
+ * IndexedDB) swaps in through the SAME interface without touching the manager — its
1925
+ * driver-pluggable twin is {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore} (the
1926
+ * snapshot as one opaque JSON column).
1927
+ *
1928
+ * - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
1929
+ * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
1930
+ * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
1931
+ *
1932
+ * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
1933
+ * bijection with {@link TerminalStoreInterface}). Hydration is a caller concern: `open` always
1934
+ * restores an EMPTY broker — parked Promises are process-bound and never resurrected.
1935
+ *
1936
+ * @example
1937
+ * ```ts
1938
+ * import { createMemoryTerminalStore } from '@src/core'
1939
+ *
1940
+ * const store = createMemoryTerminalStore()
1941
+ * await store.set({ id: 'shell', timeout: 5000 }) // persist a config
1942
+ * const snapshot = await store.get('shell')
1943
+ * await store.delete('shell') // drop it
1944
+ * ```
1945
+ */
1946
+ var MemoryTerminalStore = class {
1947
+ #snapshots = /* @__PURE__ */ new Map();
1948
+ get(id) {
1949
+ return Promise.resolve(this.#snapshots.get(id));
1950
+ }
1951
+ set(snapshot) {
1952
+ this.#snapshots.set(snapshot.id, snapshot);
1953
+ return Promise.resolve();
1954
+ }
1955
+ delete(id) {
1956
+ this.#snapshots.delete(id);
1957
+ return Promise.resolve();
1958
+ }
1959
+ };
1960
+ //#endregion
1961
+ //#region src/core/DatabaseTerminalStore.ts
1962
+ /**
1963
+ * A {@link TerminalStoreInterface} backed by one table of the `databases` layer — an endpoint's
1964
+ * durable CONFIG state IS a row, so persistence reduces to keyed point-access (`get` / `set` /
1965
+ * `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
1966
+ * {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}.
1967
+ *
1968
+ * @remarks
1969
+ * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
1970
+ * JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
1971
+ * IndexedDB backend swaps in WITHOUT touching the manager — the same seam as
1972
+ * {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}. The driver defaults to memory
1973
+ * ({@link import('./factories.js').createDatabaseTerminalStore} passes `createMemoryDriver()`), so
1974
+ * it ALSO works in memory out of the box; you opt into the durable plumbing by passing a JSON /
1975
+ * SQLite / IndexedDB driver.
1976
+ *
1977
+ * The {@link TerminalSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
1978
+ * `{ id; snapshot }` ({@link TerminalSnapshotRow}). The snapshot is already a COMPLETE,
1979
+ * self-contained, pure-JSON CONFIG payload (no live broker state), so storing it whole is lossless
1980
+ * AND keeps the row type flat (`snapshot` reads back as `unknown`).
1981
+ *
1982
+ * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
1983
+ * the row `{ id: snapshot.id, snapshot }`.
1984
+ * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
1985
+ * a {@link TerminalSnapshot} ({@link import('./helpers.js').isTerminalSnapshot} — the AGENTS §14
1986
+ * boundary narrow for an untrusted storage read), or `undefined` if none is stored.
1987
+ * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
1988
+ *
1989
+ * There is NO idle-TTL / eviction — a persisted config lives until an explicit `delete`. The public
1990
+ * surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method bijection with
1991
+ * {@link TerminalStoreInterface}). Hydration stays a caller concern: `open` always restores an EMPTY
1992
+ * broker — parked Promises are process-bound and never resurrected.
1993
+ *
1994
+ * @example
1995
+ * ```ts
1996
+ * import { createDatabaseTerminalStore, createMemoryDriver } from '@src/core'
1997
+ *
1998
+ * const store = createDatabaseTerminalStore(createMemoryDriver()) // a durable driver swaps in here
1999
+ * await store.set({ id: 'shell', timeout: 5000 }) // persist the config (one JSON column)
2000
+ * const snapshot = await store.get('shell')
2001
+ * await store.delete('shell') // drop it
2002
+ * ```
2003
+ */
2004
+ var DatabaseTerminalStore = class {
2005
+ #table;
2006
+ /**
2007
+ * Wrap a table as a terminal store.
2008
+ *
2009
+ * @param table - The {@link TableInterface} holding the snapshots — its row is the
2010
+ * {@link TerminalSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
2011
+ */
2012
+ constructor(table) {
2013
+ this.#table = table;
2014
+ }
2015
+ /** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `TerminalSnapshot`. */
2016
+ async get(id) {
2017
+ const row = await this.#table.get(id);
2018
+ if (row === void 0) return void 0;
2019
+ return isTerminalSnapshot(row.snapshot) ? row.snapshot : void 0;
2020
+ }
2021
+ /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
2022
+ async set(snapshot) {
2023
+ await this.#table.set({
2024
+ id: snapshot.id,
2025
+ snapshot
2026
+ });
2027
+ }
2028
+ /** Drop a snapshot by id; an absent id is a no-op (no throw). */
2029
+ async delete(id) {
2030
+ await this.#table.remove(id);
2031
+ }
2032
+ };
2033
+ //#endregion
1590
2034
  //#region src/core/factories.ts
1591
2035
  /**
1592
2036
  * Create the headless prompt {@link PromptInterface} BROKER — it parks each prompt call as a
@@ -1645,7 +2089,66 @@ function createPrompt(options) {
1645
2089
  function createPromptClient(options) {
1646
2090
  return new PromptClient(options);
1647
2091
  }
2092
+ /**
2093
+ * Create the multi-endpoint {@link TerminalManagerInterface} — a named registry of
2094
+ * {@link PromptInterface} brokers so several parties can `ask` prompts of each other by name,
2095
+ * with a transitive DEADLOCK check across every in-flight ask.
2096
+ *
2097
+ * @param options - See {@link TerminalManagerOptions}
2098
+ * @returns A {@link TerminalManagerInterface}
2099
+ *
2100
+ * @example
2101
+ * ```ts
2102
+ * import { createTerminalManager } from '@src/core'
2103
+ *
2104
+ * const manager = createTerminalManager()
2105
+ * manager.add('agent')
2106
+ * ```
2107
+ */
2108
+ function createTerminalManager(options) {
2109
+ return new TerminalManager(options);
2110
+ }
2111
+ /**
2112
+ * Create the in-memory {@link TerminalStoreInterface} — a process-lifetime `Map` of endpoint
2113
+ * config snapshots, the default store backing a {@link TerminalManagerInterface}'s `open` / `save`.
2114
+ *
2115
+ * @returns A {@link TerminalStoreInterface}
2116
+ *
2117
+ * @example
2118
+ * ```ts
2119
+ * import { createMemoryTerminalStore } from '@src/core'
2120
+ *
2121
+ * const store = createMemoryTerminalStore()
2122
+ * ```
2123
+ */
2124
+ function createMemoryTerminalStore() {
2125
+ return new MemoryTerminalStore();
2126
+ }
2127
+ /**
2128
+ * Create a {@link TerminalStoreInterface} backed by one table of the `databases` layer — the
2129
+ * driver-pluggable twin of {@link createMemoryTerminalStore}, storing each endpoint's config
2130
+ * snapshot as one opaque JSON column.
2131
+ *
2132
+ * @param driver - The {@link DriverInterface} backing the table (default an in-memory driver)
2133
+ * @returns A {@link TerminalStoreInterface}
2134
+ *
2135
+ * @example
2136
+ * ```ts
2137
+ * import { createDatabaseTerminalStore } from '@src/core'
2138
+ *
2139
+ * const store = createDatabaseTerminalStore() // in-memory by default
2140
+ * ```
2141
+ */
2142
+ function createDatabaseTerminalStore(driver = createMemoryDriver()) {
2143
+ return new DatabaseTerminalStore(createDatabase({
2144
+ driver,
2145
+ tables: { terminals: {
2146
+ id: stringShape(),
2147
+ snapshot: rawShape({})
2148
+ } }
2149
+ }).table("terminals"));
2150
+ }
1648
2151
  //#endregion
1649
- export { ACCEPT_EVENT_STREAM, ALPHANUMERIC_PATTERN, BACKSPACE, CONTROL_NAMES, CTRL_A, CTRL_C, CTRL_D, CTRL_E, CTRL_U, DEFAULT_MASK, DEFAULT_PROMPT_TIMEOUT_MS, DEFAULT_RECONNECT_DELAY_MS, DELETE, EMAIL_PATTERN, ESCAPE, HEADER_TOKEN, INTEGER_PATTERN, KEY_CSI, KEY_SS3, NEWLINE, NUMERIC_PATTERN, PROMPT_ICONS, Prompt, PromptClient, RETURN, RULE_MESSAGES, SEQUENCE_NAMES, SPACE, SSE_BUFFER_LIMIT, SSE_EVENTS, TAB, TerminalError, URL_PATTERN, appendRule, buildRuleValidator, checkboxReduce, checkboxView, composeValidators, confirmReduce, confirmView, createCheckboxState, createConfirmState, createEditorState, createInputState, createPasswordState, createPrompt, createPromptClient, createSelectState, defaultTimer, dispatchPendingPrompt, editLine, editorReduce, editorView, errorLine, evaluateRule, gateSelection, globalFetch, inputReduce, inputView, isAbortError, isCheckboxChoice, isInsecureRemote, isPendingPrompt, isPendingPromptStatus, isPrintable, isPromptChoice, isPromptType, isTerminalError, normalizeCheckboxChoice, normalizeChoice, parseKey, parseWireJSON, passing, passwordReduce, passwordView, promptHeader, reconstructValidationRules, resolveChoices, resolveOption, resolveValidation, sanitizeChoiceLabels, selectReduce, selectView, serializeChoices, serializePromptOptions, serializeValidationRules, submitHeader, toggleIndex };
2152
+ export { ACCEPT_EVENT_STREAM, ALPHANUMERIC_PATTERN, BACKSPACE, CONTROL_NAMES, CTRL_A, CTRL_C, CTRL_D, CTRL_E, CTRL_U, DEFAULT_MASK, DEFAULT_PROMPT_TIMEOUT_MS, DEFAULT_RECONNECT_DELAY_MS, DELETE, DatabaseTerminalStore, EMAIL_PATTERN, ESCAPE, HEADER_TOKEN, INTEGER_PATTERN, KEY_CSI, KEY_SS3, MemoryTerminalStore, NEWLINE, NUMERIC_PATTERN, PROMPT_ICONS, Prompt, PromptClient, RETURN, RULE_MESSAGES, SEQUENCE_NAMES, SPACE, SSE_BUFFER_LIMIT, SSE_EVENTS, TAB, TerminalError, TerminalManager, URL_PATTERN, appendRule, buildRuleValidator, checkboxReduce, checkboxView, composeValidators, confirmReduce, confirmView, createCheckboxState, createConfirmState, createDatabaseTerminalStore, createEditorState, createInputState, createMemoryTerminalStore, createPasswordState, createPrompt, createPromptClient, createSelectState, createTerminalManager, defaultTimer, dispatchPendingPrompt, editLine, editorReduce, editorView, errorLine, evaluateRule, gateSelection, globalFetch, inputReduce, inputView, isAbortError, isAnswerPayload, isCheckboxChoice, isInsecureRemote, isPendingPrompt, isPendingPromptStatus, isPrintable, isPromptChoice, isPromptType, isTerminalError, isTerminalSnapshot, normalizeCheckboxChoice, normalizeChoice, parseKey, parseWireJSON, passing, passwordReduce, passwordView, promptHeader, reconstructValidationRules, resolveChoices, resolveOption, resolveValidation, sanitizeChoiceLabels, selectReduce, selectView, serializeChoices, serializeExpire, serializePending, serializePromptOptions, serializeShutdown, serializeValidationRules, submitHeader, toggleIndex };
1650
2153
 
1651
2154
  //# sourceMappingURL=index.js.map