appduct 0.13.0 → 0.15.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 (62) hide show
  1. package/README.md +55 -24
  2. package/dist/artifact-inspect.d.ts +38 -39
  3. package/dist/bin.js +106 -7
  4. package/dist/cli/result-types.d.ts +35 -2
  5. package/dist/cli/routes/daemon/index.js +90 -2
  6. package/dist/cli/routes/daemon/run.js +1088 -517
  7. package/dist/cli/routes/daemon/start.js +98 -2
  8. package/dist/cli/routes/daemon/status.js +99 -2
  9. package/dist/cli/routes/daemon/stop.js +98 -2
  10. package/dist/cli/routes/doctor.js +98 -3
  11. package/dist/cli/routes/events/index.d.ts +0 -3
  12. package/dist/cli/routes/events/index.js +92 -3
  13. package/dist/cli/routes/events/ls.d.ts +5 -0
  14. package/dist/cli/routes/events/ls.js +1837 -0
  15. package/dist/cli/routes/events/since.js +98 -2
  16. package/dist/cli/routes/events/tail.js +98 -2
  17. package/dist/cli/routes/init.js +265 -108
  18. package/dist/cli/routes/keygen.js +90 -2
  19. package/dist/cli/routes/mcp.js +243 -25
  20. package/dist/cli/routes/sessions/index.js +90 -2
  21. package/dist/cli/routes/sessions/link.js +199 -22
  22. package/dist/cli/routes/sessions/ls.js +98 -2
  23. package/dist/cli/routes/sessions/revoke.js +98 -2
  24. package/dist/cli/routes/tools/call.js +98 -2
  25. package/dist/cli/routes/tools/describe.js +98 -2
  26. package/dist/cli/routes/tools/index.js +90 -2
  27. package/dist/cli/routes/tools/ls.js +98 -2
  28. package/dist/client/attach-page.d.ts +39 -0
  29. package/dist/client/bootstrap.d.ts +17 -3
  30. package/dist/client/index.d.ts +2 -1
  31. package/dist/client/index.js +286 -112
  32. package/dist/commands/doctor.d.ts +3 -4
  33. package/dist/commands/events-ls.d.ts +21 -0
  34. package/dist/commands/init.d.ts +8 -2
  35. package/dist/commands/link.d.ts +10 -1
  36. package/dist/daemon/calls.d.ts +3 -1
  37. package/dist/daemon/config.d.ts +6 -0
  38. package/dist/daemon/daemon.d.ts +6 -1
  39. package/dist/daemon/event-bus.d.ts +10 -0
  40. package/dist/daemon/links.d.ts +9 -5
  41. package/dist/daemon/listener.d.ts +28 -1
  42. package/dist/daemon/registry.d.ts +20 -12
  43. package/dist/daemon/rpc-errors.d.ts +6 -0
  44. package/dist/daemon/sessions.d.ts +14 -8
  45. package/dist/daemon/tls.d.ts +0 -3
  46. package/dist/devtools-relay/attach-tab.d.ts +33 -0
  47. package/dist/devtools-relay/index.d.ts +8 -0
  48. package/dist/devtools-relay/memory-daemon-sockets.d.ts +20 -0
  49. package/dist/devtools-relay/memory-devtools-browser.d.ts +10 -0
  50. package/dist/devtools-relay/memory-page-channel.d.ts +16 -0
  51. package/dist/devtools-relay/node-daemon-socket.d.ts +3 -0
  52. package/dist/devtools-relay/node-devtools-browser.d.ts +3 -0
  53. package/dist/devtools-relay/ports.d.ts +48 -0
  54. package/dist/devtools-relay/relay.d.ts +16 -0
  55. package/dist/index.js +106 -7
  56. package/dist/link.d.ts +35 -1
  57. package/dist/mcp/app-tools.d.ts +19 -0
  58. package/dist/mcp/connect-tool.d.ts +15 -2
  59. package/dist/mcp/events-tool.d.ts +39 -0
  60. package/dist/native-scheme.d.ts +11 -7
  61. package/dist/scheme.d.ts +5 -1
  62. package/package.json +15 -5
