@agentdeck/bridge 1.0.19 → 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/codex-turn-manager.js +6 -1
- package/dist/apme/adapters/codex-turn-manager.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 +76 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
- package/dist/apme/claude-transcript-reader.js +152 -1
- 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/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +6 -1
- package/dist/bridge-core.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 +93 -0
- package/dist/claude-turn-watchdog.d.ts.map +1 -0
- package/dist/claude-turn-watchdog.js +173 -0
- package/dist/claude-turn-watchdog.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +399 -115
- package/dist/cli.js.map +1 -1
- package/dist/codex-hook-silence.d.ts +52 -0
- package/dist/codex-hook-silence.d.ts.map +1 -0
- package/dist/codex-hook-silence.js +80 -0
- package/dist/codex-hook-silence.js.map +1 -0
- 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 +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +90 -12
- 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/state-machine.d.ts +38 -1
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +112 -8
- package/dist/state-machine.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/version-check.d.ts +8 -0
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +27 -0
- package/dist/version-check.js.map +1 -1
- 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
|
});
|
|
@@ -457,12 +546,12 @@ program
|
|
|
457
546
|
daemonHost: opts.daemonHost,
|
|
458
547
|
daemonToken: opts.daemonToken,
|
|
459
548
|
weight: opts.weight,
|
|
460
|
-
|
|
461
|
-
|
|
549
|
+
codexHooksExpected: opts.codexHooks !== false,
|
|
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(),
|
|
462
554
|
adb: opts.adb !== false ? 'auto' : false,
|
|
463
|
-
serial: false, // daemon-only
|
|
464
|
-
pixoo: false, // daemon-only
|
|
465
|
-
timebox: false, // daemon-only
|
|
466
555
|
},
|
|
467
556
|
});
|
|
468
557
|
});
|
|
@@ -517,12 +606,10 @@ program
|
|
|
517
606
|
daemonHost: opts.daemonHost,
|
|
518
607
|
daemonToken: opts.daemonToken,
|
|
519
608
|
weight: opts.weight,
|
|
520
|
-
modules: opts.local ?
|
|
521
|
-
|
|
609
|
+
modules: opts.local ? allModulesOff() : {
|
|
610
|
+
// See the claude action: all off, ADB back on.
|
|
611
|
+
...allModulesOff(),
|
|
522
612
|
adb: opts.adb !== false ? 'auto' : false,
|
|
523
|
-
serial: false,
|
|
524
|
-
pixoo: false,
|
|
525
|
-
timebox: false,
|
|
526
613
|
},
|
|
527
614
|
});
|
|
528
615
|
});
|
|
@@ -552,7 +639,7 @@ program
|
|
|
552
639
|
daemonHost: opts.daemonHost,
|
|
553
640
|
daemonToken: opts.daemonToken,
|
|
554
641
|
weight: opts.weight,
|
|
555
|
-
modules: opts.local ?
|
|
642
|
+
modules: opts.local ? allModulesOff() : undefined,
|
|
556
643
|
});
|
|
557
644
|
});
|
|
558
645
|
// The worktree-compare feature ("cockpit") was extracted into its own
|
|
@@ -597,10 +684,19 @@ async function openDaemonLogs(logDir) {
|
|
|
597
684
|
daemon
|
|
598
685
|
.command('start')
|
|
599
686
|
.description('Start monitoring daemon (WS + mDNS + Gateway proxy)')
|
|
600
|
-
|
|
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})`)
|
|
601
694
|
.option('-d, --debug', 'Enable debug logging')
|
|
602
695
|
.option('-f, --foreground', 'Run in foreground (default: background fork)')
|
|
603
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')
|
|
604
700
|
.action(async (opts) => {
|
|
605
701
|
const { findExistingDaemon, probeDaemonHealth, readDaemonInfo, removeDaemonInfo, removeDaemonSession, requestDaemonStandDown, requestDaemonShutdown, waitForDaemonExit, waitForPortBindable } = await import('./session-registry.js');
|
|
606
702
|
const { adoptPeerToken } = await import('./auth.js');
|
|
@@ -612,79 +708,53 @@ daemon
|
|
|
612
708
|
// CLI daemon can take over with the full feature set, then wait for the port
|
|
613
709
|
// to clear before binding. A real Node daemon on the port still means
|
|
614
710
|
// "already running".
|
|
615
|
-
|
|
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');
|
|
616
746
|
const incumbent = await probeDaemonHealth(targetPort);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
// Prefer /stand-down (clean demote: the app stays running as a client).
|
|
629
|
-
// Fall back to /shutdown for older app builds that predate the endpoint.
|
|
630
|
-
let acked = await requestDaemonStandDown(targetPort);
|
|
631
|
-
if (!acked) {
|
|
632
|
-
await requestDaemonShutdown(targetPort);
|
|
633
|
-
acked = true; // shutdown is best-effort (no ack body); rely on the exit wait
|
|
634
|
-
}
|
|
635
|
-
// Two conditions, not one. `waitForDaemonExit` proves the app stopped
|
|
636
|
-
// answering; `waitForPortBindable` proves the socket is actually gone.
|
|
637
|
-
// The app releases the port with NWListener.cancel(), which returns
|
|
638
|
-
// before the teardown completes — binding on the first signal alone is
|
|
639
|
-
// what silently demoted this daemon to the 9121 fallback and left the
|
|
640
|
-
// canonical port ownerless.
|
|
641
|
-
if (acked
|
|
642
|
-
&& await waitForDaemonExit(targetPort, 12000)
|
|
643
|
-
// 30s, and that is not padding. Cancelling the app's NWListener does
|
|
644
|
-
// not free the port: macOS keeps a NECP reservation on it for ~14s
|
|
645
|
-
// afterwards, during which `lsof` shows no sockets at all and bind()
|
|
646
|
-
// still returns EADDRINUSE (measured 2026-08-06 — bindable at ~17s).
|
|
647
|
-
// Anything shorter gives up while the kernel, not the app, is holding
|
|
648
|
-
// the port. The app's takeover-yield window is longer still (45s), so
|
|
649
|
-
// it stays off the port until well after this wait has claimed it.
|
|
650
|
-
&& await waitForPortBindable(targetPort, 30000)) {
|
|
651
|
-
log(`App daemon yielded port ${targetPort}. Starting CLI daemon…`);
|
|
652
|
-
// fall through — port is clear, proceed to bind below
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
// The app yielded but the PORT did not come back, and that is often
|
|
656
|
-
// not the app's doing: a WiFi device that has gone to sleep with bytes
|
|
657
|
-
// still queued to it leaves its socket in LAST_ACK on this port, and
|
|
658
|
-
// TCP holds that until it exhausts retransmits — minutes, sometimes.
|
|
659
|
-
// Network.framework exposes no SO_LINGER, so the app cannot RST out of
|
|
660
|
-
// it (measured 2026-08-06: two sleeping ESP32 boards, 11901 and 4
|
|
661
|
-
// bytes queued, were the only things left on 9120).
|
|
662
|
-
//
|
|
663
|
-
// Carry on rather than exiting. The daemon binds a fallback port and
|
|
664
|
-
// writes it to daemon.json, which is what every client resolves
|
|
665
|
-
// through and what the app itself re-reads when its yield window ends
|
|
666
|
-
// — so the deck keeps working. What must NOT happen is this being
|
|
667
|
-
// silent, which is how the canonical port ended up owned by nobody
|
|
668
|
-
// with the CLI reporting success.
|
|
669
|
-
log(`The AgentDeck app yielded port ${targetPort} but the port has not been released `
|
|
670
|
-
+ `(usually a sleeping WiFi device holding a half-closed socket on it).`);
|
|
671
|
-
if (opts.port) {
|
|
672
|
-
// The port was ASKED for, not defaulted to. Quietly binding a
|
|
673
|
-
// different one would answer a question the user did not ask;
|
|
674
|
-
// an explicit choice fails loudly instead.
|
|
675
|
-
log(`Quit the AgentDeck app and retry, or choose another port.`);
|
|
676
|
-
process.exit(1);
|
|
677
|
-
}
|
|
678
|
-
log(`Starting on a fallback port instead — clients resolve it from daemon.json. `
|
|
679
|
-
+ `For the canonical port, quit the AgentDeck app and retry, or pass -p.`);
|
|
680
|
-
// fall through to normal port selection
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
log(`Daemon already running on port ${targetPort}. Use 'agentdeck daemon stop' first.`);
|
|
685
|
-
process.exit(0);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
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);
|
|
688
758
|
const daemonInfo = readDaemonInfo();
|
|
689
759
|
if (daemonInfo) {
|
|
690
760
|
const probePort = daemonInfo.httpPort ?? daemonInfo.port;
|
|
@@ -711,12 +781,26 @@ daemon
|
|
|
711
781
|
const logDir = join(homedir(), '.agentdeck');
|
|
712
782
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
713
783
|
const args = [scriptPath, 'daemon', 'start', '--foreground'];
|
|
714
|
-
|
|
715
|
-
|
|
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));
|
|
716
790
|
if (opts.debug)
|
|
717
791
|
args.push('-d');
|
|
718
792
|
if (opts.wakeWord)
|
|
719
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));
|
|
720
804
|
const [out, err] = await openDaemonLogs(logDir);
|
|
721
805
|
const child = spawn(process.execPath, args, {
|
|
722
806
|
detached: true,
|
|
@@ -729,9 +813,12 @@ daemon
|
|
|
729
813
|
}
|
|
730
814
|
const { startDaemon } = await import('./daemon-server.js');
|
|
731
815
|
await startDaemon({
|
|
732
|
-
port:
|
|
816
|
+
port: preferredPort.port,
|
|
817
|
+
portSource: preferredPort.source,
|
|
733
818
|
debug: opts.debug,
|
|
734
819
|
wakeWord: !!opts.wakeWord,
|
|
820
|
+
local: !!opts.local,
|
|
821
|
+
loopback: !!opts.loopback,
|
|
735
822
|
});
|
|
736
823
|
});
|
|
737
824
|
daemon
|
|
@@ -744,18 +831,57 @@ daemon
|
|
|
744
831
|
daemon
|
|
745
832
|
.command('restart')
|
|
746
833
|
.description('Stop and restart the daemon')
|
|
747
|
-
.option('-p, --port <port>', 'Server port
|
|
834
|
+
.option('-p, --port <port>', 'Server port to restart on (default: the persisted daemonPort)')
|
|
748
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`)')
|
|
749
838
|
.action(async (opts) => {
|
|
750
|
-
|
|
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);
|
|
751
865
|
// Wait for port release + session cleanup
|
|
752
866
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
753
867
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
754
868
|
const args = [scriptPath, 'daemon', 'start', '--foreground'];
|
|
755
|
-
|
|
756
|
-
|
|
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));
|
|
757
873
|
if (opts.debug)
|
|
758
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
|
+
}
|
|
759
885
|
const [rOut, rErr] = await openDaemonLogs(join(homedir(), '.agentdeck'));
|
|
760
886
|
const child = spawn(process.execPath, args, {
|
|
761
887
|
detached: true,
|
|
@@ -776,11 +902,28 @@ daemon
|
|
|
776
902
|
const info = readDaemonInfo();
|
|
777
903
|
const sessions = listActive();
|
|
778
904
|
const d = sessions.find(s => s.agentType === 'daemon');
|
|
779
|
-
|
|
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;
|
|
780
913
|
try {
|
|
781
914
|
const res = await fetch(`http://127.0.0.1:${targetPort}/health`);
|
|
782
915
|
const data = await res.json();
|
|
783
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
|
+
}
|
|
784
927
|
}
|
|
785
928
|
catch {
|
|
786
929
|
if (info)
|
|
@@ -791,13 +934,70 @@ daemon
|
|
|
791
934
|
process.exit(1);
|
|
792
935
|
}
|
|
793
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
|
+
});
|
|
794
987
|
daemon
|
|
795
988
|
.command('install')
|
|
796
989
|
.description('Install daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows, systemd --user unit on Linux)')
|
|
797
|
-
.
|
|
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
|
+
}
|
|
798
998
|
if (process.platform === 'win32') {
|
|
799
999
|
try {
|
|
800
|
-
installWindowsTask();
|
|
1000
|
+
installWindowsTask(postureArgs);
|
|
801
1001
|
log(`Scheduled task '${TASK_NAME}' registered. Daemon will auto-start on logon.`);
|
|
802
1002
|
}
|
|
803
1003
|
catch (e) {
|
|
@@ -819,6 +1019,7 @@ daemon
|
|
|
819
1019
|
log('Task registered; immediate start failed — it will start on next logon.');
|
|
820
1020
|
}
|
|
821
1021
|
await refreshClaudeHooks();
|
|
1022
|
+
await refreshKiroHooks();
|
|
822
1023
|
// Install Codex lifecycle hooks for parity with the macOS install path.
|
|
823
1024
|
try {
|
|
824
1025
|
const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
@@ -856,7 +1057,7 @@ daemon
|
|
|
856
1057
|
}
|
|
857
1058
|
else {
|
|
858
1059
|
try {
|
|
859
|
-
installUnit();
|
|
1060
|
+
installUnit(postureArgs);
|
|
860
1061
|
startUnit();
|
|
861
1062
|
log(`systemd user unit '${SERVICE_NAME}' installed and started.`);
|
|
862
1063
|
log(`Unit file: ${getUnitPath()}`);
|
|
@@ -878,6 +1079,7 @@ daemon
|
|
|
878
1079
|
}
|
|
879
1080
|
}
|
|
880
1081
|
await refreshClaudeHooks();
|
|
1082
|
+
await refreshKiroHooks();
|
|
881
1083
|
// Install Codex + OpenCode hooks for parity with the macOS/Windows paths.
|
|
882
1084
|
try {
|
|
883
1085
|
const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
@@ -909,7 +1111,7 @@ daemon
|
|
|
909
1111
|
log('LaunchAgent is macOS-only');
|
|
910
1112
|
process.exit(1);
|
|
911
1113
|
}
|
|
912
|
-
const plist = buildPlist();
|
|
1114
|
+
const plist = buildPlist(postureArgs);
|
|
913
1115
|
writeFileSync(PLIST_PATH, plist, 'utf-8');
|
|
914
1116
|
log(`Wrote ${PLIST_PATH}`);
|
|
915
1117
|
try {
|
|
@@ -919,6 +1121,7 @@ daemon
|
|
|
919
1121
|
execSync(`launchctl load "${PLIST_PATH}"`);
|
|
920
1122
|
log('LaunchAgent loaded. Daemon will auto-start on login.');
|
|
921
1123
|
await refreshClaudeHooks();
|
|
1124
|
+
await refreshKiroHooks();
|
|
922
1125
|
// Install Codex lifecycle hooks parallel to the LaunchAgent install
|
|
923
1126
|
// so the daemon hub gets codex_* events as soon as Codex CLI runs.
|
|
924
1127
|
try {
|
|
@@ -1363,6 +1566,7 @@ program
|
|
|
1363
1566
|
}
|
|
1364
1567
|
}
|
|
1365
1568
|
getOrCreateToken();
|
|
1569
|
+
await warnIfLoopbackPosture(port);
|
|
1366
1570
|
const url = getWsUrl(port);
|
|
1367
1571
|
log(`\nPairing URL:\n ${url}\n`);
|
|
1368
1572
|
try {
|
|
@@ -1394,6 +1598,9 @@ program
|
|
|
1394
1598
|
? opts.adopt
|
|
1395
1599
|
: (typeof opts.adopt === 'string' ? [opts.adopt] : []);
|
|
1396
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);
|
|
1397
1604
|
let opened;
|
|
1398
1605
|
try {
|
|
1399
1606
|
const res = await fetch(`${daemon}/pair/open`, {
|
|
@@ -1520,12 +1727,29 @@ program
|
|
|
1520
1727
|
}
|
|
1521
1728
|
});
|
|
1522
1729
|
program
|
|
1523
|
-
.command('diag')
|
|
1524
|
-
.description('Generate diagnostic dump')
|
|
1730
|
+
.command('diag [target]')
|
|
1731
|
+
.description('Generate a diagnostic dump, or a privacy-safe agent diagnostic')
|
|
1525
1732
|
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
1526
1733
|
.option('-a, --analyze', 'Run AI analysis on the dump')
|
|
1527
1734
|
.option('-t, --tail <lines>', 'Number of journal entries', '200')
|
|
1528
|
-
.
|
|
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
|
+
}
|
|
1529
1753
|
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
1530
1754
|
const info = readDaemonInfo();
|
|
1531
1755
|
const port = info?.httpPort ?? info?.port ?? findDaemonPort() ?? parseInt(opts.port, 10);
|
|
@@ -2007,12 +2231,24 @@ timebox
|
|
|
2007
2231
|
const pixoo = program.command('pixoo').description('Manage Pixoo64 LED matrix devices');
|
|
2008
2232
|
pixoo
|
|
2009
2233
|
.command('scan')
|
|
2010
|
-
.description('Discover Pixoo devices on LAN')
|
|
2011
|
-
.
|
|
2012
|
-
|
|
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');
|
|
2013
2239
|
const { loadPixooDevices, savePixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
2014
|
-
|
|
2015
|
-
|
|
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 });
|
|
2016
2252
|
if (found.length === 0) {
|
|
2017
2253
|
log('No devices found.');
|
|
2018
2254
|
return;
|
|
@@ -2081,7 +2317,12 @@ pixoo
|
|
|
2081
2317
|
const { loadPixooDevices } = await import('./pixoo/pixoo-settings.js');
|
|
2082
2318
|
const devices = loadPixooDevices();
|
|
2083
2319
|
if (devices.length === 0) {
|
|
2084
|
-
|
|
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.');
|
|
2085
2326
|
return;
|
|
2086
2327
|
}
|
|
2087
2328
|
log(`${devices.length} device(s):`);
|
|
@@ -2472,6 +2713,49 @@ apme
|
|
|
2472
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}`);
|
|
2473
2714
|
}
|
|
2474
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
|
+
});
|
|
2475
2759
|
apme
|
|
2476
2760
|
.command('judge')
|
|
2477
2761
|
.description('Run evaluation on unevaluated runs')
|