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
@@ -27,6 +27,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
27
27
  "daemonLogMaxBytes",
28
28
  "eventsLogMaxBytes",
29
29
  "policy",
30
+ "webOrigins",
30
31
  "advertisedIp",
31
32
  "scheme",
32
33
  "restartDaemonOnVersionMismatch"
@@ -87,6 +88,7 @@ var defaultConfig = (paths) => {
87
88
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
88
89
  eventsLogMaxBytes: 10 * 1024 * 1024,
89
90
  restartDaemonOnVersionMismatch: false,
91
+ webOrigins: [],
90
92
  policy: {
91
93
  default: "allow",
92
94
  destructive: "allow"
@@ -157,6 +159,12 @@ var loadConfig = async (paths, options = {}) => {
157
159
  "restartDaemonOnVersionMismatch"
158
160
  );
159
161
  }
162
+ if (parsed.webOrigins !== void 0) {
163
+ if (!Array.isArray(parsed.webOrigins)) {
164
+ throw configError("webOrigins", "must be an array of origin strings.");
165
+ }
166
+ config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
167
+ }
160
168
  if (parsed.policy !== void 0) {
161
169
  if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
162
170
  throw configError("policy", "must be an object.");
@@ -916,7 +924,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
916
924
  // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
917
925
  // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
918
926
  // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
919
- // assertion failure (docs/tasks/08-appduct-doctor.md).
927
+ // assertion failure.
920
928
  inspection_error: 66,
921
929
  connection_error: 70,
922
930
  session_error: 71,
@@ -1643,31 +1651,42 @@ var probeIosProjectYml = async (root) => {
1643
1651
  };
1644
1652
  };
1645
1653
  var PROBES = [
1646
- probeAndroidGradle,
1647
- probeAndroidManifest,
1648
- probeIosInfoPlist,
1649
- probeIosProjectYml
1654
+ ["android", probeAndroidGradle],
1655
+ ["android", probeAndroidManifest],
1656
+ ["ios", probeIosInfoPlist],
1657
+ ["ios", probeIosProjectYml]
1650
1658
  ];
1659
+ var isFlutterProject = async (root) => {
1660
+ return (await readCappedText(join4(root, "pubspec.yaml"))).ok;
1661
+ };
1651
1662
  var discoverNativeScheme = async (root) => {
1652
1663
  const tried = [];
1653
1664
  const results = [];
1654
- for (const probe of PROBES) {
1655
- const outcome = await probe(root);
1656
- tried.push(...outcome.tried);
1657
- if (outcome.result !== void 0) {
1658
- results.push(outcome.result);
1665
+ const flutter = await isFlutterProject(root);
1666
+ const androidRoot = flutter ? join4(root, "android") : root;
1667
+ const iosRoots = flutter ? [join4(root, "ios"), join4(root, "macos")] : [root];
1668
+ for (const [platform, probe] of PROBES) {
1669
+ let roots = [root];
1670
+ if (platform === "android") {
1671
+ roots = [androidRoot];
1672
+ } else if (probe === probeIosInfoPlist) {
1673
+ roots = iosRoots;
1674
+ }
1675
+ for (const probeRoot of roots) {
1676
+ const outcome = await probe(probeRoot);
1677
+ tried.push(...outcome.tried);
1678
+ if (outcome.result !== void 0) {
1679
+ results.push(outcome.result);
1680
+ }
1659
1681
  }
1660
1682
  }
1661
- if (results.length === 0) {
1662
- return { tried };
1663
- }
1664
1683
  const distinctSchemes = new Set(results.map((entry) => entry.scheme));
1665
1684
  if (distinctSchemes.size > 1) {
1666
1685
  throw usageError(
1667
1686
  "Conflicting deep-link schemes found in this project: " + results.map((entry) => `${entry.origin} says ${JSON.stringify(entry.scheme)}`).join(", but ") + ". Appduct will not guess which one is right \u2014 pass --scheme <scheme> to pick one."
1668
1687
  );
1669
1688
  }
1670
- return { result: results[0], tried };
1689
+ return { results, tried };
1671
1690
  };
1672
1691
 
1673
1692
  // src/scheme.ts
@@ -1730,19 +1749,21 @@ var discoverStaticProjectScheme = async (cwd) => {
1730
1749
  const tried = [`${appJsonPath} ("expo.scheme")`];
1731
1750
  const appJsonScheme = await discoverExpoScheme(cwd);
1732
1751
  if (appJsonScheme !== void 0) {
1733
- return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, tried };
1752
+ return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, nativeResults: [], tried };
1734
1753
  }
1735
1754
  const native = await discoverNativeScheme(resolve(cwd));
1736
1755
  tried.push(...native.tried);
1737
- if (native.result !== void 0) {
1756
+ const [first] = native.results;
1757
+ if (first !== void 0) {
1738
1758
  return {
1739
- scheme: native.result.scheme,
1740
- source: native.result.source,
1741
- origin: native.result.origin,
1759
+ scheme: first.scheme,
1760
+ source: first.source,
1761
+ origin: first.origin,
1762
+ nativeResults: native.results,
1742
1763
  tried
1743
1764
  };
1744
1765
  }
1745
- return { tried };
1766
+ return { nativeResults: [], tried };
1746
1767
  };
1747
1768
  var globalConfigDirs = (options = {}) => {
1748
1769
  const excluded = /* @__PURE__ */ new Set([join5(resolve(options.homeDir ?? homedir()), PROJECT_CONFIG_DIR)]);
@@ -1926,6 +1947,39 @@ ${tried.map((location, index) => ` ${index + 1}. ${location}`).join("\n")}
1926
1947
  // src/link.ts
1927
1948
  var OPEN_TARGET_ADDRESS_OVERRIDE = "127.0.0.1";
1928
1949
  var composeDeepLink = (scheme, result) => `${scheme}:///?appduct=${result.deepLinkPayload}&pin=${encodeURIComponent(result.pin)}`;
1950
+ var assertWebPageUrl = (pageUrl) => {
1951
+ let protocol;
1952
+ try {
1953
+ protocol = new URL(pageUrl).protocol;
1954
+ } catch {
1955
+ }
1956
+ if (protocol !== "http:" && protocol !== "https:") {
1957
+ throw usageError(`"url" must be an http(s) URL (got "${pageUrl}").`);
1958
+ }
1959
+ };
1960
+ var composeWebLink = (pageUrl, result) => {
1961
+ const payload = result.deepLinkPayload;
1962
+ return {
1963
+ sessionId: result.sessionId,
1964
+ url: `${pageUrl.split("#")[0]}#appduct=${payload}`,
1965
+ script: `window.__APPDUCT__.connect("${payload}")`,
1966
+ expiresAt: result.expiresAt
1967
+ };
1968
+ };
1969
+ var mintWebLink = async (options) => {
1970
+ assertWebPageUrl(options.url);
1971
+ const result = await callDaemon(
1972
+ RPC_METHODS2.linkCreate,
1973
+ { ttlSeconds: options.ttlSeconds, transport: "web" },
1974
+ { stateDir: options.stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn }
1975
+ );
1976
+ return composeWebLink(options.url, result);
1977
+ };
1978
+ var attachWebTab = async ({ stateDir, spawn, autoSpawn, ...params }) => {
1979
+ assertWebPageUrl(params.url);
1980
+ const result = await callDaemon(RPC_METHODS2.webAttach, params, { stateDir, spawn, autoSpawn, requestTimeoutMs: 3e4 });
1981
+ return { ...result, attached: true };
1982
+ };
1929
1983
  var mintLink = async (options) => {
1930
1984
  if (options.target !== void 0 && !isOpenTarget(options.target)) {
1931
1985
  throw usageError(
@@ -2013,6 +2067,9 @@ var mintLink = async (options) => {
2013
2067
  // src/commands/link.ts
2014
2068
  var handleLinkCommand = async (options, context) => {
2015
2069
  let openTarget;
2070
+ if (options.browserUrl !== void 0 || options.targetId !== void 0) {
2071
+ throw usageError('"--browser-url" and "--target-id" only apply with "--open web".');
2072
+ }
2016
2073
  if (options.open !== void 0) {
2017
2074
  if (!isOpenTarget(options.open)) {
2018
2075
  throw usageError(
@@ -2046,6 +2103,35 @@ var handleLinkCommand = async (options, context) => {
2046
2103
  });
2047
2104
  return { ok: true, data: result };
2048
2105
  };
2106
+ var handleWebLinkCommand = async (options, context) => {
2107
+ if (options.url === void 0) {
2108
+ throw usageError('"--open web" needs the page URL: appduct sessions link --open web <url>.');
2109
+ }
2110
+ if (options.device !== void 0 || options.appId !== void 0 || options.relaunch !== void 0) {
2111
+ throw usageError('"--device", "--app-id" and "--relaunch" do not apply with "--open web".');
2112
+ }
2113
+ if (options.targetId !== void 0 && options.browserUrl === void 0) {
2114
+ throw usageError('"--target-id" needs "--browser-url": it names a tab of that browser.');
2115
+ }
2116
+ if (options.browserUrl !== void 0) {
2117
+ const attached = await attachWebTab({
2118
+ stateDir: context.stateDir,
2119
+ spawn: context.spawn,
2120
+ ttlSeconds: options.ttlSeconds,
2121
+ url: options.url,
2122
+ browserUrl: options.browserUrl,
2123
+ targetId: options.targetId
2124
+ });
2125
+ return { ok: true, data: attached };
2126
+ }
2127
+ const result = await mintWebLink({
2128
+ stateDir: context.stateDir,
2129
+ spawn: context.spawn,
2130
+ ttlSeconds: options.ttlSeconds,
2131
+ url: options.url
2132
+ });
2133
+ return { ok: true, data: result };
2134
+ };
2049
2135
 
2050
2136
  // src/cli/command-options.ts
2051
2137
  var parsePositiveIntegerOption = (value, flagName) => {
@@ -2068,6 +2154,7 @@ var formatJson = (value, flags) => {
2068
2154
  import pc from "picocolors";
2069
2155
  import {
2070
2156
  formatAgentWebSocketUrl,
2157
+ renderEventSignature,
2071
2158
  renderToolSignature,
2072
2159
  summarizeToolDescription
2073
2160
  } from "@appduct/shared";
@@ -2163,6 +2250,13 @@ var formatDevice = (device) => {
2163
2250
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
2164
2251
  return device.os ? `${label} (${device.os})` : label;
2165
2252
  };
2253
+ var SUSPEND_REASON_LABELS = {
2254
+ app_backgrounded: "app in background",
2255
+ connection_lost: "connection lost"
2256
+ };
2257
+ var formatSessionState = (session) => {
2258
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
2259
+ };
2166
2260
  var renderLsData = (colors, data, now) => {
2167
2261
  if (data.length === 0) {
2168
2262
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -2171,7 +2265,7 @@ var renderLsData = (colors, data, now) => {
2171
2265
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
2172
2266
  const rows = data.map((session) => [
2173
2267
  session.alias,
2174
- session.state,
2268
+ formatSessionState(session),
2175
2269
  formatDevice(session.device),
2176
2270
  String(session.toolCount),
2177
2271
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -2345,10 +2439,87 @@ var renderToolsListData = (colors, data, flags, full) => {
2345
2439
  }
2346
2440
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
2347
2441
  };
2442
+ var renderEventsTruncationLine = (data) => {
2443
+ const offset = data.offset ?? 0;
2444
+ if (data.events.length >= data.total) {
2445
+ return [];
2446
+ }
2447
+ return [
2448
+ "",
2449
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
2450
+ ];
2451
+ };
2452
+ var renderEventsListData = (colors, data, flags) => {
2453
+ const [only] = data.events;
2454
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
2455
+ return renderFields(
2456
+ colors.green(`Event: ${only.name}`),
2457
+ [
2458
+ ["Signature", renderEventSignature(only)],
2459
+ ["Description", only.description],
2460
+ ["Payload schema", only.payload_schema]
2461
+ ],
2462
+ flags
2463
+ );
2464
+ }
2465
+ if (data.events.length === 0 && data.total > 0) {
2466
+ return [
2467
+ colors.green("Events"),
2468
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
2469
+ ];
2470
+ }
2471
+ if (data.events.length === 0) {
2472
+ return [
2473
+ colors.green("Events"),
2474
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
2475
+ ];
2476
+ }
2477
+ const lines = data.events.flatMap((event) => [
2478
+ ` ${renderEventSignature(event)}`,
2479
+ ` ${summarizeToolDescription(event.description)}`
2480
+ ]);
2481
+ return [
2482
+ colors.green("Events"),
2483
+ ...lines,
2484
+ ...renderEventsTruncationLine(data),
2485
+ "",
2486
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
2487
+ ];
2488
+ };
2348
2489
  var renderInvokeData = (colors, data, flags) => {
2349
2490
  return [colors.green("Result"), formatScalar(data, flags)];
2350
2491
  };
2351
2492
  var renderLinkData = (colors, data, flags, qr) => {
2493
+ if ("attached" in data) {
2494
+ return [
2495
+ colors.green("Page Attached"),
2496
+ ...renderFields(
2497
+ "Page",
2498
+ [
2499
+ ["Session", data.sessionId],
2500
+ ["Tab", data.targetId],
2501
+ ["URL", data.url],
2502
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
2503
+ ],
2504
+ flags
2505
+ )
2506
+ ];
2507
+ }
2508
+ if ("script" in data) {
2509
+ return [
2510
+ colors.green("Link Created"),
2511
+ ...renderFields(
2512
+ "Link",
2513
+ [
2514
+ ["Session", data.sessionId],
2515
+ ["URL", data.url],
2516
+ ["Script", data.script],
2517
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
2518
+ ],
2519
+ flags
2520
+ )
2521
+ ];
2522
+ }
2352
2523
  const lines = [
2353
2524
  colors.green("Link Created"),
2354
2525
  ...renderFields(
@@ -2469,6 +2640,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
2469
2640
  ["PID", data.daemon.pid],
2470
2641
  ["Started at", data.daemon.started_at],
2471
2642
  ["WSS port", data.daemon.wss_port],
2643
+ ["Web port", data.daemon.web_port],
2472
2644
  ["Pinned keys", data.daemon.pinned_keys.length],
2473
2645
  ["Sessions", data.daemon.session_count]
2474
2646
  ],
@@ -2536,6 +2708,8 @@ var renderSuccessData = (colors, command, data, options) => {
2536
2708
  return renderToolsListData(colors, data, flags, options.full);
2537
2709
  case "tools describe":
2538
2710
  return renderToolDetail(colors, data, flags);
2711
+ case "events ls":
2712
+ return renderEventsListData(colors, data, flags);
2539
2713
  case "tools call":
2540
2714
  return renderInvokeData(colors, data, flags);
2541
2715
  case "sessions revoke":
@@ -2690,10 +2864,13 @@ var route = async (context) => {
2690
2864
  return executeCommand(
2691
2865
  commandName(context),
2692
2866
  guarded(context)(
2693
- () => handleLinkCommand(
2867
+ () => (options.open === "web" ? handleWebLinkCommand : handleLinkCommand)(
2694
2868
  {
2695
2869
  ttlSeconds: parsePositiveIntegerOption(options.ttl, "--ttl"),
2696
2870
  scheme: typeof options.scheme === "string" ? options.scheme : void 0,
2871
+ url: context.args[0],
2872
+ browserUrl: typeof options.browserUrl === "string" ? options.browserUrl : void 0,
2873
+ targetId: typeof options.targetId === "string" ? options.targetId : void 0,
2697
2874
  open: typeof options.open === "string" ? options.open : void 0,
2698
2875
  device: typeof options.device === "string" ? options.device : void 0,
2699
2876
  // cac camelCases `--app-id`; the dashed spelling is kept as a fallback so a
@@ -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.");
@@ -923,7 +931,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
923
931
  // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
924
932
  // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
925
933
  // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
926
- // assertion failure (docs/tasks/08-appduct-doctor.md).
934
+ // assertion failure.
927
935
  inspection_error: 66,
928
936
  connection_error: 70,
929
937
  session_error: 71,
@@ -1040,6 +1048,7 @@ var formatJson = (value, flags) => {
1040
1048
  import pc from "picocolors";
1041
1049
  import {
1042
1050
  formatAgentWebSocketUrl,
1051
+ renderEventSignature,
1043
1052
  renderToolSignature,
1044
1053
  summarizeToolDescription
1045
1054
  } from "@appduct/shared";
@@ -1135,6 +1144,13 @@ var formatDevice = (device) => {
1135
1144
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
1136
1145
  return device.os ? `${label} (${device.os})` : label;
1137
1146
  };
1147
+ var SUSPEND_REASON_LABELS = {
1148
+ app_backgrounded: "app in background",
1149
+ connection_lost: "connection lost"
1150
+ };
1151
+ var formatSessionState = (session) => {
1152
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
1153
+ };
1138
1154
  var renderLsData = (colors, data, now) => {
1139
1155
  if (data.length === 0) {
1140
1156
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -1143,7 +1159,7 @@ var renderLsData = (colors, data, now) => {
1143
1159
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
1144
1160
  const rows = data.map((session) => [
1145
1161
  session.alias,
1146
- session.state,
1162
+ formatSessionState(session),
1147
1163
  formatDevice(session.device),
1148
1164
  String(session.toolCount),
1149
1165
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -1317,10 +1333,87 @@ var renderToolsListData = (colors, data, flags, full) => {
1317
1333
  }
1318
1334
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
1319
1335
  };
1336
+ var renderEventsTruncationLine = (data) => {
1337
+ const offset = data.offset ?? 0;
1338
+ if (data.events.length >= data.total) {
1339
+ return [];
1340
+ }
1341
+ return [
1342
+ "",
1343
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
1344
+ ];
1345
+ };
1346
+ var renderEventsListData = (colors, data, flags) => {
1347
+ const [only] = data.events;
1348
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
1349
+ return renderFields(
1350
+ colors.green(`Event: ${only.name}`),
1351
+ [
1352
+ ["Signature", renderEventSignature(only)],
1353
+ ["Description", only.description],
1354
+ ["Payload schema", only.payload_schema]
1355
+ ],
1356
+ flags
1357
+ );
1358
+ }
1359
+ if (data.events.length === 0 && data.total > 0) {
1360
+ return [
1361
+ colors.green("Events"),
1362
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
1363
+ ];
1364
+ }
1365
+ if (data.events.length === 0) {
1366
+ return [
1367
+ colors.green("Events"),
1368
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
1369
+ ];
1370
+ }
1371
+ const lines = data.events.flatMap((event) => [
1372
+ ` ${renderEventSignature(event)}`,
1373
+ ` ${summarizeToolDescription(event.description)}`
1374
+ ]);
1375
+ return [
1376
+ colors.green("Events"),
1377
+ ...lines,
1378
+ ...renderEventsTruncationLine(data),
1379
+ "",
1380
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
1381
+ ];
1382
+ };
1320
1383
  var renderInvokeData = (colors, data, flags) => {
1321
1384
  return [colors.green("Result"), formatScalar(data, flags)];
1322
1385
  };
1323
1386
  var renderLinkData = (colors, data, flags, qr) => {
1387
+ if ("attached" in data) {
1388
+ return [
1389
+ colors.green("Page Attached"),
1390
+ ...renderFields(
1391
+ "Page",
1392
+ [
1393
+ ["Session", data.sessionId],
1394
+ ["Tab", data.targetId],
1395
+ ["URL", data.url],
1396
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1397
+ ],
1398
+ flags
1399
+ )
1400
+ ];
1401
+ }
1402
+ if ("script" in data) {
1403
+ return [
1404
+ colors.green("Link Created"),
1405
+ ...renderFields(
1406
+ "Link",
1407
+ [
1408
+ ["Session", data.sessionId],
1409
+ ["URL", data.url],
1410
+ ["Script", data.script],
1411
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1412
+ ],
1413
+ flags
1414
+ )
1415
+ ];
1416
+ }
1324
1417
  const lines = [
1325
1418
  colors.green("Link Created"),
1326
1419
  ...renderFields(
@@ -1441,6 +1534,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
1441
1534
  ["PID", data.daemon.pid],
1442
1535
  ["Started at", data.daemon.started_at],
1443
1536
  ["WSS port", data.daemon.wss_port],
1537
+ ["Web port", data.daemon.web_port],
1444
1538
  ["Pinned keys", data.daemon.pinned_keys.length],
1445
1539
  ["Sessions", data.daemon.session_count]
1446
1540
  ],
@@ -1508,6 +1602,8 @@ var renderSuccessData = (colors, command, data, options) => {
1508
1602
  return renderToolsListData(colors, data, flags, options.full);
1509
1603
  case "tools describe":
1510
1604
  return renderToolDetail(colors, data, flags);
1605
+ case "events ls":
1606
+ return renderEventsListData(colors, data, flags);
1511
1607
  case "tools call":
1512
1608
  return renderInvokeData(colors, data, flags);
1513
1609
  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.");
@@ -923,7 +931,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
923
931
  // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
924
932
  // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
925
933
  // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
926
- // assertion failure (docs/tasks/08-appduct-doctor.md).
934
+ // assertion failure.
927
935
  inspection_error: 66,
928
936
  connection_error: 70,
929
937
  session_error: 71,
@@ -1051,6 +1059,7 @@ var formatJson = (value, flags) => {
1051
1059
  import pc from "picocolors";
1052
1060
  import {
1053
1061
  formatAgentWebSocketUrl,
1062
+ renderEventSignature,
1054
1063
  renderToolSignature,
1055
1064
  summarizeToolDescription
1056
1065
  } from "@appduct/shared";
@@ -1146,6 +1155,13 @@ var formatDevice = (device) => {
1146
1155
  const label = parts.length > 0 ? parts.join(" ") : "unknown device";
1147
1156
  return device.os ? `${label} (${device.os})` : label;
1148
1157
  };
1158
+ var SUSPEND_REASON_LABELS = {
1159
+ app_backgrounded: "app in background",
1160
+ connection_lost: "connection lost"
1161
+ };
1162
+ var formatSessionState = (session) => {
1163
+ return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
1164
+ };
1149
1165
  var renderLsData = (colors, data, now) => {
1150
1166
  if (data.length === 0) {
1151
1167
  return [colors.dim("Sessions"), " No Appduct sessions are registered."];
@@ -1154,7 +1170,7 @@ var renderLsData = (colors, data, now) => {
1154
1170
  const headers = ["Alias", "State", "Device", "Tools", "Age"];
1155
1171
  const rows = data.map((session) => [
1156
1172
  session.alias,
1157
- session.state,
1173
+ formatSessionState(session),
1158
1174
  formatDevice(session.device),
1159
1175
  String(session.toolCount),
1160
1176
  formatAge(session.claimedAt ?? session.createdAt, nowIso)
@@ -1328,10 +1344,87 @@ var renderToolsListData = (colors, data, flags, full) => {
1328
1344
  }
1329
1345
  return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
1330
1346
  };
1347
+ var renderEventsTruncationLine = (data) => {
1348
+ const offset = data.offset ?? 0;
1349
+ if (data.events.length >= data.total) {
1350
+ return [];
1351
+ }
1352
+ return [
1353
+ "",
1354
+ `Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
1355
+ ];
1356
+ };
1357
+ var renderEventsListData = (colors, data, flags) => {
1358
+ const [only] = data.events;
1359
+ if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
1360
+ return renderFields(
1361
+ colors.green(`Event: ${only.name}`),
1362
+ [
1363
+ ["Signature", renderEventSignature(only)],
1364
+ ["Description", only.description],
1365
+ ["Payload schema", only.payload_schema]
1366
+ ],
1367
+ flags
1368
+ );
1369
+ }
1370
+ if (data.events.length === 0 && data.total > 0) {
1371
+ return [
1372
+ colors.green("Events"),
1373
+ ` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
1374
+ ];
1375
+ }
1376
+ if (data.events.length === 0) {
1377
+ return [
1378
+ colors.green("Events"),
1379
+ data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
1380
+ ];
1381
+ }
1382
+ const lines = data.events.flatMap((event) => [
1383
+ ` ${renderEventSignature(event)}`,
1384
+ ` ${summarizeToolDescription(event.description)}`
1385
+ ]);
1386
+ return [
1387
+ colors.green("Events"),
1388
+ ...lines,
1389
+ ...renderEventsTruncationLine(data),
1390
+ "",
1391
+ "Run `appduct events ls --name <name>` for an event's full payload schema."
1392
+ ];
1393
+ };
1331
1394
  var renderInvokeData = (colors, data, flags) => {
1332
1395
  return [colors.green("Result"), formatScalar(data, flags)];
1333
1396
  };
1334
1397
  var renderLinkData = (colors, data, flags, qr) => {
1398
+ if ("attached" in data) {
1399
+ return [
1400
+ colors.green("Page Attached"),
1401
+ ...renderFields(
1402
+ "Page",
1403
+ [
1404
+ ["Session", data.sessionId],
1405
+ ["Tab", data.targetId],
1406
+ ["URL", data.url],
1407
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1408
+ ],
1409
+ flags
1410
+ )
1411
+ ];
1412
+ }
1413
+ if ("script" in data) {
1414
+ return [
1415
+ colors.green("Link Created"),
1416
+ ...renderFields(
1417
+ "Link",
1418
+ [
1419
+ ["Session", data.sessionId],
1420
+ ["URL", data.url],
1421
+ ["Script", data.script],
1422
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()]
1423
+ ],
1424
+ flags
1425
+ )
1426
+ ];
1427
+ }
1335
1428
  const lines = [
1336
1429
  colors.green("Link Created"),
1337
1430
  ...renderFields(
@@ -1452,6 +1545,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
1452
1545
  ["PID", data.daemon.pid],
1453
1546
  ["Started at", data.daemon.started_at],
1454
1547
  ["WSS port", data.daemon.wss_port],
1548
+ ["Web port", data.daemon.web_port],
1455
1549
  ["Pinned keys", data.daemon.pinned_keys.length],
1456
1550
  ["Sessions", data.daemon.session_count]
1457
1551
  ],
@@ -1519,6 +1613,8 @@ var renderSuccessData = (colors, command, data, options) => {
1519
1613
  return renderToolsListData(colors, data, flags, options.full);
1520
1614
  case "tools describe":
1521
1615
  return renderToolDetail(colors, data, flags);
1616
+ case "events ls":
1617
+ return renderEventsListData(colors, data, flags);
1522
1618
  case "tools call":
1523
1619
  return renderInvokeData(colors, data, flags);
1524
1620
  case "sessions revoke":