@@ -44,6 +44,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
44
44
  "daemonLogMaxBytes",
45
45
  "eventsLogMaxBytes",
46
46
  "policy",
47
+ "webOrigins",
47
48
  "advertisedIp",
48
49
  "scheme",
49
50
  "restartDaemonOnVersionMismatch"
@@ -104,6 +105,7 @@ var defaultConfig = (paths) => {
104
105
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
105
106
  eventsLogMaxBytes: 10 * 1024 * 1024,
106
107
  restartDaemonOnVersionMismatch: false,
108
+ webOrigins: [],
107
109
  policy: {
108
110
  default: "allow",
109
111
  destructive: "allow"
@@ -174,6 +176,12 @@ var loadConfig = async (paths, options = {}) => {
174
176
  "restartDaemonOnVersionMismatch"
175
177
  );
176
178
  }
179
+ if (parsed.webOrigins !== void 0) {
180
+ if (!Array.isArray(parsed.webOrigins)) {
181
+ throw configError("webOrigins", "must be an array of origin strings.");
182
+ }
183
+ config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
184
+ }
177
185
  if (parsed.policy !== void 0) {
178
186
  if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
179
187
  throw configError("policy", "must be an object.");
@@ -1077,7 +1085,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
1077
1085
  // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
1078
1086
  // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
1079
1087
  // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
1080
- // assertion failure (docs/tasks/08-appduct-doctor.md).
1088
+ // assertion failure.
1081
1089
  inspection_error: 66,
1082
1090
  connection_error: 70,
1083
1091
  session_error: 71,
@@ -1239,6 +1247,7 @@ var formatJson = (value, flags) => {
1239
1247
  import pc from "picocolors";
1240
1248
  import {
1241
1249
  formatAgentWebSocketUrl,
1250
+ renderEventSignature,
1242
1251
  renderToolSignature,
1243
1252
  summarizeToolDescription
1244
1253
  } from "@appduct/shared";
@@ -1334,6 +1343,13 @@ var formatDevice = (device) => {
1334
1343
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
1335
1344
  return device.os ? `${label} (${device.os})` : label;
1336
1345
  };
1346
+ var SUSPEND_REASON_LABELS = {
1347
+ app_backgrounded: "app in background",
1348
+ connection_lost: "connection lost"
1349
+ };
1350
+ var formatSessionState = (session) => {
1351
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
1352
+ };
1337
1353
  var renderLsData = (colors, data, now) => {
1338
1354
  if (data.length === 0) {
1339
1355
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -1342,7 +1358,7 @@ var renderLsData = (colors, data, now) => {
1342
1358
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
1343
1359
  const rows = data.map((session) => [
1344
1360
  session.alias,
1345
- session.state,
1361
+ formatSessionState(session),
1346
1362
  formatDevice(session.device),
1347
1363
  String(session.toolCount),
1348
1364
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -1516,10 +1532,87 @@ var renderToolsListData = (colors, data, flags, full) => {
1516
1532
  }
1517
1533
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
1518
1534
  };
1535
+ var renderEventsTruncationLine = (data) => {
1536
+ const offset = data.offset ?? 0;
1537
+ if (data.events.length >= data.total) {
1538
+ return [];
1539
+ }
1540
+ return [
1541
+ "",
1542
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
1543
+ ];
1544
+ };
1545
+ var renderEventsListData = (colors, data, flags) => {
1546
+ const [only] = data.events;
1547
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
1548
+ return renderFields(
1549
+ colors.green(`Event: ${only.name}`),
1550
+ [
1551
+ ["Signature", renderEventSignature(only)],
1552
+ ["Description", only.description],
1553
+ ["Payload schema", only.payload_schema]
1554
+ ],
1555
+ flags
1556
+ );
1557
+ }
1558
+ if (data.events.length === 0 && data.total > 0) {
1559
+ return [
1560
+ colors.green("Events"),
1561
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
1562
+ ];
1563
+ }
1564
+ if (data.events.length === 0) {
1565
+ return [
1566
+ colors.green("Events"),
1567
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
1568
+ ];
1569
+ }
1570
+ const lines = data.events.flatMap((event) => [
1571
+ ` ${renderEventSignature(event)}`,
1572
+ ` ${summarizeToolDescription(event.description)}`
1573
+ ]);
1574
+ return [
1575
+ colors.green("Events"),
1576
+ ...lines,
1577
+ ...renderEventsTruncationLine(data),
1578
+ "",
1579
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
1580
+ ];
1581
+ };
1519
1582
  var renderInvokeData = (colors, data, flags) => {
1520
1583
  return [colors.green("Result"), formatScalar(data, flags)];
1521
1584
  };
1522
1585
  var renderLinkData = (colors, data, flags, qr) => {
1586
+ if ("attached" in data) {
1587
+ return [
1588
+ colors.green("Page Attached"),
1589
+ ...renderFields(
1590
+ "Page",
1591
+ [
1592
+ ["Session", data.sessionId],
1593
+ ["Tab", data.targetId],
1594
+ ["URL", data.url],
1595
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1596
+ ],
1597
+ flags
1598
+ )
1599
+ ];
1600
+ }
1601
+ if ("script" in data) {
1602
+ return [
1603
+ colors.green("Link Created"),
1604
+ ...renderFields(
1605
+ "Link",
1606
+ [
1607
+ ["Session", data.sessionId],
1608
+ ["URL", data.url],
1609
+ ["Script", data.script],
1610
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1611
+ ],
1612
+ flags
1613
+ )
1614
+ ];
1615
+ }
1523
1616
  const lines = [
1524
1617
  colors.green("Link Created"),
1525
1618
  ...renderFields(
@@ -1640,6 +1733,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
1640
1733
  ["PID", data.daemon.pid],
1641
1734
  ["Started at", data.daemon.started_at],
1642
1735
  ["WSS port", data.daemon.wss_port],
1736
+ ["Web port", data.daemon.web_port],
1643
1737
  ["Pinned keys", data.daemon.pinned_keys.length],
1644
1738
  ["Sessions", data.daemon.session_count]
1645
1739
  ],
@@ -1707,6 +1801,8 @@ var renderSuccessData = (colors, command, data, options) => {
1707
1801
  return renderToolsListData(colors, data, flags, options.full);
1708
1802
  case "tools describe":
1709
1803
  return renderToolDetail(colors, data, flags);
1804
+ case "events ls":
1805
+ return renderEventsListData(colors, data, flags);
1710
1806
  case "tools call":
1711
1807
  return renderInvokeData(colors, data, flags);
1712
1808
  case "sessions revoke":
@@ -24,6 +24,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
24
24
  "daemonLogMaxBytes",
25
25
  "eventsLogMaxBytes",
26
26
  "policy",
27
+ "webOrigins",
27
28
  "advertisedIp",
28
29
  "scheme",
29
30
  "restartDaemonOnVersionMismatch"
@@ -84,6 +85,7 @@ var defaultConfig = (paths) => {
84
85
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
85
86
  eventsLogMaxBytes: 10 * 1024 * 1024,
86
87
  restartDaemonOnVersionMismatch: false,
88
+ webOrigins: [],
87
89
  policy: {
88
90
  default: "allow",
89
91
  destructive: "allow"
@@ -154,6 +156,12 @@ var loadConfig = async (paths, options = {}) => {
154
156
  "restartDaemonOnVersionMismatch"
155
157
  );
156
158
  }
159
+ if (parsed.webOrigins !== void 0) {
160
+ if (!Array.isArray(parsed.webOrigins)) {
161
+ throw configError("webOrigins", "must be an array of origin strings.");
162
+ }
163
+ config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
164
+ }
157
165
  if (parsed.policy !== void 0) {
158
166
  if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
159
167
  throw configError("policy", "must be an object.");
@@ -913,7 +921,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
913
921
  // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
914
922
  // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
915
923
  // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
916
- // assertion failure (docs/tasks/08-appduct-doctor.md).
924
+ // assertion failure.
917
925
  inspection_error: 66,
918
926
  connection_error: 70,
919
927
  session_error: 71,
@@ -1181,6 +1189,7 @@ var formatJson = (value, flags) => {
1181
1189
  import pc from "picocolors";
1182
1190
  import {
1183
1191
  formatAgentWebSocketUrl,
1192
+ renderEventSignature,
1184
1193
  renderToolSignature,
1185
1194
  summarizeToolDescription
1186
1195
  } from "@appduct/shared";
@@ -1276,6 +1285,13 @@ var formatDevice = (device) => {
1276
1285
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
1277
1286
  return device.os ? `${label} (${device.os})` : label;
1278
1287
  };
1288
+ var SUSPEND_REASON_LABELS = {
1289
+ app_backgrounded: "app in background",
1290
+ connection_lost: "connection lost"
1291
+ };
1292
+ var formatSessionState = (session) => {
1293
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
1294
+ };
1279
1295
  var renderLsData = (colors, data, now) => {
1280
1296
  if (data.length === 0) {
1281
1297
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -1284,7 +1300,7 @@ var renderLsData = (colors, data, now) => {
1284
1300
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
1285
1301
  const rows = data.map((session) => [
1286
1302
  session.alias,
1287
- session.state,
1303
+ formatSessionState(session),
1288
1304
  formatDevice(session.device),
1289
1305
  String(session.toolCount),
1290
1306
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -1458,10 +1474,87 @@ var renderToolsListData = (colors, data, flags, full) => {
1458
1474
  }
1459
1475
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
1460
1476
  };
1477
+ var renderEventsTruncationLine = (data) => {
1478
+ const offset = data.offset ?? 0;
1479
+ if (data.events.length >= data.total) {
1480
+ return [];
1481
+ }
1482
+ return [
1483
+ "",
1484
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
1485
+ ];
1486
+ };
1487
+ var renderEventsListData = (colors, data, flags) => {
1488
+ const [only] = data.events;
1489
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
1490
+ return renderFields(
1491
+ colors.green(`Event: ${only.name}`),
1492
+ [
1493
+ ["Signature", renderEventSignature(only)],
1494
+ ["Description", only.description],
1495
+ ["Payload schema", only.payload_schema]
1496
+ ],
1497
+ flags
1498
+ );
1499
+ }
1500
+ if (data.events.length === 0 && data.total > 0) {
1501
+ return [
1502
+ colors.green("Events"),
1503
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
1504
+ ];
1505
+ }
1506
+ if (data.events.length === 0) {
1507
+ return [
1508
+ colors.green("Events"),
1509
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
1510
+ ];
1511
+ }
1512
+ const lines = data.events.flatMap((event) => [
1513
+ ` ${renderEventSignature(event)}`,
1514
+ ` ${summarizeToolDescription(event.description)}`
1515
+ ]);
1516
+ return [
1517
+ colors.green("Events"),
1518
+ ...lines,
1519
+ ...renderEventsTruncationLine(data),
1520
+ "",
1521
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
1522
+ ];
1523
+ };
1461
1524
  var renderInvokeData = (colors, data, flags) => {
1462
1525
  return [colors.green("Result"), formatScalar(data, flags)];
1463
1526
  };
1464
1527
  var renderLinkData = (colors, data, flags, qr) => {
1528
+ if ("attached" in data) {
1529
+ return [
1530
+ colors.green("Page Attached"),
1531
+ ...renderFields(
1532
+ "Page",
1533
+ [
1534
+ ["Session", data.sessionId],
1535
+ ["Tab", data.targetId],
1536
+ ["URL", data.url],
1537
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1538
+ ],
1539
+ flags
1540
+ )
1541
+ ];
1542
+ }
1543
+ if ("script" in data) {
1544
+ return [
1545
+ colors.green("Link Created"),
1546
+ ...renderFields(
1547
+ "Link",
1548
+ [
1549
+ ["Session", data.sessionId],
1550
+ ["URL", data.url],
1551
+ ["Script", data.script],
1552
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1553
+ ],
1554
+ flags
1555
+ )
1556
+ ];
1557
+ }
1465
1558
  const lines = [
1466
1559
  colors.green("Link Created"),
1467
1560
  ...renderFields(
@@ -1582,6 +1675,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
1582
1675
  ["PID", data.daemon.pid],
1583
1676
  ["Started at", data.daemon.started_at],
1584
1677
  ["WSS port", data.daemon.wss_port],
1678
+ ["Web port", data.daemon.web_port],
1585
1679
  ["Pinned keys", data.daemon.pinned_keys.length],
1586
1680
  ["Sessions", data.daemon.session_count]
1587
1681
  ],
@@ -1649,6 +1743,8 @@ var renderSuccessData = (colors, command, data, options) => {
1649
1743
  return renderToolsListData(colors, data, flags, options.full);
1650
1744
  case "tools describe":
1651
1745
  return renderToolDetail(colors, data, flags);
1746
+ case "events ls":
1747
+ return renderEventsListData(colors, data, flags);
1652
1748
  case "tools call":
1653
1749
  return renderInvokeData(colors, data, flags);
1654
1750
  case "sessions revoke":
@@ -93,7 +93,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
93
93
  // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
94
94
  // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
95
95
  // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
96
- // assertion failure (docs/tasks/08-appduct-doctor.md).
96
+ // assertion failure.
97
97
  inspection_error: 66,
98
98
  connection_error: 70,
99
99
  session_error: 71,
@@ -213,6 +213,7 @@ var formatJson = (value, flags) => {
213
213
  import pc from "picocolors";
214
214
  import {
215
215
  formatAgentWebSocketUrl,
216
+ renderEventSignature,
216
217
  renderToolSignature,
217
218
  summarizeToolDescription
218
219
  } from "@appduct/shared";
@@ -308,6 +309,13 @@ var formatDevice = (device) => {
308
309
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
309
310
  return device.os ? `${label} (${device.os})` : label;
310
311
  };
312
+ var SUSPEND_REASON_LABELS = {
313
+ app_backgrounded: "app in background",
314
+ connection_lost: "connection lost"
315
+ };
316
+ var formatSessionState = (session) => {
317
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
318
+ };
311
319
  var renderLsData = (colors, data, now) => {
312
320
  if (data.length === 0) {
313
321
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -316,7 +324,7 @@ var renderLsData = (colors, data, now) => {
316
324
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
317
325
  const rows = data.map((session) => [
318
326
  session.alias,
319
- session.state,
327
+ formatSessionState(session),
320
328
  formatDevice(session.device),
321
329
  String(session.toolCount),
322
330
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -490,10 +498,87 @@ var renderToolsListData = (colors, data, flags, full) => {
490
498
  }
491
499
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
492
500
  };
501
+ var renderEventsTruncationLine = (data) => {
502
+ const offset = data.offset ?? 0;
503
+ if (data.events.length >= data.total) {
504
+ return [];
505
+ }
506
+ return [
507
+ "",
508
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
509
+ ];
510
+ };
511
+ var renderEventsListData = (colors, data, flags) => {
512
+ const [only] = data.events;
513
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
514
+ return renderFields(
515
+ colors.green(`Event: ${only.name}`),
516
+ [
517
+ ["Signature", renderEventSignature(only)],
518
+ ["Description", only.description],
519
+ ["Payload schema", only.payload_schema]
520
+ ],
521
+ flags
522
+ );
523
+ }
524
+ if (data.events.length === 0 && data.total > 0) {
525
+ return [
526
+ colors.green("Events"),
527
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
528
+ ];
529
+ }
530
+ if (data.events.length === 0) {
531
+ return [
532
+ colors.green("Events"),
533
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
534
+ ];
535
+ }
536
+ const lines = data.events.flatMap((event) => [
537
+ ` ${renderEventSignature(event)}`,
538
+ ` ${summarizeToolDescription(event.description)}`
539
+ ]);
540
+ return [
541
+ colors.green("Events"),
542
+ ...lines,
543
+ ...renderEventsTruncationLine(data),
544
+ "",
545
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
546
+ ];
547
+ };
493
548
  var renderInvokeData = (colors, data, flags) => {
494
549
  return [colors.green("Result"), formatScalar(data, flags)];
495
550
  };
496
551
  var renderLinkData = (colors, data, flags, qr) => {
552
+ if ("attached" in data) {
553
+ return [
554
+ colors.green("Page Attached"),
555
+ ...renderFields(
556
+ "Page",
557
+ [
558
+ ["Session", data.sessionId],
559
+ ["Tab", data.targetId],
560
+ ["URL", data.url],
561
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
562
+ ],
563
+ flags
564
+ )
565
+ ];
566
+ }
567
+ if ("script" in data) {
568
+ return [
569
+ colors.green("Link Created"),
570
+ ...renderFields(
571
+ "Link",
572
+ [
573
+ ["Session", data.sessionId],
574
+ ["URL", data.url],
575
+ ["Script", data.script],
576
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
577
+ ],
578
+ flags
579
+ )
580
+ ];
581
+ }
497
582
  const lines = [
498
583
  colors.green("Link Created"),
499
584
  ...renderFields(
@@ -614,6 +699,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
614
699
  ["PID", data.daemon.pid],
615
700
  ["Started at", data.daemon.started_at],
616
701
  ["WSS port", data.daemon.wss_port],
702
+ ["Web port", data.daemon.web_port],
617
703
  ["Pinned keys", data.daemon.pinned_keys.length],
618
704
  ["Sessions", data.daemon.session_count]
619
705
  ],
@@ -681,6 +767,8 @@ var renderSuccessData = (colors, command, data, options) => {
681
767
  return renderToolsListData(colors, data, flags, options.full);
682
768
  case "tools describe":
683
769
  return renderToolDetail(colors, data, flags);
770
+ case "events ls":
771
+ return renderEventsListData(colors, data, flags);
684
772
  case "tools call":
685
773
  return renderInvokeData(colors, data, flags);
686
774
  case "sessions revoke":
@@ -24,6 +24,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
24
24
  "daemonLogMaxBytes",
25
25
  "eventsLogMaxBytes",
26
26
  "policy",
27
+ "webOrigins",
27
28
  "advertisedIp",
28
29
  "scheme",
29
30
  "restartDaemonOnVersionMismatch"
@@ -84,6 +85,7 @@ var defaultConfig = (paths) => {
84
85
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
85
86
  eventsLogMaxBytes: 10 * 1024 * 1024,
86
87
  restartDaemonOnVersionMismatch: false,
88
+ webOrigins: [],
87
89
  policy: {
88
90
  default: "allow",
89
91
  destructive: "allow"
@@ -154,6 +156,12 @@ var loadConfig = async (paths, options = {}) => {
154
156
  "restartDaemonOnVersionMismatch"
155
157
  );
156
158
  }
159
+ if (parsed.webOrigins !== void 0) {
160
+ if (!Array.isArray(parsed.webOrigins)) {
161
+ throw configError("webOrigins", "must be an array of origin strings.");
162
+ }
163
+ config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
164
+ }
157
165
  if (parsed.policy !== void 0) {
158
166
  if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
159
167
  throw configError("policy", "must be an object.");
@@ -913,7 +921,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
913
921
  // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
914
922
  // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
915
923
  // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
916
- // assertion failure (docs/tasks/08-appduct-doctor.md).
924
+ // assertion failure.
917
925
  inspection_error: 66,
918
926
  connection_error: 70,
919
927
  session_error: 71,
@@ -1175,6 +1183,7 @@ var formatJson = (value, flags) => {
1175
1183
  import pc from "picocolors";
1176
1184
  import {
1177
1185
  formatAgentWebSocketUrl,
1186
+ renderEventSignature,
1178
1187
  renderToolSignature,
1179
1188
  summarizeToolDescription
1180
1189
  } from "@appduct/shared";
@@ -1270,6 +1279,13 @@ var formatDevice = (device) => {
1270
1279
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
1271
1280
  return device.os ? `${label} (${device.os})` : label;
1272
1281
  };
1282
+ var SUSPEND_REASON_LABELS = {
1283
+ app_backgrounded: "app in background",
1284
+ connection_lost: "connection lost"
1285
+ };
1286
+ var formatSessionState = (session) => {
1287
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
1288
+ };
1273
1289
  var renderLsData = (colors, data, now) => {
1274
1290
  if (data.length === 0) {
1275
1291
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -1278,7 +1294,7 @@ var renderLsData = (colors, data, now) => {
1278
1294
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
1279
1295
  const rows = data.map((session) => [
1280
1296
  session.alias,
1281
- session.state,
1297
+ formatSessionState(session),
1282
1298
  formatDevice(session.device),
1283
1299
  String(session.toolCount),
1284
1300
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -1452,10 +1468,87 @@ var renderToolsListData = (colors, data, flags, full) => {
1452
1468
  }
1453
1469
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
1454
1470
  };
1471
+ var renderEventsTruncationLine = (data) => {
1472
+ const offset = data.offset ?? 0;
1473
+ if (data.events.length >= data.total) {
1474
+ return [];
1475
+ }
1476
+ return [
1477
+ "",
1478
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
1479
+ ];
1480
+ };
1481
+ var renderEventsListData = (colors, data, flags) => {
1482
+ const [only] = data.events;
1483
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
1484
+ return renderFields(
1485
+ colors.green(`Event: ${only.name}`),
1486
+ [
1487
+ ["Signature", renderEventSignature(only)],
1488
+ ["Description", only.description],
1489
+ ["Payload schema", only.payload_schema]
1490
+ ],
1491
+ flags
1492
+ );
1493
+ }
1494
+ if (data.events.length === 0 && data.total > 0) {
1495
+ return [
1496
+ colors.green("Events"),
1497
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
1498
+ ];
1499
+ }
1500
+ if (data.events.length === 0) {
1501
+ return [
1502
+ colors.green("Events"),
1503
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
1504
+ ];
1505
+ }
1506
+ const lines = data.events.flatMap((event) => [
1507
+ ` ${renderEventSignature(event)}`,
1508
+ ` ${summarizeToolDescription(event.description)}`
1509
+ ]);
1510
+ return [
1511
+ colors.green("Events"),
1512
+ ...lines,
1513
+ ...renderEventsTruncationLine(data),
1514
+ "",
1515
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
1516
+ ];
1517
+ };
1455
1518
  var renderInvokeData = (colors, data, flags) => {
1456
1519
  return [colors.green("Result"), formatScalar(data, flags)];
1457
1520
  };
1458
1521
  var renderLinkData = (colors, data, flags, qr) => {
1522
+ if ("attached" in data) {
1523
+ return [
1524
+ colors.green("Page Attached"),
1525
+ ...renderFields(
1526
+ "Page",
1527
+ [
1528
+ ["Session", data.sessionId],
1529
+ ["Tab", data.targetId],
1530
+ ["URL", data.url],
1531
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1532
+ ],
1533
+ flags
1534
+ )
1535
+ ];
1536
+ }
1537
+ if ("script" in data) {
1538
+ return [
1539
+ colors.green("Link Created"),
1540
+ ...renderFields(
1541
+ "Link",
1542
+ [
1543
+ ["Session", data.sessionId],
1544
+ ["URL", data.url],
1545
+ ["Script", data.script],
1546
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1547
+ ],
1548
+ flags
1549
+ )
1550
+ ];
1551
+ }
1459
1552
  const lines = [
1460
1553
  colors.green("Link Created"),
1461
1554
  ...renderFields(
@@ -1576,6 +1669,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
1576
1669
  ["PID", data.daemon.pid],
1577
1670
  ["Started at", data.daemon.started_at],
1578
1671
  ["WSS port", data.daemon.wss_port],
1672
+ ["Web port", data.daemon.web_port],
1579
1673
  ["Pinned keys", data.daemon.pinned_keys.length],
1580
1674
  ["Sessions", data.daemon.session_count]
1581
1675
  ],
@@ -1643,6 +1737,8 @@ var renderSuccessData = (colors, command, data, options) => {
1643
1737
  return renderToolsListData(colors, data, flags, options.full);
1644
1738
  case "tools describe":
1645
1739
  return renderToolDetail(colors, data, flags);
1740
+ case "events ls":
1741
+ return renderEventsListData(colors, data, flags);
1646
1742
  case "tools call":
1647
1743
  return renderInvokeData(colors, data, flags);
1648
1744
  case "sessions revoke":
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `attachPage()` (issue #177): runs a web page's Appduct session through a Playwright page instead of
3
+ * a socket the page opens itself. The page and the daemon talk through a binding Playwright exposes,
4
+ * so it works on an `https` page with no permission prompt and no debugging port. The binding belongs
5
+ * to the page's target: a popup, or a navigation that creates a new target, is not relayed, and the
6
+ * session stays on the page it was attached to.
7
+ */
8
+ import { type RelayLink } from "../devtools-relay/index.js";
9
+ /** The part of a Playwright `Page` that `attachPage` uses, so `appduct` has no Playwright dependency. */
10
+ export type AttachablePage = {
11
+ exposeBinding(name: string, callback: (source: {
12
+ frame: unknown;
13
+ }, json: string) => void): Promise<void>;
14
+ addInitScript(script: string): Promise<void>;
15
+ evaluate(expression: string): Promise<unknown>;
16
+ waitForFunction(expression: string): Promise<unknown>;
17
+ mainFrame(): unknown;
18
+ on(event: "close", handler: () => void): unknown;
19
+ };
20
+ export type AttachPageOptions = {
21
+ /** The result of `link({ target: "web", url })` for the page's address. */
22
+ link: RelayLink;
23
+ };
24
+ /**
25
+ * Connects the page, which must already load `@appduct/web`, to the daemon through its Playwright
26
+ * binding. Resolves once the session is claimed. A reload resumes the session over the same binding.
27
+ *
28
+ * Attaching a page that is already attached closes the first link's socket, so the daemon suspends
29
+ * the first session for its grace period and it stays listed as `suspended`. While it is, a
30
+ * `connect()` with no selector finds two sessions and fails with `ambiguous_session`: select the new
31
+ * one with `connect({ selector: link.sessionId })` or `waitForSession`. An invalid `link` rejects
32
+ * before anything is dropped.
33
+ *
34
+ * ```ts
35
+ * await page.goto("https://staging.example.com");
36
+ * await attachPage(page, { link: await link({ target: "web", url: page.url() }) });
37
+ * ```
38
+ */
39
+ export declare const attachPage: (page: AttachablePage, options: AttachPageOptions) => Promise<void>;