@agentdeck/bridge 1.0.20 → 1.0.21

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 (175) hide show
  1. package/dist/adapters/openclaw.d.ts +55 -1
  2. package/dist/adapters/openclaw.d.ts.map +1 -1
  3. package/dist/adapters/openclaw.js +278 -45
  4. package/dist/adapters/openclaw.js.map +1 -1
  5. package/dist/adb-reverse.d.ts +26 -3
  6. package/dist/adb-reverse.d.ts.map +1 -1
  7. package/dist/adb-reverse.js +26 -5
  8. package/dist/adb-reverse.js.map +1 -1
  9. package/dist/apme/adapters/openclaw-hook.d.ts +45 -1
  10. package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -1
  11. package/dist/apme/adapters/openclaw-hook.js +84 -4
  12. package/dist/apme/adapters/openclaw-hook.js.map +1 -1
  13. package/dist/apme/claude-transcript-reader.d.ts +54 -0
  14. package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
  15. package/dist/apme/claude-transcript-reader.js +77 -0
  16. package/dist/apme/claude-transcript-reader.js.map +1 -1
  17. package/dist/apme/collector.d.ts +63 -5
  18. package/dist/apme/collector.d.ts.map +1 -1
  19. package/dist/apme/collector.js +147 -10
  20. package/dist/apme/collector.js.map +1 -1
  21. package/dist/apme/store.d.ts +23 -1
  22. package/dist/apme/store.d.ts.map +1 -1
  23. package/dist/apme/store.js +81 -2
  24. package/dist/apme/store.js.map +1 -1
  25. package/dist/apme/types.d.ts +1 -1
  26. package/dist/apme/types.d.ts.map +1 -1
  27. package/dist/apme/types.js.map +1 -1
  28. package/dist/auth.d.ts +37 -0
  29. package/dist/auth.d.ts.map +1 -1
  30. package/dist/auth.js +23 -0
  31. package/dist/auth.js.map +1 -1
  32. package/dist/claude-interrupt-marker.d.ts +31 -0
  33. package/dist/claude-interrupt-marker.d.ts.map +1 -0
  34. package/dist/claude-interrupt-marker.js +58 -0
  35. package/dist/claude-interrupt-marker.js.map +1 -0
  36. package/dist/claude-turn-watchdog.d.ts +23 -0
  37. package/dist/claude-turn-watchdog.d.ts.map +1 -1
  38. package/dist/claude-turn-watchdog.js +30 -4
  39. package/dist/claude-turn-watchdog.js.map +1 -1
  40. package/dist/cli.d.ts +6 -0
  41. package/dist/cli.d.ts.map +1 -1
  42. package/dist/cli.js +398 -115
  43. package/dist/cli.js.map +1 -1
  44. package/dist/daemon-port.d.ts +90 -0
  45. package/dist/daemon-port.d.ts.map +1 -0
  46. package/dist/daemon-port.js +119 -0
  47. package/dist/daemon-port.js.map +1 -0
  48. package/dist/daemon-server.d.ts +14 -1
  49. package/dist/daemon-server.d.ts.map +1 -1
  50. package/dist/daemon-server.js +297 -69
  51. package/dist/daemon-server.js.map +1 -1
  52. package/dist/daemon-settings.d.ts +46 -0
  53. package/dist/daemon-settings.d.ts.map +1 -0
  54. package/dist/daemon-settings.js +85 -0
  55. package/dist/daemon-settings.js.map +1 -0
  56. package/dist/daemon-takeover.d.ts +75 -0
  57. package/dist/daemon-takeover.d.ts.map +1 -0
  58. package/dist/daemon-takeover.js +137 -0
  59. package/dist/daemon-takeover.js.map +1 -0
  60. package/dist/daemon.js +1 -1
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +30 -11
  64. package/dist/index.js.map +1 -1
  65. package/dist/kiro-diagnostics.d.ts +90 -0
  66. package/dist/kiro-diagnostics.d.ts.map +1 -0
  67. package/dist/kiro-diagnostics.js +278 -0
  68. package/dist/kiro-diagnostics.js.map +1 -0
  69. package/dist/kiro-session.d.ts +113 -0
  70. package/dist/kiro-session.d.ts.map +1 -0
  71. package/dist/kiro-session.js +688 -0
  72. package/dist/kiro-session.js.map +1 -0
  73. package/dist/kiro-timeline-feed.d.ts +45 -0
  74. package/dist/kiro-timeline-feed.d.ts.map +1 -0
  75. package/dist/kiro-timeline-feed.js +116 -0
  76. package/dist/kiro-timeline-feed.js.map +1 -0
  77. package/dist/kiro-transcript-timeline.d.ts +57 -0
  78. package/dist/kiro-transcript-timeline.d.ts.map +1 -0
  79. package/dist/kiro-transcript-timeline.js +183 -0
  80. package/dist/kiro-transcript-timeline.js.map +1 -0
  81. package/dist/linux-service.d.ts +2 -1
  82. package/dist/linux-service.d.ts.map +1 -1
  83. package/dist/linux-service.js +8 -3
  84. package/dist/linux-service.js.map +1 -1
  85. package/dist/mdns-discover.d.ts +27 -0
  86. package/dist/mdns-discover.d.ts.map +1 -1
  87. package/dist/mdns-discover.js +60 -7
  88. package/dist/mdns-discover.js.map +1 -1
  89. package/dist/mdns.d.ts.map +1 -1
  90. package/dist/mdns.js +16 -3
  91. package/dist/mdns.js.map +1 -1
  92. package/dist/modules/adb-module.d.ts.map +1 -1
  93. package/dist/modules/adb-module.js +5 -2
  94. package/dist/modules/adb-module.js.map +1 -1
  95. package/dist/modules/index.d.ts +2 -1
  96. package/dist/modules/index.d.ts.map +1 -1
  97. package/dist/modules/index.js +1 -0
  98. package/dist/modules/index.js.map +1 -1
  99. package/dist/modules/types.d.ts +34 -9
  100. package/dist/modules/types.d.ts.map +1 -1
  101. package/dist/modules/types.js +34 -1
  102. package/dist/modules/types.js.map +1 -1
  103. package/dist/network-posture.d.ts +67 -0
  104. package/dist/network-posture.d.ts.map +1 -0
  105. package/dist/network-posture.js +100 -0
  106. package/dist/network-posture.js.map +1 -0
  107. package/dist/observed-steering.d.ts +14 -0
  108. package/dist/observed-steering.d.ts.map +1 -1
  109. package/dist/observed-steering.js +18 -0
  110. package/dist/observed-steering.js.map +1 -1
  111. package/dist/observed-turn-watchdogs.d.ts +84 -0
  112. package/dist/observed-turn-watchdogs.d.ts.map +1 -0
  113. package/dist/observed-turn-watchdogs.js +134 -0
  114. package/dist/observed-turn-watchdogs.js.map +1 -0
  115. package/dist/openclaw-session.d.ts +10 -0
  116. package/dist/openclaw-session.d.ts.map +1 -1
  117. package/dist/openclaw-session.js +12 -0
  118. package/dist/openclaw-session.js.map +1 -1
  119. package/dist/passive-observer.d.ts +19 -0
  120. package/dist/passive-observer.d.ts.map +1 -1
  121. package/dist/passive-observer.js +208 -24
  122. package/dist/passive-observer.js.map +1 -1
  123. package/dist/pixoo/micro-glyphs.d.ts +1 -1
  124. package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
  125. package/dist/pixoo/micro-glyphs.js +2 -0
  126. package/dist/pixoo/micro-glyphs.js.map +1 -1
  127. package/dist/pixoo/official-dot-glyphs.generated.d.ts +1 -1
  128. package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -1
  129. package/dist/pixoo/official-dot-glyphs.generated.js +47 -0
  130. package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -1
  131. package/dist/pixoo/pixoo-discover.d.ts +8 -1
  132. package/dist/pixoo/pixoo-discover.d.ts.map +1 -1
  133. package/dist/pixoo/pixoo-discover.js +15 -8
  134. package/dist/pixoo/pixoo-discover.js.map +1 -1
  135. package/dist/pixoo/pixoo-renderer.d.ts +4 -2
  136. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  137. package/dist/pixoo/pixoo-renderer.js +105 -36
  138. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  139. package/dist/pixoo/pixoo-settings.d.ts +19 -2
  140. package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
  141. package/dist/pixoo/pixoo-settings.js +22 -4
  142. package/dist/pixoo/pixoo-settings.js.map +1 -1
  143. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
  144. package/dist/pixoo/pixoo-sprites.js +2 -0
  145. package/dist/pixoo/pixoo-sprites.js.map +1 -1
  146. package/dist/session-aggregator.d.ts +9 -1
  147. package/dist/session-aggregator.d.ts.map +1 -1
  148. package/dist/session-aggregator.js +1 -1
  149. package/dist/session-aggregator.js.map +1 -1
  150. package/dist/session-registry.d.ts +71 -3
  151. package/dist/session-registry.d.ts.map +1 -1
  152. package/dist/session-registry.js +126 -19
  153. package/dist/session-registry.js.map +1 -1
  154. package/dist/subagent-timeline.d.ts +38 -7
  155. package/dist/subagent-timeline.d.ts.map +1 -1
  156. package/dist/subagent-timeline.js +154 -23
  157. package/dist/subagent-timeline.js.map +1 -1
  158. package/dist/timeline-store.d.ts.map +1 -1
  159. package/dist/timeline-store.js +26 -1
  160. package/dist/timeline-store.js.map +1 -1
  161. package/dist/tui/renderer.d.ts.map +1 -1
  162. package/dist/tui/renderer.js +13 -1
  163. package/dist/tui/renderer.js.map +1 -1
  164. package/dist/tui/terrarium.d.ts.map +1 -1
  165. package/dist/tui/terrarium.js +10 -5
  166. package/dist/tui/terrarium.js.map +1 -1
  167. package/dist/utils/redact-secrets.d.ts +3 -0
  168. package/dist/utils/redact-secrets.d.ts.map +1 -0
  169. package/dist/utils/redact-secrets.js +29 -0
  170. package/dist/utils/redact-secrets.js.map +1 -0
  171. package/dist/windows-service.d.ts +2 -1
  172. package/dist/windows-service.d.ts.map +1 -1
  173. package/dist/windows-service.js +6 -3
  174. package/dist/windows-service.js.map +1 -1
  175. package/package.json +3 -3
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `settings.json` access for the daemon — one reader, one writer.
3
+ *
4
+ * The reader was private to `daemon-server.ts`, which meant anything else that
5
+ * needed a setting either re-implemented the cross-directory discovery or, as
6
+ * the device-settings modules do, hardcoded `homedir()/.agentdeck` and lost the
7
+ * `AGENTDECK_DATA_DIR` override (roadmap item 14). Extracted here so a second
8
+ * consumer inherits the discovery rules rather than a copy of them.
9
+ *
10
+ * Read and write are deliberately asymmetric:
11
+ *
12
+ * - **Read** looks at every candidate data dir and takes the NEWEST file. A CLI
13
+ * world and an app world can both be installed, and the most recently written
14
+ * file is the one the user last edited.
15
+ * - **Write** touches only `getDataDir()` — this process's own directory. A
16
+ * cross-directory write would reach into another install's state (and, on
17
+ * macOS, into a sandbox container this process cannot legally touch).
18
+ */
19
+ /**
20
+ * Newest `settings.json` across candidate data dirs — mirrors the
21
+ * daemon.json/sessions.json cross-dir discovery (and honors the
22
+ * AGENTDECK_DATA_DIR test override, which the hardcoded `~/.agentdeck` path in
23
+ * the device-settings modules ignores). The App Store sandbox container is
24
+ * intentionally NOT a candidate (TCC hang risk — see `getCandidateDataDirs`),
25
+ * so settings written by the sandboxed Swift app stay invisible here; that
26
+ * coexistence limit is documented in docs/appstore-feature-matrix.md.
27
+ *
28
+ * Never throws: an absent, unreadable or malformed file yields `{}`, so a
29
+ * setting's fallback is always the one its own reader defines.
30
+ */
31
+ export declare function loadDaemonSettings(): Record<string, unknown>;
32
+ /**
33
+ * Read-modify-write one key of this process's own `settings.json`.
34
+ *
35
+ * `undefined` deletes the key rather than storing a null — a persisted setting
36
+ * that has been cleared must be indistinguishable from one that was never set,
37
+ * or every reader needs a third case.
38
+ *
39
+ * The write is tmp+rename like every other file this daemon owns: settings.json
40
+ * is read by the daemon, the CLI and the device modules, and a half-written
41
+ * file reads as `{}` — which is to say, as every default at once.
42
+ */
43
+ export declare function updateDaemonSetting(key: string, value: unknown): void;
44
+ /** Absolute path of the `settings.json` this process writes to. */
45
+ export declare function ownSettingsPath(): string;
46
+ //# sourceMappingURL=daemon-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-settings.d.ts","sourceRoot":"","sources":["../src/daemon-settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAa5D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAgBrE;AAED,mEAAmE;AACnE,wBAAgB,eAAe,IAAI,MAAM,CAExC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `settings.json` access for the daemon — one reader, one writer.
3
+ *
4
+ * The reader was private to `daemon-server.ts`, which meant anything else that
5
+ * needed a setting either re-implemented the cross-directory discovery or, as
6
+ * the device-settings modules do, hardcoded `homedir()/.agentdeck` and lost the
7
+ * `AGENTDECK_DATA_DIR` override (roadmap item 14). Extracted here so a second
8
+ * consumer inherits the discovery rules rather than a copy of them.
9
+ *
10
+ * Read and write are deliberately asymmetric:
11
+ *
12
+ * - **Read** looks at every candidate data dir and takes the NEWEST file. A CLI
13
+ * world and an app world can both be installed, and the most recently written
14
+ * file is the one the user last edited.
15
+ * - **Write** touches only `getDataDir()` — this process's own directory. A
16
+ * cross-directory write would reach into another install's state (and, on
17
+ * macOS, into a sandbox container this process cannot legally touch).
18
+ */
19
+ import { readFileSync, writeFileSync, mkdirSync, renameSync, statSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { randomUUID } from 'crypto';
22
+ import { getCandidateDataDirs, getDataDir } from './session-registry.js';
23
+ /**
24
+ * Newest `settings.json` across candidate data dirs — mirrors the
25
+ * daemon.json/sessions.json cross-dir discovery (and honors the
26
+ * AGENTDECK_DATA_DIR test override, which the hardcoded `~/.agentdeck` path in
27
+ * the device-settings modules ignores). The App Store sandbox container is
28
+ * intentionally NOT a candidate (TCC hang risk — see `getCandidateDataDirs`),
29
+ * so settings written by the sandboxed Swift app stay invisible here; that
30
+ * coexistence limit is documented in docs/appstore-feature-matrix.md.
31
+ *
32
+ * Never throws: an absent, unreadable or malformed file yields `{}`, so a
33
+ * setting's fallback is always the one its own reader defines.
34
+ */
35
+ export function loadDaemonSettings() {
36
+ let best = null;
37
+ for (const dir of getCandidateDataDirs()) {
38
+ try {
39
+ const path = join(dir, 'settings.json');
40
+ const mtime = statSync(path).mtimeMs;
41
+ if (best && best.mtime >= mtime)
42
+ continue;
43
+ best = { mtime, parsed: JSON.parse(readFileSync(path, 'utf-8')) };
44
+ }
45
+ catch {
46
+ // Missing or unreadable — skip this candidate.
47
+ }
48
+ }
49
+ return best?.parsed ?? {};
50
+ }
51
+ /**
52
+ * Read-modify-write one key of this process's own `settings.json`.
53
+ *
54
+ * `undefined` deletes the key rather than storing a null — a persisted setting
55
+ * that has been cleared must be indistinguishable from one that was never set,
56
+ * or every reader needs a third case.
57
+ *
58
+ * The write is tmp+rename like every other file this daemon owns: settings.json
59
+ * is read by the daemon, the CLI and the device modules, and a half-written
60
+ * file reads as `{}` — which is to say, as every default at once.
61
+ */
62
+ export function updateDaemonSetting(key, value) {
63
+ const dir = getDataDir();
64
+ mkdirSync(dir, { recursive: true });
65
+ const path = join(dir, 'settings.json');
66
+ let settings = {};
67
+ try {
68
+ settings = JSON.parse(readFileSync(path, 'utf-8'));
69
+ }
70
+ catch {
71
+ // Missing or malformed — start from empty rather than refusing to write.
72
+ }
73
+ if (value === undefined)
74
+ delete settings[key];
75
+ else
76
+ settings[key] = value;
77
+ const tmp = join(dir, `.settings.${randomUUID()}.tmp`);
78
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
79
+ renameSync(tmp, path);
80
+ }
81
+ /** Absolute path of the `settings.json` this process writes to. */
82
+ export function ownSettingsPath() {
83
+ return join(getDataDir(), 'settings.json');
84
+ }
85
+ //# sourceMappingURL=daemon-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-settings.js","sourceRoot":"","sources":["../src/daemon-settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAClF,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEzE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,IAAI,GAA8D,IAAI,CAAC;IAC3E,KAAK,MAAM,GAAG,IAAI,oBAAoB,EAAE,EAAE,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;YACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;YACrC,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,KAAK;gBAAE,SAAS;YAC1C,IAAI,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAA4B,EAAE,CAAC;QAC/F,CAAC;QAAC,MAAM,CAAC;YACP,+CAA+C;QACjD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW,EAAE,KAAc;IAC7D,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IACxC,IAAI,QAAQ,GAA4B,EAAE,CAAC;IAC3C,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAA4B,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;IAC3E,CAAC;IACD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC;;QACzC,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAE3B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,aAAa,UAAU,EAAE,MAAM,CAAC,CAAC;IACvD,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;IACtE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,CAAC,UAAU,EAAE,EAAE,eAAe,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * What `agentdeck daemon start` does about a daemon already answering on the
3
+ * port it wants.
4
+ *
5
+ * This used to live inline in the CLI action, which made it untestable and hid
6
+ * how much trust it extends: it ADOPTS the incumbent's pairing token and can
7
+ * ask it to stand down or shut down — all on the strength of `/health`
8
+ * answering on 127.0.0.1. That was right when the only two daemons on a
9
+ * machine were the Node CLI and the macOS app belonging to the same human. On
10
+ * a shared host it is a credential leak in one direction and a denial of
11
+ * service in the other: user B's `daemon start` copies user A's token into B's
12
+ * home, then demotes A's daemon (see docs/ENTERPRISE-ROADMAP.md §1.2, §1.3).
13
+ *
14
+ * So the decision now asks who owns the incumbent process before doing either.
15
+ * Everything with an effect is a dependency, which is what lets a test drive
16
+ * this against a foreign-owned incumbent and prove the adopt and the
17
+ * stand-down never happened — a truth table over the ownership predicate would
18
+ * stay green while this function forgot to call it.
19
+ */
20
+ import { type LocalPeerOwnership } from './auth.js';
21
+ /** The subset of `/health` this decision reads. */
22
+ export interface IncumbentHealth {
23
+ mode?: string;
24
+ pid?: number;
25
+ isSwift?: boolean;
26
+ pairingToken?: string;
27
+ }
28
+ /**
29
+ * Everything with an effect is required, not optional-with-a-default. A
30
+ * defaulted `standDown` that resolves false reads as "the app refused" and a
31
+ * defaulted `waitForBindable` as "the port never came back" — so a caller that
32
+ * forgot to wire one would take a fallback port and log a hardware excuse for
33
+ * it, silently and plausibly. Making them required turns that into a type
34
+ * error. Only the two dependencies whose real implementation is always correct
35
+ * (ownership, logging) carry defaults.
36
+ */
37
+ export interface TakeoverDeps {
38
+ ownership?: (pid: number | null | undefined) => LocalPeerOwnership;
39
+ log?: (msg: string) => void;
40
+ /** Returns true when the served token actually changed. */
41
+ adoptToken: (token: unknown) => boolean;
42
+ standDown: (port: number) => Promise<boolean>;
43
+ shutdown: (port: number) => Promise<void>;
44
+ waitForExit: (port: number, timeoutMs: number) => Promise<boolean>;
45
+ waitForBindable: (port: number, timeoutMs: number) => Promise<boolean>;
46
+ }
47
+ export type TakeoverOutcome =
48
+ /** Nothing in the way (or it yielded) — go bind. */
49
+ 'proceed'
50
+ /** Our own daemon is already serving this port; exit 0. */
51
+ | 'already-running'
52
+ /** The user asked for a port we must not take. Exit 1 with the reason. */
53
+ | 'refuse';
54
+ /**
55
+ * True when a daemon answering on this machine belongs to a DIFFERENT OS user,
56
+ * and must therefore be left alone entirely: not evicted, not stood down, not
57
+ * conceded to, not adopted from.
58
+ *
59
+ * The daemon's own startup sweeps the whole port window and shuts down every
60
+ * Swift daemon it finds there, which on a shared host means shutting down a
61
+ * coworker's macOS app the moment this user starts a CLI daemon. Only
62
+ * `other-user` — a definite refusal from the OS — blocks; `unknown` stays
63
+ * permissive for the same reason it does everywhere else (see
64
+ * `localPeerOwnership`), so a peer that reports no pid keeps working exactly
65
+ * as it did before.
66
+ */
67
+ export declare function isForeignDaemon(health: {
68
+ pid?: number;
69
+ } | null | undefined, ownership?: (pid: number | null | undefined) => LocalPeerOwnership): boolean;
70
+ export declare function negotiateIncumbentDaemon(args: {
71
+ port: number;
72
+ incumbent: IncumbentHealth | null;
73
+ portWasExplicit: boolean;
74
+ }, deps: TakeoverDeps): Promise<TakeoverOutcome>;
75
+ //# sourceMappingURL=daemon-takeover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-takeover.d.ts","sourceRoot":"","sources":["../src/daemon-takeover.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAGxE,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,kBAAkB,CAAC;IACnE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,2DAA2D;IAC3D,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IACxC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxE;AAED,MAAM,MAAM,eAAe;AACzB,oDAAoD;AAClD,SAAS;AACX,2DAA2D;GACzD,iBAAiB;AACnB,0EAA0E;GACxE,QAAQ,CAAC;AAEb;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EAC3C,SAAS,GAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,kBAAuC,GACrF,OAAO,CAET;AASD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAAC,eAAe,EAAE,OAAO,CAAA;CAAE,EACnF,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,eAAe,CAAC,CAmG1B"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * What `agentdeck daemon start` does about a daemon already answering on the
3
+ * port it wants.
4
+ *
5
+ * This used to live inline in the CLI action, which made it untestable and hid
6
+ * how much trust it extends: it ADOPTS the incumbent's pairing token and can
7
+ * ask it to stand down or shut down — all on the strength of `/health`
8
+ * answering on 127.0.0.1. That was right when the only two daemons on a
9
+ * machine were the Node CLI and the macOS app belonging to the same human. On
10
+ * a shared host it is a credential leak in one direction and a denial of
11
+ * service in the other: user B's `daemon start` copies user A's token into B's
12
+ * home, then demotes A's daemon (see docs/ENTERPRISE-ROADMAP.md §1.2, §1.3).
13
+ *
14
+ * So the decision now asks who owns the incumbent process before doing either.
15
+ * Everything with an effect is a dependency, which is what lets a test drive
16
+ * this against a foreign-owned incumbent and prove the adopt and the
17
+ * stand-down never happened — a truth table over the ownership predicate would
18
+ * stay green while this function forgot to call it.
19
+ */
20
+ import { localPeerOwnership } from './auth.js';
21
+ import { log as defaultLog } from './logger.js';
22
+ /**
23
+ * True when a daemon answering on this machine belongs to a DIFFERENT OS user,
24
+ * and must therefore be left alone entirely: not evicted, not stood down, not
25
+ * conceded to, not adopted from.
26
+ *
27
+ * The daemon's own startup sweeps the whole port window and shuts down every
28
+ * Swift daemon it finds there, which on a shared host means shutting down a
29
+ * coworker's macOS app the moment this user starts a CLI daemon. Only
30
+ * `other-user` — a definite refusal from the OS — blocks; `unknown` stays
31
+ * permissive for the same reason it does everywhere else (see
32
+ * `localPeerOwnership`), so a peer that reports no pid keeps working exactly
33
+ * as it did before.
34
+ */
35
+ export function isForeignDaemon(health, ownership = localPeerOwnership) {
36
+ return ownership(health?.pid) === 'other-user';
37
+ }
38
+ /** 12s: `waitForDaemonExit` only proves the daemon stopped ANSWERING. */
39
+ const EXIT_WAIT_MS = 12_000;
40
+ /** 30s, and not padding: macOS keeps a NECP reservation on a cancelled
41
+ * listener's port for ~14s, during which `lsof` shows no sockets at all and
42
+ * bind() still returns EADDRINUSE (measured 2026-08-06 — bindable at ~17s). */
43
+ const BINDABLE_WAIT_MS = 30_000;
44
+ export async function negotiateIncumbentDaemon(args, deps) {
45
+ const { port, incumbent, portWasExplicit } = args;
46
+ if (incumbent?.mode !== 'daemon')
47
+ return 'proceed';
48
+ const log = deps.log ?? defaultLog;
49
+ const ownership = (deps.ownership ?? localPeerOwnership)(incumbent.pid);
50
+ if (ownership === 'other-user') {
51
+ // Neither half of the handover is available across users. Adopting would
52
+ // copy their credential into this user's home; standing them down would
53
+ // let any local account demote any other's daemon. Both are refused
54
+ // together — a daemon we may not talk to is also one we may not take the
55
+ // port from.
56
+ log(`Port ${port} is held by a daemon belonging to another user on this machine.`);
57
+ log(`Not adopting its pairing token and not asking it to stand down — a daemon is per-user.`);
58
+ if (portWasExplicit) {
59
+ log(`Choose another port with -p, or ask that user to stop their daemon.`);
60
+ return 'refuse';
61
+ }
62
+ log(`Starting on a free port instead; clients of yours resolve it from your own daemon.json.`);
63
+ return 'proceed';
64
+ }
65
+ if (ownership === 'unknown') {
66
+ // Permissive on purpose (see `localPeerOwnership`), but never silently:
67
+ // this is the branch where a token crosses a process boundary without
68
+ // proof of who is on the other side.
69
+ log(`Could not confirm which user owns the daemon on port ${port} (it reports no pid) — `
70
+ + `proceeding as if it were yours.`);
71
+ }
72
+ // Take over the fleet's credential along with the port. The app's daemon
73
+ // keeps its token inside its sandbox container, unreadable from here, so
74
+ // this loopback answer is the only place we can learn what every paired
75
+ // board is currently holding. Adopting it BEFORE the stand-down means the
76
+ // handover costs no device its pairing.
77
+ if (deps.adoptToken(incumbent.pairingToken)) {
78
+ log(`Adopted the incumbent daemon's pairing token so paired devices survive the handover.`);
79
+ }
80
+ if (!incumbent.isSwift) {
81
+ log(`Daemon already running on port ${port}. Use 'agentdeck daemon stop' first.`);
82
+ return 'already-running';
83
+ }
84
+ // Reverse two-tier upgrade path: the macOS app may already own the canonical
85
+ // port with its in-process Swift daemon (Tier 1 — limited: no ADB devices,
86
+ // no subscription usage, …). A plain "already running → exit" would strand
87
+ // the user on Tier 1. Ask it to STAND DOWN (demote to client, app keeps
88
+ // running) so this CLI daemon can take over with the full feature set.
89
+ log(`AgentDeck app's in-process daemon holds port ${port} — requesting stand-down to take over with the full CLI feature set…`);
90
+ // Prefer /stand-down (clean demote: the app stays running as a client).
91
+ // Fall back to /shutdown for older app builds that predate the endpoint.
92
+ let acked = await deps.standDown(port);
93
+ if (!acked) {
94
+ await deps.shutdown(port);
95
+ acked = true; // shutdown is best-effort (no ack body); rely on the exit wait
96
+ }
97
+ // Two conditions, not one. `waitForDaemonExit` proves the app stopped
98
+ // answering; `waitForBindable` proves the socket is actually gone. The app
99
+ // releases the port with NWListener.cancel(), which returns before the
100
+ // teardown completes — binding on the first signal alone is what silently
101
+ // demoted this daemon to the 9121 fallback and left the canonical port
102
+ // ownerless. The app's takeover-yield window is longer (45s), so it stays
103
+ // off the port until well after this wait has claimed it.
104
+ const yielded = acked
105
+ && await deps.waitForExit(port, EXIT_WAIT_MS)
106
+ && await deps.waitForBindable(port, BINDABLE_WAIT_MS);
107
+ if (yielded) {
108
+ log(`App daemon yielded port ${port}. Starting CLI daemon…`);
109
+ return 'proceed';
110
+ }
111
+ // The app yielded but the PORT did not come back, and that is often not the
112
+ // app's doing: a WiFi device that has gone to sleep with bytes still queued
113
+ // leaves its socket in LAST_ACK on this port, and TCP holds that until it
114
+ // exhausts retransmits — minutes, sometimes. Network.framework exposes no
115
+ // SO_LINGER, so the app cannot RST out of it (measured 2026-08-06: two
116
+ // sleeping ESP32 boards, 11901 and 4 bytes queued, were the only things left
117
+ // on 9120).
118
+ //
119
+ // Carry on rather than exiting. The daemon binds a fallback port and writes
120
+ // it to daemon.json, which is what every client resolves through and what the
121
+ // app itself re-reads when its yield window ends — so the deck keeps working.
122
+ // What must NOT happen is this being silent, which is how the canonical port
123
+ // ended up owned by nobody with the CLI reporting success.
124
+ log(`The AgentDeck app yielded port ${port} but the port has not been released `
125
+ + `(usually a sleeping WiFi device holding a half-closed socket on it).`);
126
+ if (portWasExplicit) {
127
+ // The port was ASKED for, not defaulted to. Quietly binding a different one
128
+ // would answer a question the user did not ask; an explicit choice fails
129
+ // loudly instead.
130
+ log(`Quit the AgentDeck app and retry, or choose another port.`);
131
+ return 'refuse';
132
+ }
133
+ log(`Starting on a fallback port instead — clients resolve it from daemon.json. `
134
+ + `For the canonical port, quit the AgentDeck app and retry, or pass -p.`);
135
+ return 'proceed';
136
+ }
137
+ //# sourceMappingURL=daemon-takeover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-takeover.js","sourceRoot":"","sources":["../src/daemon-takeover.ts"],"names":[],"mappings":"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"}
package/dist/daemon.js CHANGED
@@ -17,7 +17,7 @@ const program = new Command();
17
17
  program
18
18
  .name('agentdeck')
19
19
  .description('AgentDeck daemon (legacy entry point — use `agentdeck daemon` instead)')
20
- .version('1.0.20');
20
+ .version('1.0.21');
21
21
  program
22
22
  .command('start', { isDefault: true })
23
23
  .description('Start monitoring daemon')
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Exports `startSession()` called by cli.ts.
5
5
  * Uses BridgeCore for shared infrastructure.
6
6
  */
7
- import type { ModuleConfigs } from './modules/types.js';
7
+ import { type ModuleConfigs } from './modules/types.js';
8
8
  import { type AgentType } from './types.js';
9
9
  export interface SessionOptions {
10
10
  agentType: AgentType;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6DH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAUxD,OAAO,EAOL,KAAK,SAAS,EAQf,MAAM,YAAY,CAAC;AAwDpB,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,wEAAwE;IACxE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;kFAC8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;0EACsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;gEAC4D;IAC5D,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmrCtE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6DH,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAUvE,OAAO,EAOL,KAAK,SAAS,EAQf,MAAM,YAAY,CAAC;AAwDpB,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,wEAAwE;IACxE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;kFAC8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;0EACsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;gEAC4D;IAC5D,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA0rCtE"}
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ import { DaemonWsClient } from './daemon-ws-client.js';
43
43
  import { getLanIp } from '@agentdeck/shared';
44
44
  import { migrateHooksIfNeeded } from '@agentdeck/hooks';
45
45
  import { initModules, stopModules, createDefaultModules, } from './modules/index.js';
46
+ import { allModulesOff } from './modules/types.js';
46
47
  import { onESP32Message, sendWifiProvisionToAll, } from './esp32-serial.js';
47
48
  import { loadWifiConfig } from './wifi-config.js';
48
49
  import { getAdbDeviceCount } from './adb-reverse.js';
@@ -348,12 +349,12 @@ export async function startSession(opts) {
348
349
  });
349
350
  // ===== Device modules =====
350
351
  const moduleConfigs = {
351
- ...(opts.modules ?? {
352
- adb: 'auto',
353
- serial: false, // daemon-only session bridges never talk to ESP32
354
- pixoo: false, // daemon-only session bridges never talk to Pixoo
355
- timebox: false, // daemon-only — session bridges never talk to Timebox
356
- }),
352
+ // "All off, then add back ADB" rather than a literal list of the modules
353
+ // that were known when this was written: initModules() reads an absent key
354
+ // as 'auto', so every module added since arrived here switched ON. That is
355
+ // how `idotmatrix` ended up spawning a Python BLE client per session
356
+ // bridge, duplicating the daemon's own.
357
+ ...(opts.modules ?? { ...allModulesOff(), adb: 'auto' }),
357
358
  // Session bridges are internal hook/PTY processes. Only the daemon hub is
358
359
  // discoverable on the LAN; never advertise per-session project metadata.
359
360
  mdns: false,
@@ -468,11 +469,18 @@ export async function startSession(opts) {
468
469
  // this same event pipe — state machine, timeline, and APME all close via
469
470
  // their existing Stop paths, and a late real Stop dedups downstream.
470
471
  const turnWatchdog = new ClaudeTurnWatchdog({
471
- onMissedStop: ({ transcript_path }) => {
472
+ onMissedStop: ({ transcript_path, reason }) => {
472
473
  adapter.emit('event', {
473
474
  source: 'hook',
474
475
  event: 'Stop',
475
- data: { transcript_path, synthetic_stop: true },
476
+ data: {
477
+ transcript_path,
478
+ synthetic_stop: true,
479
+ // A user cancel and a client abort are not lost hooks — same
480
+ // close, different buckets.
481
+ ...(reason === 'interrupted' ? { interrupted: true } : {}),
482
+ ...(reason === 'aborted' ? { aborted: true } : {}),
483
+ },
476
484
  });
477
485
  },
478
486
  });
@@ -1521,10 +1529,21 @@ function wireClaudeCodeTimeline(adapter, core) {
1521
1529
  debug('agentdeck', `transcript read skipped: ${String(err)}`);
1522
1530
  }
1523
1531
  }
1524
- // APME: store Claude's response on the current turn
1532
+ // APME: store Claude's response on the current turn, then close the
1533
+ // turn at the Stop that produced it (response first — the trajectory
1534
+ // event and response_kind tag read the still-active turn). The close
1535
+ // records WHICH Stop closed it: a watchdog-injected synthetic Stop is
1536
+ // one that the real hook dropped, and counting those is the standing
1537
+ // measurement of hook loss (turns.end_source).
1525
1538
  const apmeRef = core.getApme();
1526
- if (apmeRef && lastAssistantMsg) {
1527
- apmeRef.collector.setTurnResponse(core.sessionId, lastAssistantMsg);
1539
+ if (apmeRef) {
1540
+ if (lastAssistantMsg)
1541
+ apmeRef.collector.setTurnResponse(core.sessionId, lastAssistantMsg);
1542
+ apmeRef.collector.noteTurnStop(core.sessionId, {
1543
+ synthetic: evt.data?.synthetic_stop === true,
1544
+ interrupted: evt.data?.interrupted === true,
1545
+ aborted: evt.data?.aborted === true,
1546
+ });
1528
1547
  }
1529
1548
  emitCompletion(lastAssistantMsg, duration, toolSummary);
1530
1549
  break;