@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.
- package/dist/adapters/openclaw.d.ts +55 -1
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +278 -45
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/adb-reverse.d.ts +26 -3
- package/dist/adb-reverse.d.ts.map +1 -1
- package/dist/adb-reverse.js +26 -5
- package/dist/adb-reverse.js.map +1 -1
- package/dist/apme/adapters/openclaw-hook.d.ts +45 -1
- package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -1
- package/dist/apme/adapters/openclaw-hook.js +84 -4
- package/dist/apme/adapters/openclaw-hook.js.map +1 -1
- package/dist/apme/claude-transcript-reader.d.ts +54 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
- package/dist/apme/claude-transcript-reader.js +77 -0
- package/dist/apme/claude-transcript-reader.js.map +1 -1
- package/dist/apme/collector.d.ts +63 -5
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +147 -10
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/store.d.ts +23 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +81 -2
- package/dist/apme/store.js.map +1 -1
- package/dist/apme/types.d.ts +1 -1
- package/dist/apme/types.d.ts.map +1 -1
- package/dist/apme/types.js.map +1 -1
- package/dist/auth.d.ts +37 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +23 -0
- package/dist/auth.js.map +1 -1
- package/dist/claude-interrupt-marker.d.ts +31 -0
- package/dist/claude-interrupt-marker.d.ts.map +1 -0
- package/dist/claude-interrupt-marker.js +58 -0
- package/dist/claude-interrupt-marker.js.map +1 -0
- package/dist/claude-turn-watchdog.d.ts +23 -0
- package/dist/claude-turn-watchdog.d.ts.map +1 -1
- package/dist/claude-turn-watchdog.js +30 -4
- package/dist/claude-turn-watchdog.js.map +1 -1
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +398 -115
- package/dist/cli.js.map +1 -1
- package/dist/daemon-port.d.ts +90 -0
- package/dist/daemon-port.d.ts.map +1 -0
- package/dist/daemon-port.js +119 -0
- package/dist/daemon-port.js.map +1 -0
- package/dist/daemon-server.d.ts +14 -1
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +297 -69
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon-settings.d.ts +46 -0
- package/dist/daemon-settings.d.ts.map +1 -0
- package/dist/daemon-settings.js +85 -0
- package/dist/daemon-settings.js.map +1 -0
- package/dist/daemon-takeover.d.ts +75 -0
- package/dist/daemon-takeover.d.ts.map +1 -0
- package/dist/daemon-takeover.js +137 -0
- package/dist/daemon-takeover.js.map +1 -0
- package/dist/daemon.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -11
- package/dist/index.js.map +1 -1
- package/dist/kiro-diagnostics.d.ts +90 -0
- package/dist/kiro-diagnostics.d.ts.map +1 -0
- package/dist/kiro-diagnostics.js +278 -0
- package/dist/kiro-diagnostics.js.map +1 -0
- package/dist/kiro-session.d.ts +113 -0
- package/dist/kiro-session.d.ts.map +1 -0
- package/dist/kiro-session.js +688 -0
- package/dist/kiro-session.js.map +1 -0
- package/dist/kiro-timeline-feed.d.ts +45 -0
- package/dist/kiro-timeline-feed.d.ts.map +1 -0
- package/dist/kiro-timeline-feed.js +116 -0
- package/dist/kiro-timeline-feed.js.map +1 -0
- package/dist/kiro-transcript-timeline.d.ts +57 -0
- package/dist/kiro-transcript-timeline.d.ts.map +1 -0
- package/dist/kiro-transcript-timeline.js +183 -0
- package/dist/kiro-transcript-timeline.js.map +1 -0
- package/dist/linux-service.d.ts +2 -1
- package/dist/linux-service.d.ts.map +1 -1
- package/dist/linux-service.js +8 -3
- package/dist/linux-service.js.map +1 -1
- package/dist/mdns-discover.d.ts +27 -0
- package/dist/mdns-discover.d.ts.map +1 -1
- package/dist/mdns-discover.js +60 -7
- package/dist/mdns-discover.js.map +1 -1
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +16 -3
- package/dist/mdns.js.map +1 -1
- package/dist/modules/adb-module.d.ts.map +1 -1
- package/dist/modules/adb-module.js +5 -2
- package/dist/modules/adb-module.js.map +1 -1
- package/dist/modules/index.d.ts +2 -1
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -0
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/types.d.ts +34 -9
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js +34 -1
- package/dist/modules/types.js.map +1 -1
- package/dist/network-posture.d.ts +67 -0
- package/dist/network-posture.d.ts.map +1 -0
- package/dist/network-posture.js +100 -0
- package/dist/network-posture.js.map +1 -0
- package/dist/observed-steering.d.ts +14 -0
- package/dist/observed-steering.d.ts.map +1 -1
- package/dist/observed-steering.js +18 -0
- package/dist/observed-steering.js.map +1 -1
- package/dist/observed-turn-watchdogs.d.ts +84 -0
- package/dist/observed-turn-watchdogs.d.ts.map +1 -0
- package/dist/observed-turn-watchdogs.js +134 -0
- package/dist/observed-turn-watchdogs.js.map +1 -0
- package/dist/openclaw-session.d.ts +10 -0
- package/dist/openclaw-session.d.ts.map +1 -1
- package/dist/openclaw-session.js +12 -0
- package/dist/openclaw-session.js.map +1 -1
- package/dist/passive-observer.d.ts +19 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +208 -24
- package/dist/passive-observer.js.map +1 -1
- package/dist/pixoo/micro-glyphs.d.ts +1 -1
- package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
- package/dist/pixoo/micro-glyphs.js +2 -0
- package/dist/pixoo/micro-glyphs.js.map +1 -1
- package/dist/pixoo/official-dot-glyphs.generated.d.ts +1 -1
- package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -1
- package/dist/pixoo/official-dot-glyphs.generated.js +47 -0
- package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -1
- package/dist/pixoo/pixoo-discover.d.ts +8 -1
- package/dist/pixoo/pixoo-discover.d.ts.map +1 -1
- package/dist/pixoo/pixoo-discover.js +15 -8
- package/dist/pixoo/pixoo-discover.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts +4 -2
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +105 -36
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pixoo/pixoo-settings.d.ts +19 -2
- package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
- package/dist/pixoo/pixoo-settings.js +22 -4
- package/dist/pixoo/pixoo-settings.js.map +1 -1
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
- package/dist/pixoo/pixoo-sprites.js +2 -0
- package/dist/pixoo/pixoo-sprites.js.map +1 -1
- package/dist/session-aggregator.d.ts +9 -1
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js +1 -1
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-registry.d.ts +71 -3
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +126 -19
- package/dist/session-registry.js.map +1 -1
- package/dist/subagent-timeline.d.ts +38 -7
- package/dist/subagent-timeline.d.ts.map +1 -1
- package/dist/subagent-timeline.js +154 -23
- package/dist/subagent-timeline.js.map +1 -1
- package/dist/timeline-store.d.ts.map +1 -1
- package/dist/timeline-store.js +26 -1
- package/dist/timeline-store.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +13 -1
- package/dist/tui/renderer.js.map +1 -1
- package/dist/tui/terrarium.d.ts.map +1 -1
- package/dist/tui/terrarium.js +10 -5
- package/dist/tui/terrarium.js.map +1 -1
- package/dist/utils/redact-secrets.d.ts +3 -0
- package/dist/utils/redact-secrets.d.ts.map +1 -0
- package/dist/utils/redact-secrets.js +29 -0
- package/dist/utils/redact-secrets.js.map +1 -0
- package/dist/windows-service.d.ts +2 -1
- package/dist/windows-service.d.ts.map +1 -1
- package/dist/windows-service.js +6 -3
- package/dist/windows-service.js.map +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -8,10 +8,13 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
import { request } from 'http';
|
|
10
10
|
import { BRIDGE_WS_PORT } from './types.js';
|
|
11
|
-
import { SESSION_WEIGHT_MIN, SESSION_WEIGHT_MAX } from '@agentdeck/shared';
|
|
11
|
+
import { SESSION_WEIGHT_MIN, SESSION_WEIGHT_MAX, stopDeliveryLoss } from '@agentdeck/shared';
|
|
12
12
|
import { ensureBleRuntime, getBleRuntimeStatus } from './python-ble-runtime.js';
|
|
13
13
|
import { TASK_NAME, installWindowsTask, taskExists, runWindowsTask, endWindowsTask, deleteWindowsTask, } from './windows-service.js';
|
|
14
|
-
import { deriveRemoteAttachOpts } from './session-registry.js';
|
|
14
|
+
import { deriveRemoteAttachOpts, daemonPortWindow, isDefaultDaemonPortWindow, DEFAULT_DAEMON_PORT_WINDOW, } from './session-registry.js';
|
|
15
|
+
// From modules/types.js, not modules/index.js: the latter pulls the whole
|
|
16
|
+
// device stack (serial, Pixoo, BLE) into `agentdeck --help`.
|
|
17
|
+
import { allModulesOff } from './modules/types.js';
|
|
15
18
|
import { SERVICE_NAME, hasSystemctl, unitExists, installUnit, startUnit, disableUnit, getUnitPath, getDataDir, } from './linux-service.js';
|
|
16
19
|
const require = createRequire(import.meta.url);
|
|
17
20
|
const packageJson = require('../package.json');
|
|
@@ -41,6 +44,22 @@ async function refreshClaudeHooks() {
|
|
|
41
44
|
log(`Claude hooks unavailable: ${String(err)}`);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
47
|
+
/** Kiro v3 global hooks preserve native `kiro-cli --v3` launch behavior. */
|
|
48
|
+
async function refreshKiroHooks() {
|
|
49
|
+
try {
|
|
50
|
+
const { installKiroHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
51
|
+
const result = installKiroHooksIfNeeded();
|
|
52
|
+
if (result.installed) {
|
|
53
|
+
log(`Kiro v3 lifecycle hooks installed/refreshed in ${result.path}`);
|
|
54
|
+
}
|
|
55
|
+
else if (result.reason) {
|
|
56
|
+
log(`Kiro hooks skipped: ${result.reason}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
log(`Kiro hooks unavailable: ${String(err)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
44
63
|
/**
|
|
45
64
|
* Parse a `--weight <n>` value into an integer sort override. Rejects
|
|
46
65
|
* non-integers so a typo (`--weight foo`) fails loudly instead of silently
|
|
@@ -65,6 +84,19 @@ function formatBytes(value) {
|
|
|
65
84
|
return `${Math.round(bytes / 1024)}KB`;
|
|
66
85
|
return `${bytes}B`;
|
|
67
86
|
}
|
|
87
|
+
/** Parse a `--since`-style lookback (`90m`, `6h`, `3d`, `2w`) into milliseconds.
|
|
88
|
+
* Returns null for anything unrecognized so the caller can reject it loudly —
|
|
89
|
+
* silently defaulting a typo'd window would report a rate over the wrong span. */
|
|
90
|
+
function parseLookbackWindow(value) {
|
|
91
|
+
const m = /^(\d+(?:\.\d+)?)\s*(m|h|d|w)$/i.exec(String(value ?? '').trim());
|
|
92
|
+
if (!m)
|
|
93
|
+
return null;
|
|
94
|
+
const n = Number(m[1]);
|
|
95
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
96
|
+
return null;
|
|
97
|
+
const unit = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[m[2].toLowerCase()];
|
|
98
|
+
return n * unit;
|
|
99
|
+
}
|
|
68
100
|
function postJsonWithTimeout(urlString, body, timeoutMs) {
|
|
69
101
|
return new Promise((resolve, reject) => {
|
|
70
102
|
const payload = JSON.stringify(body);
|
|
@@ -113,9 +145,54 @@ function getAgentdeckBin() {
|
|
|
113
145
|
return join(distDir, 'cli.js');
|
|
114
146
|
}
|
|
115
147
|
}
|
|
116
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Extra `daemon start` argv the autostart unit must carry.
|
|
150
|
+
*
|
|
151
|
+
* A posture that only applies when typed by hand is not a posture: an
|
|
152
|
+
* enterprise install IS an autostart install, so the flags have to live in the
|
|
153
|
+
* LaunchAgent / Scheduled Task / systemd unit. Baking them into argv (rather
|
|
154
|
+
* than an environment variable) is the one mechanism all three writers already
|
|
155
|
+
* have — Task Scheduler has no environment element at all.
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* Warn before handing out a LAN credential that cannot be used.
|
|
159
|
+
*
|
|
160
|
+
* `agentdeck qr` and `agentdeck pair` both hand a phone or a board a
|
|
161
|
+
* `ws://<lan-ip>:<port>` endpoint. Against a loopback-only daemon that endpoint
|
|
162
|
+
* refuses every connection, and nothing on either side says why — the user sees
|
|
163
|
+
* a QR that "doesn't work". Ask the daemon what posture it is in (that is what
|
|
164
|
+
* `posture` on `/health` is for) and say so, rather than printing a dead URL.
|
|
165
|
+
* Silent on any probe failure: this is an explanation, never a gate.
|
|
166
|
+
*/
|
|
167
|
+
async function warnIfLoopbackPosture(port) {
|
|
168
|
+
try {
|
|
169
|
+
const { probeDaemonHealth } = await import('./session-registry.js');
|
|
170
|
+
const health = await probeDaemonHealth(port);
|
|
171
|
+
if (health?.posture?.loopbackOnly !== true)
|
|
172
|
+
return;
|
|
173
|
+
log('Note: this daemon is running loopback-only — it is bound to 127.0.0.1 and refuses LAN connections.');
|
|
174
|
+
log(' A phone, tablet or WiFi board cannot reach the address below, whatever credential it holds.');
|
|
175
|
+
log(' Restart without --loopback (and unset AGENTDECK_LOOPBACK_ONLY) to pair a LAN device.');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* daemon not answering — nothing to say about its posture */
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export function daemonPostureArgs(opts) {
|
|
182
|
+
const args = [];
|
|
183
|
+
// --enterprise is the admin-facing spelling of the loopback posture: bind
|
|
184
|
+
// 127.0.0.1 and put nothing on the wire. It deliberately does NOT imply
|
|
185
|
+
// --local — some sites want hardware on a lab subnet with discovery off.
|
|
186
|
+
if (opts.local)
|
|
187
|
+
args.push('--local');
|
|
188
|
+
if (opts.loopback || opts.enterprise)
|
|
189
|
+
args.push('--loopback');
|
|
190
|
+
return args;
|
|
191
|
+
}
|
|
192
|
+
export function buildPlist(extraArgs = []) {
|
|
117
193
|
const bin = getAgentdeckBin();
|
|
118
194
|
const logDir = join(homedir(), '.agentdeck');
|
|
195
|
+
const extra = extraArgs.map((a) => `\n <string>${a}</string>`).join('');
|
|
119
196
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
120
197
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
121
198
|
<plist version="1.0">
|
|
@@ -127,7 +204,7 @@ function buildPlist() {
|
|
|
127
204
|
<string>${bin}</string>
|
|
128
205
|
<string>daemon</string>
|
|
129
206
|
<string>start</string>
|
|
130
|
-
<string>--foreground</string
|
|
207
|
+
<string>--foreground</string>${extra}
|
|
131
208
|
</array>
|
|
132
209
|
<key>RunAtLoad</key>
|
|
133
210
|
<true/>
|
|
@@ -150,9 +227,18 @@ function buildPlist() {
|
|
|
150
227
|
}
|
|
151
228
|
// ===== Helpers =====
|
|
152
229
|
async function stopDaemon(port) {
|
|
153
|
-
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
230
|
+
const { readDaemonInfo, findDaemonPort, probeDaemonHealth } = await import('./session-registry.js');
|
|
231
|
+
const { isForeignDaemon } = await import('./daemon-takeover.js');
|
|
154
232
|
const info = readDaemonInfo();
|
|
155
233
|
const targetPort = info?.httpPort ?? info?.port ?? findDaemonPort() ?? port;
|
|
234
|
+
// The registry resolves to this user's own daemon, but the `-p` fallback
|
|
235
|
+
// resolves to whatever is on that port — which on a shared host is somebody
|
|
236
|
+
// else's daemon, and `/shutdown` is trusted purely for being local.
|
|
237
|
+
if (isForeignDaemon(await probeDaemonHealth(targetPort))) {
|
|
238
|
+
log(`Port ${targetPort} is held by another user's daemon — refusing to stop it.`);
|
|
239
|
+
log(`You have no daemon of your own running.`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
156
242
|
try {
|
|
157
243
|
await fetch(`http://127.0.0.1:${targetPort}/shutdown`, {
|
|
158
244
|
method: 'POST',
|
|
@@ -396,12 +482,15 @@ program
|
|
|
396
482
|
daemonHost: opts.daemonHost,
|
|
397
483
|
daemonToken: opts.daemonToken,
|
|
398
484
|
weight: opts.weight,
|
|
399
|
-
modules: opts.local ?
|
|
400
|
-
|
|
485
|
+
modules: opts.local ? allModulesOff() : {
|
|
486
|
+
// Off by default, ADB back on: a session bridge is an internal hook/PTY
|
|
487
|
+
// process. Only the daemon hub advertises itself or drives devices —
|
|
488
|
+
// and spelling that as "all off, then add back" is what stops the next
|
|
489
|
+
// module from arriving as an implicit 'auto' here (idotmatrix did, and
|
|
490
|
+
// every session spawned a second Python BLE client alongside the
|
|
491
|
+
// daemon's).
|
|
492
|
+
...allModulesOff(),
|
|
401
493
|
adb: opts.adb !== false ? 'auto' : false,
|
|
402
|
-
serial: false, // daemon-only — session bridges never talk to ESP32
|
|
403
|
-
pixoo: false, // daemon-only — session bridges never talk to Pixoo
|
|
404
|
-
timebox: false, // daemon-only — session bridges never talk to Timebox
|
|
405
494
|
},
|
|
406
495
|
});
|
|
407
496
|
});
|
|
@@ -458,12 +547,11 @@ program
|
|
|
458
547
|
daemonToken: opts.daemonToken,
|
|
459
548
|
weight: opts.weight,
|
|
460
549
|
codexHooksExpected: opts.codexHooks !== false,
|
|
461
|
-
modules: opts.local ?
|
|
462
|
-
|
|
550
|
+
modules: opts.local ? allModulesOff() : {
|
|
551
|
+
// See the claude action: all off, ADB back on. Session bridges are
|
|
552
|
+
// internal hook/PTY processes; only the daemon hub drives devices.
|
|
553
|
+
...allModulesOff(),
|
|
463
554
|
adb: opts.adb !== false ? 'auto' : false,
|
|
464
|
-
serial: false, // daemon-only
|
|
465
|
-
pixoo: false, // daemon-only
|
|
466
|
-
timebox: false, // daemon-only
|
|
467
555
|
},
|
|
468
556
|
});
|
|
469
557
|
});
|
|
@@ -518,12 +606,10 @@ program
|
|
|
518
606
|
daemonHost: opts.daemonHost,
|
|
519
607
|
daemonToken: opts.daemonToken,
|
|
520
608
|
weight: opts.weight,
|
|
521
|
-
modules: opts.local ?
|
|
522
|
-
|
|
609
|
+
modules: opts.local ? allModulesOff() : {
|
|
610
|
+
// See the claude action: all off, ADB back on.
|
|
611
|
+
...allModulesOff(),
|
|
523
612
|
adb: opts.adb !== false ? 'auto' : false,
|
|
524
|
-
serial: false,
|
|
525
|
-
pixoo: false,
|
|
526
|
-
timebox: false,
|
|
527
613
|
},
|
|
528
614
|
});
|
|
529
615
|
});
|
|
@@ -553,7 +639,7 @@ program
|
|
|
553
639
|
daemonHost: opts.daemonHost,
|
|
554
640
|
daemonToken: opts.daemonToken,
|
|
555
641
|
weight: opts.weight,
|
|
556
|
-
modules: opts.local ?
|
|
642
|
+
modules: opts.local ? allModulesOff() : undefined,
|
|
557
643
|
});
|
|
558
644
|
});
|
|
559
645
|
// The worktree-compare feature ("cockpit") was extracted into its own
|
|
@@ -598,10 +684,19 @@ async function openDaemonLogs(logDir) {
|
|
|
598
684
|
daemon
|
|
599
685
|
.command('start')
|
|
600
686
|
.description('Start monitoring daemon (WS + mDNS + Gateway proxy)')
|
|
601
|
-
|
|
687
|
+
// No default value on purpose: an absent `-p` must be distinguishable from a
|
|
688
|
+
// typed one. The port then resolves through AGENTDECK_DAEMON_PORT and the
|
|
689
|
+
// persisted `daemonPort` (see `agentdeck daemon port`), and the "was this
|
|
690
|
+
// port asked for?" test below has something real to test. With commander's
|
|
691
|
+
// old default of "9120" that test was always true, so the branch meant to
|
|
692
|
+
// start on a fallback port could never be reached.
|
|
693
|
+
.option('-p, --port <port>', `Server port (default: the persisted daemonPort, else ${BRIDGE_WS_PORT})`)
|
|
602
694
|
.option('-d, --debug', 'Enable debug logging')
|
|
603
695
|
.option('-f, --foreground', 'Run in foreground (default: background fork)')
|
|
604
696
|
.option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
|
|
697
|
+
.option('--local', 'Disable all device modules (no mDNS, UDP beacon, LAN sweep, BLE, ADB or serial); still binds all interfaces for paired companion apps')
|
|
698
|
+
.option('--loopback', 'Bind 127.0.0.1 only and emit nothing onto the LAN (no mDNS, UDP beacon, LAN sweep or BLE; USB serial and ADB reverse stay on). Same as AGENTDECK_LOOPBACK_ONLY=1')
|
|
699
|
+
.option('--port-window <range>', 'Work in a non-default daemon port window, e.g. 9200-9209. Only for running a throwaway daemon beside the real one: the singleton guard sweeps this range, and a daemon outside the documented 9120-9139 window is invisible to clients that scan it. Same as AGENTDECK_PORT_WINDOW')
|
|
605
700
|
.action(async (opts) => {
|
|
606
701
|
const { findExistingDaemon, probeDaemonHealth, readDaemonInfo, removeDaemonInfo, removeDaemonSession, requestDaemonStandDown, requestDaemonShutdown, waitForDaemonExit, waitForPortBindable } = await import('./session-registry.js');
|
|
607
702
|
const { adoptPeerToken } = await import('./auth.js');
|
|
@@ -613,79 +708,53 @@ daemon
|
|
|
613
708
|
// CLI daemon can take over with the full feature set, then wait for the port
|
|
614
709
|
// to clear before binding. A real Node daemon on the port still means
|
|
615
710
|
// "already running".
|
|
616
|
-
|
|
711
|
+
// Set before anything reads the window: the singleton sweep, the port
|
|
712
|
+
// allocator and the daemon itself all resolve it per call, and this action
|
|
713
|
+
// is the first place the flag is known.
|
|
714
|
+
if (opts.portWindow)
|
|
715
|
+
process.env.AGENTDECK_PORT_WINDOW = String(opts.portWindow);
|
|
716
|
+
const portWindow = daemonPortWindow();
|
|
717
|
+
if (!isDefaultDaemonPortWindow(portWindow)) {
|
|
718
|
+
const [lo, hi] = portWindow;
|
|
719
|
+
const [dlo, dhi] = DEFAULT_DAEMON_PORT_WINDOW;
|
|
720
|
+
log(`Port window ${lo}-${hi} (default ${dlo}-${dhi}).`);
|
|
721
|
+
log(`Note: clients discover daemons by scanning ${dlo}-${dhi}, so a daemon outside that range `
|
|
722
|
+
+ `is reachable only by explicit host/port. Use this for a throwaway daemon, not the real one.`);
|
|
723
|
+
if (opts.portWindow && String(opts.portWindow).trim() && lo === dlo && hi === dhi) {
|
|
724
|
+
log(`(--port-window ${opts.portWindow} was not parseable — falling back to the default window.)`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
else if (opts.portWindow) {
|
|
728
|
+
log(`--port-window ${opts.portWindow} was not parseable — using the default window.`);
|
|
729
|
+
}
|
|
730
|
+
// The preferred port — what this daemon intends to serve. Everything below
|
|
731
|
+
// (the stand-down negotiation, the singleton guard, the bind) aims at it.
|
|
732
|
+
const { resolveDaemonPort, describeDaemonPortSource, DAEMON_PORT_MIN, DAEMON_PORT_MAX } = await import('./daemon-port.js');
|
|
733
|
+
const preferredPort = resolveDaemonPort({ flag: opts.port });
|
|
734
|
+
if (opts.port && preferredPort.source !== 'flag') {
|
|
735
|
+
log(`--port ${opts.port} is not a valid port (${DAEMON_PORT_MIN}-${DAEMON_PORT_MAX}) — using ${preferredPort.port} instead.`);
|
|
736
|
+
}
|
|
737
|
+
if (preferredPort.source === 'settings' || preferredPort.source === 'env') {
|
|
738
|
+
log(`Preferred port ${preferredPort.port} (${describeDaemonPortSource(preferredPort.source)}).`);
|
|
739
|
+
}
|
|
740
|
+
const targetPort = preferredPort.port;
|
|
741
|
+
// Adopting a credential from the incumbent and asking it to stand down are
|
|
742
|
+
// both same-user-only operations, and the whole decision — including who
|
|
743
|
+
// owns that process — lives in `negotiateIncumbentDaemon` so it can be
|
|
744
|
+
// driven in tests against a foreign-owned incumbent.
|
|
745
|
+
const { negotiateIncumbentDaemon } = await import('./daemon-takeover.js');
|
|
617
746
|
const incumbent = await probeDaemonHealth(targetPort);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
// Prefer /stand-down (clean demote: the app stays running as a client).
|
|
630
|
-
// Fall back to /shutdown for older app builds that predate the endpoint.
|
|
631
|
-
let acked = await requestDaemonStandDown(targetPort);
|
|
632
|
-
if (!acked) {
|
|
633
|
-
await requestDaemonShutdown(targetPort);
|
|
634
|
-
acked = true; // shutdown is best-effort (no ack body); rely on the exit wait
|
|
635
|
-
}
|
|
636
|
-
// Two conditions, not one. `waitForDaemonExit` proves the app stopped
|
|
637
|
-
// answering; `waitForPortBindable` proves the socket is actually gone.
|
|
638
|
-
// The app releases the port with NWListener.cancel(), which returns
|
|
639
|
-
// before the teardown completes — binding on the first signal alone is
|
|
640
|
-
// what silently demoted this daemon to the 9121 fallback and left the
|
|
641
|
-
// canonical port ownerless.
|
|
642
|
-
if (acked
|
|
643
|
-
&& await waitForDaemonExit(targetPort, 12000)
|
|
644
|
-
// 30s, and that is not padding. Cancelling the app's NWListener does
|
|
645
|
-
// not free the port: macOS keeps a NECP reservation on it for ~14s
|
|
646
|
-
// afterwards, during which `lsof` shows no sockets at all and bind()
|
|
647
|
-
// still returns EADDRINUSE (measured 2026-08-06 — bindable at ~17s).
|
|
648
|
-
// Anything shorter gives up while the kernel, not the app, is holding
|
|
649
|
-
// the port. The app's takeover-yield window is longer still (45s), so
|
|
650
|
-
// it stays off the port until well after this wait has claimed it.
|
|
651
|
-
&& await waitForPortBindable(targetPort, 30000)) {
|
|
652
|
-
log(`App daemon yielded port ${targetPort}. Starting CLI daemon…`);
|
|
653
|
-
// fall through — port is clear, proceed to bind below
|
|
654
|
-
}
|
|
655
|
-
else {
|
|
656
|
-
// The app yielded but the PORT did not come back, and that is often
|
|
657
|
-
// not the app's doing: a WiFi device that has gone to sleep with bytes
|
|
658
|
-
// still queued to it leaves its socket in LAST_ACK on this port, and
|
|
659
|
-
// TCP holds that until it exhausts retransmits — minutes, sometimes.
|
|
660
|
-
// Network.framework exposes no SO_LINGER, so the app cannot RST out of
|
|
661
|
-
// it (measured 2026-08-06: two sleeping ESP32 boards, 11901 and 4
|
|
662
|
-
// bytes queued, were the only things left on 9120).
|
|
663
|
-
//
|
|
664
|
-
// Carry on rather than exiting. The daemon binds a fallback port and
|
|
665
|
-
// writes it to daemon.json, which is what every client resolves
|
|
666
|
-
// through and what the app itself re-reads when its yield window ends
|
|
667
|
-
// — so the deck keeps working. What must NOT happen is this being
|
|
668
|
-
// silent, which is how the canonical port ended up owned by nobody
|
|
669
|
-
// with the CLI reporting success.
|
|
670
|
-
log(`The AgentDeck app yielded port ${targetPort} but the port has not been released `
|
|
671
|
-
+ `(usually a sleeping WiFi device holding a half-closed socket on it).`);
|
|
672
|
-
if (opts.port) {
|
|
673
|
-
// The port was ASKED for, not defaulted to. Quietly binding a
|
|
674
|
-
// different one would answer a question the user did not ask;
|
|
675
|
-
// an explicit choice fails loudly instead.
|
|
676
|
-
log(`Quit the AgentDeck app and retry, or choose another port.`);
|
|
677
|
-
process.exit(1);
|
|
678
|
-
}
|
|
679
|
-
log(`Starting on a fallback port instead — clients resolve it from daemon.json. `
|
|
680
|
-
+ `For the canonical port, quit the AgentDeck app and retry, or pass -p.`);
|
|
681
|
-
// fall through to normal port selection
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
log(`Daemon already running on port ${targetPort}. Use 'agentdeck daemon stop' first.`);
|
|
686
|
-
process.exit(0);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
747
|
+
const outcome = await negotiateIncumbentDaemon({ port: targetPort, incumbent, portWasExplicit: !!opts.port }, {
|
|
748
|
+
adoptToken: adoptPeerToken,
|
|
749
|
+
standDown: requestDaemonStandDown,
|
|
750
|
+
shutdown: requestDaemonShutdown,
|
|
751
|
+
waitForExit: waitForDaemonExit,
|
|
752
|
+
waitForBindable: waitForPortBindable,
|
|
753
|
+
});
|
|
754
|
+
if (outcome === 'already-running')
|
|
755
|
+
process.exit(0);
|
|
756
|
+
if (outcome === 'refuse')
|
|
757
|
+
process.exit(1);
|
|
689
758
|
const daemonInfo = readDaemonInfo();
|
|
690
759
|
if (daemonInfo) {
|
|
691
760
|
const probePort = daemonInfo.httpPort ?? daemonInfo.port;
|
|
@@ -712,12 +781,26 @@ daemon
|
|
|
712
781
|
const logDir = join(homedir(), '.agentdeck');
|
|
713
782
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
714
783
|
const args = [scriptPath, 'daemon', 'start', '--foreground'];
|
|
715
|
-
|
|
716
|
-
|
|
784
|
+
// Forward `-p` only when the user typed it. For every other source the
|
|
785
|
+
// child re-resolves from the same inherited environment and the same
|
|
786
|
+
// settings.json, so it lands on the same port — and it keeps its own
|
|
787
|
+
// provenance, which a forwarded `-p` would flatten into "asked for".
|
|
788
|
+
if (preferredPort.source === 'flag')
|
|
789
|
+
args.push('-p', String(preferredPort.port));
|
|
717
790
|
if (opts.debug)
|
|
718
791
|
args.push('-d');
|
|
719
792
|
if (opts.wakeWord)
|
|
720
793
|
args.push('--wake-word');
|
|
794
|
+
// The posture flags must survive the fork — the forked process IS the
|
|
795
|
+
// daemon, and a posture that only applies to the parent is no posture.
|
|
796
|
+
if (opts.local)
|
|
797
|
+
args.push('--local');
|
|
798
|
+
if (opts.loopback)
|
|
799
|
+
args.push('--loopback');
|
|
800
|
+
// Same reason as the posture flags: the forked process IS the daemon, and
|
|
801
|
+
// a window that only applies to the parent moves nothing.
|
|
802
|
+
if (opts.portWindow)
|
|
803
|
+
args.push('--port-window', String(opts.portWindow));
|
|
721
804
|
const [out, err] = await openDaemonLogs(logDir);
|
|
722
805
|
const child = spawn(process.execPath, args, {
|
|
723
806
|
detached: true,
|
|
@@ -730,9 +813,12 @@ daemon
|
|
|
730
813
|
}
|
|
731
814
|
const { startDaemon } = await import('./daemon-server.js');
|
|
732
815
|
await startDaemon({
|
|
733
|
-
port:
|
|
816
|
+
port: preferredPort.port,
|
|
817
|
+
portSource: preferredPort.source,
|
|
734
818
|
debug: opts.debug,
|
|
735
819
|
wakeWord: !!opts.wakeWord,
|
|
820
|
+
local: !!opts.local,
|
|
821
|
+
loopback: !!opts.loopback,
|
|
736
822
|
});
|
|
737
823
|
});
|
|
738
824
|
daemon
|
|
@@ -745,18 +831,57 @@ daemon
|
|
|
745
831
|
daemon
|
|
746
832
|
.command('restart')
|
|
747
833
|
.description('Stop and restart the daemon')
|
|
748
|
-
.option('-p, --port <port>', 'Server port
|
|
834
|
+
.option('-p, --port <port>', 'Server port to restart on (default: the persisted daemonPort)')
|
|
749
835
|
.option('-d, --debug', 'Enable debug logging')
|
|
836
|
+
.option('--local', 'Disable all device modules on the restarted daemon')
|
|
837
|
+
.option('--loopback', 'Restart with a loopback-only posture (see `daemon start --loopback`)')
|
|
750
838
|
.action(async (opts) => {
|
|
751
|
-
|
|
839
|
+
// Two different ports, and conflating them is what made this command lose
|
|
840
|
+
// the posture. The daemon must be PROBED and STOPPED where it actually is
|
|
841
|
+
// (daemon.json — which is 9121+ whenever it once fell back), but RESTARTED
|
|
842
|
+
// on the port it prefers. Probing 9120 blindly found nothing whenever the
|
|
843
|
+
// daemon had fallen back, and "no posture found" reads exactly like "open
|
|
844
|
+
// posture" — a silent enterprise downgrade, which is the one thing this
|
|
845
|
+
// inheritance exists to prevent.
|
|
846
|
+
const { probeDaemonHealth: probeHealth, readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
847
|
+
const { resolveDaemonPort } = await import('./daemon-port.js');
|
|
848
|
+
const preferredPort = resolveDaemonPort({ flag: opts.port });
|
|
849
|
+
const info = readDaemonInfo();
|
|
850
|
+
const runningPort = info?.httpPort ?? info?.port ?? findDaemonPort() ?? preferredPort.port;
|
|
851
|
+
// Read the running daemon's posture BEFORE stopping it. A restart that
|
|
852
|
+
// defaults to "advertise everything" would silently undo an enterprise
|
|
853
|
+
// install — the LaunchAgent / Scheduled Task / systemd unit carries the
|
|
854
|
+
// posture in its argv, and this command does not read those three files.
|
|
855
|
+
// Explicit flags still win; inheritance only fills in what wasn't asked for.
|
|
856
|
+
const running = await probeHealth(runningPort);
|
|
857
|
+
const inheritedLocal = running?.posture?.noDeviceModules === true;
|
|
858
|
+
const inheritedLoopback = running?.posture?.loopbackOnly === true;
|
|
859
|
+
const useLocal = !!opts.local || inheritedLocal;
|
|
860
|
+
const useLoopback = !!opts.loopback || inheritedLoopback;
|
|
861
|
+
if (running && runningPort !== preferredPort.port) {
|
|
862
|
+
log(`Daemon is on port ${runningPort} but prefers ${preferredPort.port} — restarting there.`);
|
|
863
|
+
}
|
|
864
|
+
await stopDaemon(runningPort);
|
|
752
865
|
// Wait for port release + session cleanup
|
|
753
866
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
754
867
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
755
868
|
const args = [scriptPath, 'daemon', 'start', '--foreground'];
|
|
756
|
-
|
|
757
|
-
|
|
869
|
+
// Same rule as `daemon start`: forward `-p` only when it was typed, so the
|
|
870
|
+
// child keeps the real provenance of its port.
|
|
871
|
+
if (preferredPort.source === 'flag')
|
|
872
|
+
args.push('-p', String(preferredPort.port));
|
|
758
873
|
if (opts.debug)
|
|
759
874
|
args.push('-d');
|
|
875
|
+
if (useLocal)
|
|
876
|
+
args.push('--local');
|
|
877
|
+
if (useLoopback)
|
|
878
|
+
args.push('--loopback');
|
|
879
|
+
if ((inheritedLocal && !opts.local) || (inheritedLoopback && !opts.loopback)) {
|
|
880
|
+
log(`Carrying over the running daemon's posture (${[
|
|
881
|
+
inheritedLoopback ? 'loopback-only' : null,
|
|
882
|
+
inheritedLocal ? 'no device modules' : null,
|
|
883
|
+
].filter(Boolean).join(', ')}).`);
|
|
884
|
+
}
|
|
760
885
|
const [rOut, rErr] = await openDaemonLogs(join(homedir(), '.agentdeck'));
|
|
761
886
|
const child = spawn(process.execPath, args, {
|
|
762
887
|
detached: true,
|
|
@@ -777,11 +902,28 @@ daemon
|
|
|
777
902
|
const info = readDaemonInfo();
|
|
778
903
|
const sessions = listActive();
|
|
779
904
|
const d = sessions.find(s => s.agentType === 'daemon');
|
|
780
|
-
|
|
905
|
+
// Two ways to arrive at a port, and only one of them identifies OUR daemon.
|
|
906
|
+
// `registryPort` comes from this install's own daemon.json / sessions.json;
|
|
907
|
+
// the `?? port` tail is a blind probe of the default, which on a machine
|
|
908
|
+
// running a second AgentDeck answers with *its* daemon. Anything that
|
|
909
|
+
// claims something about "this daemon" may only use the registry-resolved
|
|
910
|
+
// port — a note attached to a stranger's daemon is worse than no note.
|
|
911
|
+
const registryPort = info?.httpPort ?? info?.port ?? d?.port ?? null;
|
|
912
|
+
const targetPort = registryPort ?? port;
|
|
781
913
|
try {
|
|
782
914
|
const res = await fetch(`http://127.0.0.1:${targetPort}/health`);
|
|
783
915
|
const data = await res.json();
|
|
784
916
|
log(`Daemon status (port ${targetPort}): ${JSON.stringify(data, null, 2)}`);
|
|
917
|
+
// A daemon on a fallback port keeps working — clients resolve it from
|
|
918
|
+
// daemon.json — so nothing else ever says it happened. Without this line
|
|
919
|
+
// the only symptom is that the canonical port is owned by nobody.
|
|
920
|
+
const { resolveDaemonPort, describeDaemonPortSource } = await import('./daemon-port.js');
|
|
921
|
+
const preferred = resolveDaemonPort();
|
|
922
|
+
if (registryPort !== null && preferred.port !== registryPort) {
|
|
923
|
+
log(`Note: this daemon prefers port ${preferred.port} (${describeDaemonPortSource(preferred.source)}) `
|
|
924
|
+
+ `but is serving ${registryPort} — it fell back at startup. `
|
|
925
|
+
+ `Run 'agentdeck daemon restart' to aim at ${preferred.port} again.`);
|
|
926
|
+
}
|
|
785
927
|
}
|
|
786
928
|
catch {
|
|
787
929
|
if (info)
|
|
@@ -792,13 +934,70 @@ daemon
|
|
|
792
934
|
process.exit(1);
|
|
793
935
|
}
|
|
794
936
|
});
|
|
937
|
+
daemon
|
|
938
|
+
.command('port')
|
|
939
|
+
.argument('[port]', 'Port to persist as this machine\'s preferred daemon port')
|
|
940
|
+
.description('Show or set the persisted daemon port (settings.json daemonPort)')
|
|
941
|
+
.option('--clear', 'Forget the persisted port and go back to the built-in default')
|
|
942
|
+
.action(async (portArg, opts) => {
|
|
943
|
+
const { resolveDaemonPort, describeDaemonPortSource, parseDaemonPort, preferredDaemonPortFrom, DAEMON_PORT_SETTING_KEY, DAEMON_PORT_MIN, DAEMON_PORT_MAX, } = await import('./daemon-port.js');
|
|
944
|
+
const { loadDaemonSettings, updateDaemonSetting, ownSettingsPath } = await import('./daemon-settings.js');
|
|
945
|
+
const { readDaemonInfo } = await import('./session-registry.js');
|
|
946
|
+
if (opts.clear) {
|
|
947
|
+
updateDaemonSetting(DAEMON_PORT_SETTING_KEY, undefined);
|
|
948
|
+
log(`Cleared the persisted daemon port in ${ownSettingsPath()}.`);
|
|
949
|
+
}
|
|
950
|
+
else if (portArg !== undefined) {
|
|
951
|
+
const parsed = parseDaemonPort(portArg);
|
|
952
|
+
if (parsed === null) {
|
|
953
|
+
// Strict here, lenient in the reader: a typo must fail where the user
|
|
954
|
+
// can see it, not silently revert to 9120 six weeks later.
|
|
955
|
+
log(`"${portArg}" is not a valid port. Give an integer between ${DAEMON_PORT_MIN} and ${DAEMON_PORT_MAX}.`);
|
|
956
|
+
process.exit(1);
|
|
957
|
+
}
|
|
958
|
+
updateDaemonSetting(DAEMON_PORT_SETTING_KEY, parsed);
|
|
959
|
+
log(`Persisted daemon port ${parsed} in ${ownSettingsPath()}.`);
|
|
960
|
+
log(`It takes effect on the next 'agentdeck daemon restart'.`);
|
|
961
|
+
// A preferred port outside the sweep window is a quiet split-brain: the
|
|
962
|
+
// singleton guard scans the window, so a second daemon started later
|
|
963
|
+
// never sees this one and both run. Same warning `--port-window` prints,
|
|
964
|
+
// for the same reason — the window is a discovery contract, not a range
|
|
965
|
+
// of convenience.
|
|
966
|
+
const [lo, hi] = daemonPortWindow();
|
|
967
|
+
if (parsed < lo || parsed > hi) {
|
|
968
|
+
log(`Warning: ${parsed} is outside the daemon port window ${lo}-${hi}. Clients that discover `
|
|
969
|
+
+ `by scanning that window will not find this daemon, and the singleton guard will not see `
|
|
970
|
+
+ `it either — a second daemon could start alongside it. Move the window too `
|
|
971
|
+
+ `(AGENTDECK_PORT_WINDOW), or pick a port inside it.`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const resolved = resolveDaemonPort();
|
|
975
|
+
const stored = preferredDaemonPortFrom(loadDaemonSettings());
|
|
976
|
+
log(`Preferred port: ${resolved.port} (${describeDaemonPortSource(resolved.source)})`);
|
|
977
|
+
log(`Persisted value: ${stored ?? '(none)'}`);
|
|
978
|
+
const info = readDaemonInfo();
|
|
979
|
+
if (info) {
|
|
980
|
+
log(`Running daemon: port ${info.port} (PID ${info.pid})`
|
|
981
|
+
+ (info.port === resolved.port ? '' : ' — on a fallback port, not its preferred one'));
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
log('Running daemon: none found');
|
|
985
|
+
}
|
|
986
|
+
});
|
|
795
987
|
daemon
|
|
796
988
|
.command('install')
|
|
797
989
|
.description('Install daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows, systemd --user unit on Linux)')
|
|
798
|
-
.
|
|
990
|
+
.option('--enterprise', 'Install with a loopback-only posture: bind 127.0.0.1, no mDNS, no UDP beacon, no LAN sweep, no BLE, no ADB')
|
|
991
|
+
.option('--local', 'Install with all device modules disabled')
|
|
992
|
+
.option('--loopback', 'Alias for --enterprise')
|
|
993
|
+
.action(async (opts) => {
|
|
994
|
+
const postureArgs = daemonPostureArgs(opts);
|
|
995
|
+
if (postureArgs.length > 0) {
|
|
996
|
+
log(`Autostart posture: ${postureArgs.join(' ')} (baked into the autostart unit's arguments).`);
|
|
997
|
+
}
|
|
799
998
|
if (process.platform === 'win32') {
|
|
800
999
|
try {
|
|
801
|
-
installWindowsTask();
|
|
1000
|
+
installWindowsTask(postureArgs);
|
|
802
1001
|
log(`Scheduled task '${TASK_NAME}' registered. Daemon will auto-start on logon.`);
|
|
803
1002
|
}
|
|
804
1003
|
catch (e) {
|
|
@@ -820,6 +1019,7 @@ daemon
|
|
|
820
1019
|
log('Task registered; immediate start failed — it will start on next logon.');
|
|
821
1020
|
}
|
|
822
1021
|
await refreshClaudeHooks();
|
|
1022
|
+
await refreshKiroHooks();
|
|
823
1023
|
// Install Codex lifecycle hooks for parity with the macOS install path.
|
|
824
1024
|
try {
|
|
825
1025
|
const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
@@ -857,7 +1057,7 @@ daemon
|
|
|
857
1057
|
}
|
|
858
1058
|
else {
|
|
859
1059
|
try {
|
|
860
|
-
installUnit();
|
|
1060
|
+
installUnit(postureArgs);
|
|
861
1061
|
startUnit();
|
|
862
1062
|
log(`systemd user unit '${SERVICE_NAME}' installed and started.`);
|
|
863
1063
|
log(`Unit file: ${getUnitPath()}`);
|
|
@@ -879,6 +1079,7 @@ daemon
|
|
|
879
1079
|
}
|
|
880
1080
|
}
|
|
881
1081
|
await refreshClaudeHooks();
|
|
1082
|
+
await refreshKiroHooks();
|
|
882
1083
|
// Install Codex + OpenCode hooks for parity with the macOS/Windows paths.
|
|
883
1084
|
try {
|
|
884
1085
|
const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
@@ -910,7 +1111,7 @@ daemon
|
|
|
910
1111
|
log('LaunchAgent is macOS-only');
|
|
911
1112
|
process.exit(1);
|
|
912
1113
|
}
|
|
913
|
-
const plist = buildPlist();
|
|
1114
|
+
const plist = buildPlist(postureArgs);
|
|
914
1115
|
writeFileSync(PLIST_PATH, plist, 'utf-8');
|
|
915
1116
|
log(`Wrote ${PLIST_PATH}`);
|
|
916
1117
|
try {
|
|
@@ -920,6 +1121,7 @@ daemon
|
|
|
920
1121
|
execSync(`launchctl load "${PLIST_PATH}"`);
|
|
921
1122
|
log('LaunchAgent loaded. Daemon will auto-start on login.');
|
|
922
1123
|
await refreshClaudeHooks();
|
|
1124
|
+
await refreshKiroHooks();
|
|
923
1125
|
// Install Codex lifecycle hooks parallel to the LaunchAgent install
|
|
924
1126
|
// so the daemon hub gets codex_* events as soon as Codex CLI runs.
|
|
925
1127
|
try {
|
|
@@ -1364,6 +1566,7 @@ program
|
|
|
1364
1566
|
}
|
|
1365
1567
|
}
|
|
1366
1568
|
getOrCreateToken();
|
|
1569
|
+
await warnIfLoopbackPosture(port);
|
|
1367
1570
|
const url = getWsUrl(port);
|
|
1368
1571
|
log(`\nPairing URL:\n ${url}\n`);
|
|
1369
1572
|
try {
|
|
@@ -1395,6 +1598,9 @@ program
|
|
|
1395
1598
|
? opts.adopt
|
|
1396
1599
|
: (typeof opts.adopt === 'string' ? [opts.adopt] : []);
|
|
1397
1600
|
const daemon = `http://127.0.0.1:${port}`;
|
|
1601
|
+
// A pairing window against a loopback-only daemon can never be redeemed:
|
|
1602
|
+
// the peer it is meant for cannot open a socket to this host at all.
|
|
1603
|
+
await warnIfLoopbackPosture(port);
|
|
1398
1604
|
let opened;
|
|
1399
1605
|
try {
|
|
1400
1606
|
const res = await fetch(`${daemon}/pair/open`, {
|
|
@@ -1521,12 +1727,29 @@ program
|
|
|
1521
1727
|
}
|
|
1522
1728
|
});
|
|
1523
1729
|
program
|
|
1524
|
-
.command('diag')
|
|
1525
|
-
.description('Generate diagnostic dump')
|
|
1730
|
+
.command('diag [target]')
|
|
1731
|
+
.description('Generate a diagnostic dump, or a privacy-safe agent diagnostic')
|
|
1526
1732
|
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
1527
1733
|
.option('-a, --analyze', 'Run AI analysis on the dump')
|
|
1528
1734
|
.option('-t, --tail <lines>', 'Number of journal entries', '200')
|
|
1529
|
-
.
|
|
1735
|
+
.option('--json', 'Print target diagnostics as machine-readable JSON')
|
|
1736
|
+
.action(async (target, opts) => {
|
|
1737
|
+
if (target) {
|
|
1738
|
+
if (target !== 'kiro') {
|
|
1739
|
+
log(`Unknown diagnostic target: ${target}. Supported target: kiro`);
|
|
1740
|
+
process.exitCode = 1;
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (opts.analyze) {
|
|
1744
|
+
log('`--analyze` is only available for the general daemon diagnostic dump.');
|
|
1745
|
+
process.exitCode = 1;
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const { collectKiroDiagnosticReport, formatKiroDiagnosticReport } = await import('./kiro-diagnostics.js');
|
|
1749
|
+
const report = await collectKiroDiagnosticReport();
|
|
1750
|
+
process.stdout.write(`${opts.json ? JSON.stringify(report, null, 2) : formatKiroDiagnosticReport(report)}\n`);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1530
1753
|
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
1531
1754
|
const info = readDaemonInfo();
|
|
1532
1755
|
const port = info?.httpPort ?? info?.port ?? findDaemonPort() ?? parseInt(opts.port, 10);
|
|
@@ -2008,12 +2231,24 @@ timebox
|
|
|
2008
2231
|
const pixoo = program.command('pixoo').description('Manage Pixoo64 LED matrix devices');
|
|
2009
2232
|
pixoo
|
|
2010
2233
|
.command('scan')
|
|
2011
|
-
.description('Discover Pixoo devices on LAN')
|
|
2012
|
-
.
|
|
2013
|
-
|
|
2234
|
+
.description('Discover Pixoo devices on LAN (Divoom cloud lookup, then a local /24 sweep)')
|
|
2235
|
+
.option('--no-cloud', 'Skip the Divoom cloud lookup (app.divoom-gz.com) and sweep the local subnet only')
|
|
2236
|
+
.action(async (opts) => {
|
|
2237
|
+
const { getDeviceConfig } = await import('./pixoo/pixoo-client.js');
|
|
2238
|
+
const { discoverPixoo } = await import('./pixoo/pixoo-discover.js');
|
|
2014
2239
|
const { loadPixooDevices, savePixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
2015
|
-
|
|
2016
|
-
|
|
2240
|
+
// Say what this actually does before it does it. Auto-discovery is off by
|
|
2241
|
+
// default precisely because these two steps are not free on a shared
|
|
2242
|
+
// network; running them from an explicit command is fine, running them
|
|
2243
|
+
// unannounced is not.
|
|
2244
|
+
if (opts.cloud === false) {
|
|
2245
|
+
log('Scanning the local subnet for Pixoo devices (cloud lookup skipped)...');
|
|
2246
|
+
}
|
|
2247
|
+
else {
|
|
2248
|
+
log('Scanning for Pixoo devices — querying the Divoom cloud (app.divoom-gz.com), then sweeping the local /24 if needed.');
|
|
2249
|
+
log('Use --no-cloud to keep the scan on your own network.');
|
|
2250
|
+
}
|
|
2251
|
+
const found = await discoverPixoo({ cloud: opts.cloud !== false });
|
|
2017
2252
|
if (found.length === 0) {
|
|
2018
2253
|
log('No devices found.');
|
|
2019
2254
|
return;
|
|
@@ -2082,7 +2317,12 @@ pixoo
|
|
|
2082
2317
|
const { loadPixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
2083
2318
|
const devices = loadPixooDevices();
|
|
2084
2319
|
if (devices.length === 0) {
|
|
2085
|
-
|
|
2320
|
+
// Says why nothing appeared on its own. The daemon no longer sweeps the
|
|
2321
|
+
// LAN unprompted, so "I plugged it in and nothing happened" needs an
|
|
2322
|
+
// answer here — this is where a user looks when the panel is missing.
|
|
2323
|
+
log('No Pixoo devices configured. Run `agentdeck pixoo scan` (add --no-cloud to keep the lookup on your own network).');
|
|
2324
|
+
log('Auto-discovery is off by default: the daemon does not sweep the LAN or call the Divoom cloud on its own.');
|
|
2325
|
+
log('Set "pixooAutoDiscover": true in ~/.agentdeck/settings.json to opt back in.');
|
|
2086
2326
|
return;
|
|
2087
2327
|
}
|
|
2088
2328
|
log(`${devices.length} device(s):`);
|
|
@@ -2473,6 +2713,49 @@ apme
|
|
|
2473
2713
|
log(` ${c.modelId.slice(0, 26).padEnd(26)} ${c.agentType.padEnd(14)} ${String(c.runs).padEnd(6)} ${score.padEnd(8)} ${tests.padEnd(8)} ${cost.padEnd(10)} ${cpq}`);
|
|
2474
2714
|
}
|
|
2475
2715
|
});
|
|
2716
|
+
apme
|
|
2717
|
+
.command('stop-health')
|
|
2718
|
+
.description('Stop-hook delivery rate — how turns actually got closed')
|
|
2719
|
+
.option('--since <window>', 'Lookback window, e.g. 6h / 3d / 2w (default 7d)', '7d')
|
|
2720
|
+
.option('--agent <type>', 'Restrict to one agent type (claude-code, codex-cli, …)')
|
|
2721
|
+
.action(async (opts) => {
|
|
2722
|
+
const { initApme } = await import('./apme/index.js');
|
|
2723
|
+
const apme = await initApme();
|
|
2724
|
+
if (!apme) {
|
|
2725
|
+
log('APME not available');
|
|
2726
|
+
process.exit(1);
|
|
2727
|
+
}
|
|
2728
|
+
const windowMs = parseLookbackWindow(opts.since);
|
|
2729
|
+
if (windowMs == null) {
|
|
2730
|
+
log(`Unrecognized --since value: ${opts.since}`);
|
|
2731
|
+
process.exit(1);
|
|
2732
|
+
}
|
|
2733
|
+
const sinceMs = Date.now() - windowMs;
|
|
2734
|
+
const rows = apme.store.stopDelivery({ sinceMs, agentType: opts.agent });
|
|
2735
|
+
if (rows.length === 0) {
|
|
2736
|
+
log(`No turns started in the last ${opts.since}.`);
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
log(`\n Turns started in the last ${opts.since} — how each one's end was learned`);
|
|
2740
|
+
log(` ${'Agent'.padEnd(14)} ${'Turns'.padEnd(7)} ${'Stop'.padEnd(7)} ${'Synth'.padEnd(7)} ${'NoStop'.padEnd(7)} ${'Esc'.padEnd(6)} ${'Abort'.padEnd(6)} ${'Folded'.padEnd(7)} ${'SessEnd'.padEnd(8)} ${'Open'.padEnd(6)} ${'?'.padEnd(6)} Stop loss`);
|
|
2741
|
+
log(` ${'─'.repeat(14)} ${'─'.repeat(7)} ${'─'.repeat(7)} ${'─'.repeat(7)} ${'─'.repeat(7)} ${'─'.repeat(6)} ${'─'.repeat(6)} ${'─'.repeat(7)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(6)} ${'─'.repeat(9)}`);
|
|
2742
|
+
for (const r of rows) {
|
|
2743
|
+
// Which buckets the ratio may read is defined once, in shared — see
|
|
2744
|
+
// `stopDeliveryLoss`. Restating it here is how the instrument would come
|
|
2745
|
+
// to measure something other than what its legend claims.
|
|
2746
|
+
const { adjudicated, ratio } = stopDeliveryLoss(r);
|
|
2747
|
+
const rate = ratio != null ? `${(ratio * 100).toFixed(0)}% of ${adjudicated}` : '—';
|
|
2748
|
+
log(` ${r.agentType.slice(0, 14).padEnd(14)} ${String(r.total).padEnd(7)} ${String(r.stop).padEnd(7)} ${String(r.syntheticStop).padEnd(7)} ${String(r.nextPrompt).padEnd(7)} ${String(r.interrupted).padEnd(6)} ${String(r.aborted).padEnd(6)} ${String(r.superseded).padEnd(7)} ${String(r.sessionEnd).padEnd(8)} ${String(r.open).padEnd(6)} ${String(r.preInstrument).padEnd(6)} ${rate}`);
|
|
2749
|
+
}
|
|
2750
|
+
log('');
|
|
2751
|
+
log(' Synth = Stop hook dropped, watchdog recovered it from the transcript');
|
|
2752
|
+
log(' NoStop = Stop hook dropped and nothing recovered it (closed by the next prompt)');
|
|
2753
|
+
log(' Esc = user cancelled the turn — Claude Code owes no Stop, so not a loss');
|
|
2754
|
+
log(' Abort = usage limit / auth / API error ended the turn — no Stop is fired for those');
|
|
2755
|
+
log(' Folded = a second prompt arrived before this one ran; one turn served both');
|
|
2756
|
+
log(' ? = closed before end_source existed — signal unknown, never guessed');
|
|
2757
|
+
log('');
|
|
2758
|
+
});
|
|
2476
2759
|
apme
|
|
2477
2760
|
.command('judge')
|
|
2478
2761
|
.description('Run evaluation on unevaluated runs')
|