@agentdeck/bridge 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +3 -0
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adb-reverse.d.ts.map +1 -1
  6. package/dist/adb-reverse.js +4 -2
  7. package/dist/adb-reverse.js.map +1 -1
  8. package/dist/apme/collector.js +2 -2
  9. package/dist/apme/collector.js.map +1 -1
  10. package/dist/apme/outcome.js +2 -2
  11. package/dist/apme/outcome.js.map +1 -1
  12. package/dist/apme/runner.js +3 -3
  13. package/dist/apme/runner.js.map +1 -1
  14. package/dist/ble-sync-spawn.js +1 -1
  15. package/dist/ble-sync-spawn.js.map +1 -1
  16. package/dist/calendar.d.ts +82 -0
  17. package/dist/calendar.d.ts.map +1 -0
  18. package/dist/calendar.js +324 -0
  19. package/dist/calendar.js.map +1 -0
  20. package/dist/card-feed.d.ts +63 -5
  21. package/dist/card-feed.d.ts.map +1 -1
  22. package/dist/card-feed.js +218 -14
  23. package/dist/card-feed.js.map +1 -1
  24. package/dist/card-modules.d.ts +4 -0
  25. package/dist/card-modules.d.ts.map +1 -1
  26. package/dist/card-modules.js +5 -3
  27. package/dist/card-modules.js.map +1 -1
  28. package/dist/check-deps.js +4 -4
  29. package/dist/check-deps.js.map +1 -1
  30. package/dist/cli.d.ts +35 -0
  31. package/dist/cli.d.ts.map +1 -1
  32. package/dist/cli.js +269 -9
  33. package/dist/cli.js.map +1 -1
  34. package/dist/codex-otel.d.ts +89 -0
  35. package/dist/codex-otel.d.ts.map +1 -0
  36. package/dist/codex-otel.js +487 -0
  37. package/dist/codex-otel.js.map +1 -0
  38. package/dist/codex-rate-limits.d.ts.map +1 -1
  39. package/dist/codex-rate-limits.js +15 -3
  40. package/dist/codex-rate-limits.js.map +1 -1
  41. package/dist/daemon-server.d.ts +4 -0
  42. package/dist/daemon-server.d.ts.map +1 -1
  43. package/dist/daemon-server.js +797 -54
  44. package/dist/daemon-server.js.map +1 -1
  45. package/dist/daemon-ws-client.d.ts +108 -24
  46. package/dist/daemon-ws-client.d.ts.map +1 -1
  47. package/dist/daemon-ws-client.js +175 -39
  48. package/dist/daemon-ws-client.js.map +1 -1
  49. package/dist/daemon.js +1 -1
  50. package/dist/device-voice-reply.d.ts.map +1 -1
  51. package/dist/device-voice-reply.js +15 -3
  52. package/dist/device-voice-reply.js.map +1 -1
  53. package/dist/device-voice.d.ts +37 -0
  54. package/dist/device-voice.d.ts.map +1 -1
  55. package/dist/device-voice.js +130 -1
  56. package/dist/device-voice.js.map +1 -1
  57. package/dist/diag-analyzer.d.ts.map +1 -1
  58. package/dist/diag-analyzer.js +1 -0
  59. package/dist/diag-analyzer.js.map +1 -1
  60. package/dist/display-monitor.d.ts.map +1 -1
  61. package/dist/display-monitor.js +1 -0
  62. package/dist/display-monitor.js.map +1 -1
  63. package/dist/esp32-serial.d.ts +4 -0
  64. package/dist/esp32-serial.d.ts.map +1 -1
  65. package/dist/esp32-serial.js +6 -3
  66. package/dist/esp32-serial.js.map +1 -1
  67. package/dist/foundation-models-helper.d.ts +23 -0
  68. package/dist/foundation-models-helper.d.ts.map +1 -1
  69. package/dist/foundation-models-helper.js +32 -0
  70. package/dist/foundation-models-helper.js.map +1 -1
  71. package/dist/gateway-probe.js +1 -1
  72. package/dist/gateway-probe.js.map +1 -1
  73. package/dist/glance-frame.d.ts +64 -0
  74. package/dist/glance-frame.d.ts.map +1 -0
  75. package/dist/glance-frame.js +364 -0
  76. package/dist/glance-frame.js.map +1 -0
  77. package/dist/hook-codex-sessions.d.ts +40 -0
  78. package/dist/hook-codex-sessions.d.ts.map +1 -0
  79. package/dist/hook-codex-sessions.js +174 -0
  80. package/dist/hook-codex-sessions.js.map +1 -0
  81. package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -1
  82. package/dist/idotmatrix/idotmatrix-discover.js +7 -4
  83. package/dist/idotmatrix/idotmatrix-discover.js.map +1 -1
  84. package/dist/idotmatrix/idotmatrix-settings.d.ts +15 -0
  85. package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -1
  86. package/dist/idotmatrix/idotmatrix-settings.js +21 -0
  87. package/dist/idotmatrix/idotmatrix-settings.js.map +1 -1
  88. package/dist/index.d.ts +7 -0
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +88 -7
  91. package/dist/index.js.map +1 -1
  92. package/dist/linux-service.d.ts +52 -0
  93. package/dist/linux-service.d.ts.map +1 -0
  94. package/dist/linux-service.js +206 -0
  95. package/dist/linux-service.js.map +1 -0
  96. package/dist/logger.d.ts +6 -0
  97. package/dist/logger.d.ts.map +1 -1
  98. package/dist/logger.js +15 -0
  99. package/dist/logger.js.map +1 -1
  100. package/dist/mdns-discover.d.ts +19 -0
  101. package/dist/mdns-discover.d.ts.map +1 -0
  102. package/dist/mdns-discover.js +109 -0
  103. package/dist/mdns-discover.js.map +1 -0
  104. package/dist/model-catalog.d.ts.map +1 -1
  105. package/dist/model-catalog.js +1 -0
  106. package/dist/model-catalog.js.map +1 -1
  107. package/dist/modules/adb-module.js +1 -1
  108. package/dist/modules/adb-module.js.map +1 -1
  109. package/dist/output-parser.d.ts +9 -0
  110. package/dist/output-parser.d.ts.map +1 -1
  111. package/dist/output-parser.js +67 -16
  112. package/dist/output-parser.js.map +1 -1
  113. package/dist/passive-observer.d.ts +57 -8
  114. package/dist/passive-observer.d.ts.map +1 -1
  115. package/dist/passive-observer.js +210 -74
  116. package/dist/passive-observer.js.map +1 -1
  117. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
  118. package/dist/pixoo/pixoo-bridge.js +47 -6
  119. package/dist/pixoo/pixoo-bridge.js.map +1 -1
  120. package/dist/pixoo/pixoo-renderer.d.ts +2 -1
  121. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  122. package/dist/pixoo/pixoo-renderer.js +137 -8
  123. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  124. package/dist/pocket-autonomy.d.ts +78 -0
  125. package/dist/pocket-autonomy.d.ts.map +1 -0
  126. package/dist/pocket-autonomy.js +529 -0
  127. package/dist/pocket-autonomy.js.map +1 -0
  128. package/dist/pty-manager.d.ts.map +1 -1
  129. package/dist/pty-manager.js +3 -2
  130. package/dist/pty-manager.js.map +1 -1
  131. package/dist/python-ble-runtime.d.ts +2 -0
  132. package/dist/python-ble-runtime.d.ts.map +1 -1
  133. package/dist/python-ble-runtime.js +2 -1
  134. package/dist/python-ble-runtime.js.map +1 -1
  135. package/dist/remote-sessions.d.ts +64 -0
  136. package/dist/remote-sessions.d.ts.map +1 -0
  137. package/dist/remote-sessions.js +81 -0
  138. package/dist/remote-sessions.js.map +1 -0
  139. package/dist/review-runner.js +2 -2
  140. package/dist/review-runner.js.map +1 -1
  141. package/dist/session-aggregator.d.ts +2 -0
  142. package/dist/session-aggregator.d.ts.map +1 -1
  143. package/dist/session-aggregator.js +6 -1
  144. package/dist/session-aggregator.js.map +1 -1
  145. package/dist/session-focus-relay.d.ts +47 -1
  146. package/dist/session-focus-relay.d.ts.map +1 -1
  147. package/dist/session-focus-relay.js +130 -15
  148. package/dist/session-focus-relay.js.map +1 -1
  149. package/dist/session-push-channel.d.ts +31 -0
  150. package/dist/session-push-channel.d.ts.map +1 -0
  151. package/dist/session-push-channel.js +94 -0
  152. package/dist/session-push-channel.js.map +1 -0
  153. package/dist/session-registry.d.ts +126 -0
  154. package/dist/session-registry.d.ts.map +1 -1
  155. package/dist/session-registry.js +180 -3
  156. package/dist/session-registry.js.map +1 -1
  157. package/dist/subagent-timeline.d.ts +34 -0
  158. package/dist/subagent-timeline.d.ts.map +1 -0
  159. package/dist/subagent-timeline.js +156 -0
  160. package/dist/subagent-timeline.js.map +1 -0
  161. package/dist/timebox/timebox-discover.js +1 -1
  162. package/dist/timebox/timebox-discover.js.map +1 -1
  163. package/dist/tui/gauge.d.ts +4 -2
  164. package/dist/tui/gauge.d.ts.map +1 -1
  165. package/dist/tui/gauge.js +8 -4
  166. package/dist/tui/gauge.js.map +1 -1
  167. package/dist/tui/renderer.d.ts.map +1 -1
  168. package/dist/tui/renderer.js +28 -0
  169. package/dist/tui/renderer.js.map +1 -1
  170. package/dist/types.d.ts +1 -1
  171. package/dist/types.d.ts.map +1 -1
  172. package/dist/types.js.map +1 -1
  173. package/dist/usage-api.d.ts +43 -0
  174. package/dist/usage-api.d.ts.map +1 -1
  175. package/dist/usage-api.js +114 -9
  176. package/dist/usage-api.js.map +1 -1
  177. package/dist/usage-event.d.ts +19 -0
  178. package/dist/usage-event.d.ts.map +1 -1
  179. package/dist/usage-event.js +35 -0
  180. package/dist/usage-event.js.map +1 -1
  181. package/dist/utils/project-name.d.ts.map +1 -1
  182. package/dist/utils/project-name.js +1 -0
  183. package/dist/utils/project-name.js.map +1 -1
  184. package/dist/version-check.d.ts.map +1 -1
  185. package/dist/version-check.js +3 -2
  186. package/dist/version-check.js.map +1 -1
  187. package/dist/voice-assistant.d.ts.map +1 -1
  188. package/dist/voice-assistant.js +2 -1
  189. package/dist/voice-assistant.js.map +1 -1
  190. package/dist/voice.d.ts.map +1 -1
  191. package/dist/voice.js +1 -0
  192. package/dist/voice.js.map +1 -1
  193. package/dist/weather.d.ts +72 -0
  194. package/dist/weather.d.ts.map +1 -0
  195. package/dist/weather.js +206 -0
  196. package/dist/weather.js.map +1 -0
  197. package/dist/windows-service.js +5 -5
  198. package/dist/windows-service.js.map +1 -1
  199. package/fm-helper/AgentDeckFMHelper.swift +200 -0
  200. package/package.json +15 -13
  201. package/src/idotmatrix/identity_generated.py +50 -0
  202. package/src/idotmatrix/scan.py +33 -13
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
2
+ import { Command, InvalidArgumentError } from 'commander';
3
3
  import { writeFileSync, unlinkSync, existsSync, realpathSync } from 'fs';
4
4
  import { homedir } from 'os';
5
5
  import { dirname, join } from 'path';
@@ -8,13 +8,42 @@ 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
12
  import { ensureBleRuntime, getBleRuntimeStatus } from './python-ble-runtime.js';
12
13
  import { TASK_NAME, installWindowsTask, taskExists, runWindowsTask, endWindowsTask, deleteWindowsTask, } from './windows-service.js';
14
+ import { deriveRemoteAttachOpts } from './session-registry.js';
15
+ import { SERVICE_NAME, hasSystemctl, unitExists, installUnit, startUnit, disableUnit, getUnitPath, getDataDir, } from './linux-service.js';
13
16
  const require = createRequire(import.meta.url);
14
17
  const packageJson = require('../package.json');
18
+ /**
19
+ * Pre-PTY warning for a host hint given without the opt-in switch. Must run in
20
+ * the CLI action handler BEFORE the bridge starts: once the PTY takes the
21
+ * terminal, `log()` output is swallowed and the user would never see it.
22
+ */
23
+ function warnIfDaemonHostIgnored(opts) {
24
+ if (deriveRemoteAttachOpts(opts).ignoredHostHint) {
25
+ console.error('[agentdeck] --daemon-host / AGENTDECK_DAEMON_HOST is set but ignored: remote attach requires --remote-daemon (or AGENTDECK_REMOTE_DAEMON=1). Session stays local-only.');
26
+ }
27
+ }
15
28
  function log(msg) {
16
29
  process.stderr.write(msg + '\n');
17
30
  }
31
+ /**
32
+ * Parse a `--weight <n>` value into an integer sort override. Rejects
33
+ * non-integers so a typo (`--weight foo`) fails loudly instead of silently
34
+ * collapsing to 0, and rejects values outside the documented cross-platform
35
+ * range (shared SESSION_WEIGHT_MIN/MAX) — the wire value must fit every
36
+ * consumer's integer type (Kotlin `Int` in particular), and comparators on
37
+ * fixed-width platforms must never see values that could overflow. Lower
38
+ * sorts first (see shared sortSessions).
39
+ */
40
+ function parseWeight(value) {
41
+ const n = Number(value);
42
+ if (!Number.isInteger(n) || n < SESSION_WEIGHT_MIN || n > SESSION_WEIGHT_MAX) {
43
+ throw new InvalidArgumentError(`weight must be an integer between ${SESSION_WEIGHT_MIN} and ${SESSION_WEIGHT_MAX} (e.g. --weight 1, --weight -5)`);
44
+ }
45
+ return n;
46
+ }
18
47
  function formatBytes(value) {
19
48
  const bytes = typeof value === 'number' && Number.isFinite(value) ? value : 0;
20
49
  if (bytes >= 1024 * 1024)
@@ -141,7 +170,7 @@ function projectRootPath() {
141
170
  }
142
171
  async function runBlePython(python, args, captureStdout = false) {
143
172
  return new Promise((resolve, reject) => {
144
- const child = spawn(python, args, captureStdout ? { stdio: ['ignore', 'pipe', 'inherit'] } : { stdio: 'inherit' });
173
+ const child = spawn(python, args, captureStdout ? { stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true } : { stdio: 'inherit', windowsHide: true });
145
174
  let stdout = '';
146
175
  child.stdout?.on('data', (data) => {
147
176
  stdout += data.toString();
@@ -204,6 +233,117 @@ function platformioBin() {
204
233
  return '/opt/homebrew/bin/pio';
205
234
  return 'pio';
206
235
  }
236
+ // ===== Env-var default CLI args =====
237
+ //
238
+ // Two layers of environment-variable defaults, so a shell profile can set
239
+ // repeated flags once:
240
+ // AGENTDECK_COMMANDER_ARGS → args for the `agentdeck` (commander) layer,
241
+ // spliced into argv after the session subcommand.
242
+ // AGENTDECK_<AGENT>_ARGS → args appended to the spawned agent command,
243
+ // weaving into an explicit `-c` when present.
244
+ // See docs and cli.test.ts for behavior.
245
+ /** Session subcommands the global env-args var applies to. */
246
+ const SESSION_COMMANDS = ['claude', 'codex', 'opencode', 'monitor'];
247
+ /** Env var holding extra args for the spawned agent command, per agent type. */
248
+ const AGENT_ARGS_ENV = {
249
+ 'claude-code': 'AGENTDECK_CLAUDE_ARGS',
250
+ 'codex-cli': 'AGENTDECK_CODEX_ARGS',
251
+ opencode: 'AGENTDECK_OPENCODE_ARGS',
252
+ };
253
+ /**
254
+ * Split an env-provided arg string into tokens, honoring single/double quotes
255
+ * so values like `--flag "a b"` survive. No shell expansion (no $VAR,
256
+ * globbing, or escapes beyond quote grouping) — this only groups whitespace.
257
+ * Cross-platform: pure JS, never invokes a shell.
258
+ */
259
+ export function tokenizeArgString(s) {
260
+ if (!s)
261
+ return [];
262
+ const tokens = [];
263
+ // Match: double-quoted run | single-quoted run | bare non-space run.
264
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
265
+ let cur = '';
266
+ let curActive = false;
267
+ let lastIndex = 0;
268
+ let m;
269
+ while ((m = re.exec(s)) !== null) {
270
+ // A gap of whitespace between matches ends the current token.
271
+ if (curActive && m.index > lastIndex) {
272
+ tokens.push(cur);
273
+ cur = '';
274
+ curActive = false;
275
+ }
276
+ cur += m[1] ?? m[2] ?? m[3] ?? '';
277
+ curActive = true;
278
+ lastIndex = re.lastIndex;
279
+ }
280
+ if (curActive)
281
+ tokens.push(cur);
282
+ return tokens;
283
+ }
284
+ /** Per-invocation escape hatch: disables BOTH env-default layers when typed. */
285
+ const NO_ENV_ARGS_FLAG = '--no-env-args';
286
+ /**
287
+ * If argv's subcommand is a session command and AGENTDECK_COMMANDER_ARGS is set,
288
+ * splice its tokens in immediately after the subcommand name. Env tokens land
289
+ * before any user-typed tokens, giving last-write override for SCALAR options
290
+ * (retype `--weight`/`--daemon-host` to win); boolean flags have no inverse
291
+ * spelling, so the per-invocation override is the typed `--no-env-args` hatch,
292
+ * which skips the splice entirely (and, parsed by commander, turns the
293
+ * per-agent append off in the session actions too).
294
+ * Returns a new argv; no-op (returns input) when unset or not a session command.
295
+ */
296
+ export function applyGlobalEnvArgs(argv, env = process.env) {
297
+ // The hatch itself is stripped from the env tokens: an env-smuggled
298
+ // --no-env-args would otherwise splice normally and then parse to
299
+ // envArgs=false, silently disabling only the per-agent layer — an
300
+ // undocumented half-state. The env var cannot turn the hatch on or off;
301
+ // only a typed flag can.
302
+ const extra = tokenizeArgString(env.AGENTDECK_COMMANDER_ARGS)
303
+ .filter((tok) => tok !== NO_ENV_ARGS_FLAG);
304
+ if (extra.length === 0)
305
+ return argv;
306
+ // The CLI has a fixed top-level command position: argv[0]=node,
307
+ // argv[1]=script, argv[2]=subcommand. Keying the decision to argv[2] (not a
308
+ // token scan) keeps non-session commands a true no-op even when a
309
+ // positional VALUE equals a session command name (`agentdeck speak b claude`).
310
+ const sub = argv[2];
311
+ if (sub === undefined || !SESSION_COMMANDS.includes(sub))
312
+ return argv;
313
+ // Typed escape hatch — checked on the RAW argv before splicing, because
314
+ // post-parse is too late (env tokens would already have parsed into opts).
315
+ // Exact-token match: a flag VALUE lexically equal to --no-env-args would
316
+ // false-positive, but producing that requires a deliberately pathological
317
+ // invocation (`-c --no-env-args`), and the argv[2] gate above bounds the
318
+ // scan to session commands.
319
+ if (argv.slice(3).includes(NO_ENV_ARGS_FLAG))
320
+ return argv;
321
+ const next = argv.slice();
322
+ next.splice(3, 0, ...extra);
323
+ return next;
324
+ }
325
+ /**
326
+ * Append the per-agent env var to the resolved `-c` command string. Raw append
327
+ * (single space + trim) — the PTY shell re-parses the whole string at spawn, so
328
+ * this rides the same platform shell path (cmd.exe / POSIX) that already handles
329
+ * a user's `-c`. No-op (returns `command`) when the var is unset/empty.
330
+ */
331
+ export function weaveAgentCommand(agentType, command, env = process.env) {
332
+ const extra = (env[AGENT_ARGS_ENV[agentType]] ?? '').trim();
333
+ if (!extra)
334
+ return command;
335
+ return `${command} ${extra}`.trim();
336
+ }
337
+ /**
338
+ * The session actions' one-call selection for the spawned agent command:
339
+ * weave the per-agent env args unless the user typed `--no-env-args`
340
+ * (commander parses it as `opts.envArgs === false`; the hatch covers BOTH
341
+ * env layers, and the commander-layer half is handled pre-parse in
342
+ * `applyGlobalEnvArgs`).
343
+ */
344
+ export function resolveAgentCommand(agentType, command, envArgsEnabled, env = process.env) {
345
+ return envArgsEnabled ? weaveAgentCommand(agentType, command, env) : command;
346
+ }
207
347
  // ===== Program =====
208
348
  const program = new Command();
209
349
  program
@@ -223,16 +363,24 @@ program
223
363
  .option('--no-adb', 'Disable ADB reverse setup')
224
364
  .option('--no-postit', 'Disable terminal tab title updates')
225
365
  .option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
366
+ .option('--remote-daemon', 'Opt in to attaching this session to a daemon on another machine (gates --daemon-host and mDNS discovery)')
367
+ .option('--daemon-host <host>', 'Explicit remote daemon endpoint (host or host:port); requires --remote-daemon')
368
+ .option('--no-env-args', 'Ignore AGENTDECK_COMMANDER_ARGS and AGENTDECK_<AGENT>_ARGS for this invocation')
369
+ .option('--weight <n>', 'Deck/tab sort order override (integer, lower first; default 0)', parseWeight)
226
370
  .action(async (opts) => {
371
+ warnIfDaemonHostIgnored(opts);
227
372
  const { startSession } = await import('./index.js');
228
373
  await startSession({
229
374
  agentType: 'claude-code',
230
375
  port: parseInt(opts.port, 10),
231
- command: opts.command,
376
+ command: resolveAgentCommand('claude-code', opts.command, opts.envArgs !== false),
232
377
  debug: opts.debug,
233
378
  noUpdateCheck: opts.updateCheck === false,
234
379
  postit: opts.postit !== false,
235
380
  wakeWord: !!opts.wakeWord,
381
+ remoteDaemon: !!opts.remoteDaemon,
382
+ daemonHost: opts.daemonHost,
383
+ weight: opts.weight,
236
384
  modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
237
385
  mdns: false, // daemon-only — session bridges never advertise mDNS
238
386
  adb: opts.adb !== false ? 'auto' : false,
@@ -252,7 +400,12 @@ program
252
400
  .option('--no-adb', 'Disable ADB reverse setup')
253
401
  .option('--no-postit', 'Disable terminal tab title updates')
254
402
  .option('--no-codex-hooks', 'Skip ~/.codex/config.toml hook install')
403
+ .option('--remote-daemon', 'Opt in to attaching this session to a daemon on another machine (gates --daemon-host and mDNS discovery)')
404
+ .option('--daemon-host <host>', 'Explicit remote daemon endpoint (host or host:port); requires --remote-daemon')
405
+ .option('--no-env-args', 'Ignore AGENTDECK_COMMANDER_ARGS and AGENTDECK_<AGENT>_ARGS for this invocation')
406
+ .option('--weight <n>', 'Deck/tab sort order override (integer, lower first; default 0)', parseWeight)
255
407
  .action(async (opts) => {
408
+ warnIfDaemonHostIgnored(opts);
256
409
  // Install Codex lifecycle hooks before starting the session so the
257
410
  // first prompt's UserPromptSubmit / Stop events reach the daemon.
258
411
  // Idempotent: re-running with the same daemon port is a no-op.
@@ -281,9 +434,12 @@ program
281
434
  await startSession({
282
435
  agentType: 'codex-cli',
283
436
  port: parseInt(opts.port, 10),
284
- command: opts.command,
437
+ command: resolveAgentCommand('codex-cli', opts.command, opts.envArgs !== false),
285
438
  debug: opts.debug,
286
439
  postit: opts.postit !== false,
440
+ remoteDaemon: !!opts.remoteDaemon,
441
+ daemonHost: opts.daemonHost,
442
+ weight: opts.weight,
287
443
  modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
288
444
  mdns: false, // daemon-only
289
445
  adb: opts.adb !== false ? 'auto' : false,
@@ -303,7 +459,12 @@ program
303
459
  .option('--no-adb', 'Disable ADB reverse setup')
304
460
  .option('--no-postit', 'Disable terminal tab title updates')
305
461
  .option('--no-opencode-hooks', 'Skip OpenCode observer plugin install')
462
+ .option('--remote-daemon', 'Opt in to attaching this session to a daemon on another machine (gates --daemon-host and mDNS discovery)')
463
+ .option('--daemon-host <host>', 'Explicit remote daemon endpoint (host or host:port); requires --remote-daemon')
464
+ .option('--no-env-args', 'Ignore AGENTDECK_COMMANDER_ARGS and AGENTDECK_<AGENT>_ARGS for this invocation')
465
+ .option('--weight <n>', 'Deck/tab sort order override (integer, lower first; default 0)', parseWeight)
306
466
  .action(async (opts) => {
467
+ warnIfDaemonHostIgnored(opts);
307
468
  // Install the OpenCode observer plugin so standalone `opencode` runs
308
469
  // (outside this managed session) also reach the daemon timeline. The
309
470
  // plugin self-disables inside managed PTYs via AGENTDECK_PORT, so this
@@ -331,9 +492,12 @@ program
331
492
  await startSession({
332
493
  agentType: 'opencode',
333
494
  port: parseInt(opts.port, 10),
334
- command: opts.command,
495
+ command: resolveAgentCommand('opencode', opts.command, opts.envArgs !== false),
335
496
  debug: opts.debug,
336
497
  postit: opts.postit !== false,
498
+ remoteDaemon: !!opts.remoteDaemon,
499
+ daemonHost: opts.daemonHost,
500
+ weight: opts.weight,
337
501
  modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
338
502
  mdns: false,
339
503
  adb: opts.adb !== false ? 'auto' : false,
@@ -349,7 +513,13 @@ program
349
513
  .option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
350
514
  .option('-d, --debug', 'Enable debug logging')
351
515
  .option('--local', 'Disable all device modules')
516
+ .option('--remote-daemon', 'Opt in to attaching this session to a daemon on another machine (gates --daemon-host and mDNS discovery)')
517
+ .option('--daemon-host <host>', 'Explicit remote daemon endpoint (host or host:port); requires --remote-daemon')
518
+ // Commander-layer half only: monitor spawns no agent command to weave into.
519
+ .option('--no-env-args', 'Ignore AGENTDECK_COMMANDER_ARGS for this invocation')
520
+ .option('--weight <n>', 'Deck/tab sort order override (integer, lower first; default 0)', parseWeight)
352
521
  .action(async (opts) => {
522
+ warnIfDaemonHostIgnored(opts);
353
523
  const { startSession } = await import('./index.js');
354
524
  const port = parseInt(opts.port, 10);
355
525
  log(`\nMonitor mode: hook server on port ${port}`);
@@ -358,6 +528,9 @@ program
358
528
  agentType: 'monitor',
359
529
  port,
360
530
  debug: opts.debug,
531
+ remoteDaemon: !!opts.remoteDaemon,
532
+ daemonHost: opts.daemonHost,
533
+ weight: opts.weight,
361
534
  modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : undefined,
362
535
  });
363
536
  });
@@ -465,6 +638,7 @@ daemon
465
638
  const child = spawn(process.execPath, args, {
466
639
  detached: true,
467
640
  stdio: ['ignore', out, err],
641
+ windowsHide: true,
468
642
  });
469
643
  child.unref();
470
644
  log(`Daemon started (PID ${child.pid})`);
@@ -502,6 +676,7 @@ daemon
502
676
  const child = spawn(process.execPath, args, {
503
677
  detached: true,
504
678
  stdio: 'ignore',
679
+ windowsHide: true,
505
680
  });
506
681
  child.unref();
507
682
  log(`Daemon restarted (PID ${child.pid})`);
@@ -534,7 +709,7 @@ daemon
534
709
  });
535
710
  daemon
536
711
  .command('install')
537
- .description('Install daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows)')
712
+ .description('Install daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows, systemd --user unit on Linux)')
538
713
  .action(async () => {
539
714
  if (process.platform === 'win32') {
540
715
  try {
@@ -588,6 +763,62 @@ daemon
588
763
  catch { /* hooks package not built yet — task install still succeeds */ }
589
764
  process.exit(0);
590
765
  }
766
+ if (process.platform === 'linux') {
767
+ if (!hasSystemctl()) {
768
+ log('systemd --user is not available on this host.');
769
+ log('Run the daemon manually with: agentdeck daemon start');
770
+ log('(or add your own autostart entry).');
771
+ }
772
+ else {
773
+ try {
774
+ installUnit();
775
+ startUnit();
776
+ log(`systemd user unit '${SERVICE_NAME}' installed and started.`);
777
+ log(`Unit file: ${getUnitPath()}`);
778
+ if (process.env.AGENTDECK_DATA_DIR) {
779
+ log(`Data dir: ${getDataDir()} (AGENTDECK_DATA_DIR persisted into the unit — re-run 'agentdeck daemon install' to change it)`);
780
+ }
781
+ else {
782
+ log(`Data dir: ${getDataDir()}`);
783
+ }
784
+ log('For boot-without-login on a headless host, run once: loginctl enable-linger $USER');
785
+ }
786
+ catch (e) {
787
+ const detail = e.stderr?.toString().trim() || e.message;
788
+ log('Failed to install the systemd user unit.');
789
+ if (detail)
790
+ log(detail);
791
+ log('You can still run the daemon manually with: agentdeck daemon start');
792
+ process.exit(1);
793
+ }
794
+ }
795
+ // Install Codex + OpenCode hooks for parity with the macOS/Windows paths.
796
+ try {
797
+ const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
798
+ const result = installCodexHooksIfNeeded();
799
+ if (result.installed) {
800
+ log('Codex lifecycle hooks installed in ~/.codex/config.toml');
801
+ if (result.warning)
802
+ log(`Codex hooks degraded: ${result.warning}`);
803
+ }
804
+ else if (result.reason) {
805
+ log(`Codex hooks skipped: ${result.reason}`);
806
+ }
807
+ }
808
+ catch { /* hooks package not built yet — install still succeeds */ }
809
+ try {
810
+ const { installOpenCodeHooksIfNeeded, opencodePluginPath } = await import('@agentdeck/hooks');
811
+ const result = installOpenCodeHooksIfNeeded();
812
+ if (result.installed) {
813
+ log(`OpenCode observer plugin installed at ${opencodePluginPath()}`);
814
+ }
815
+ else if (result.reason) {
816
+ log(`OpenCode observer plugin skipped: ${result.reason}`);
817
+ }
818
+ }
819
+ catch { /* hooks package not built yet — install still succeeds */ }
820
+ process.exit(0);
821
+ }
591
822
  if (process.platform !== 'darwin') {
592
823
  log('LaunchAgent is macOS-only');
593
824
  process.exit(1);
@@ -630,7 +861,7 @@ daemon
630
861
  });
631
862
  daemon
632
863
  .command('uninstall')
633
- .description('Remove daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows)')
864
+ .description('Remove daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows, systemd --user unit on Linux)')
634
865
  .action(async () => {
635
866
  if (process.platform === 'win32') {
636
867
  if (!taskExists()) {
@@ -656,6 +887,17 @@ daemon
656
887
  }
657
888
  process.exit(0);
658
889
  }
890
+ if (process.platform === 'linux') {
891
+ if (!unitExists()) {
892
+ log(`systemd user unit '${SERVICE_NAME}' is not installed.`);
893
+ process.exit(0);
894
+ }
895
+ // Graceful daemon shutdown first, then disable + remove the unit.
896
+ await stopDaemon(BRIDGE_WS_PORT);
897
+ disableUnit();
898
+ log(`systemd user unit '${SERVICE_NAME}' removed.`);
899
+ process.exit(0);
900
+ }
659
901
  if (!existsSync(PLIST_PATH)) {
660
902
  log('LaunchAgent not installed');
661
903
  return;
@@ -961,6 +1203,7 @@ program
961
1203
  .option('-e, --env <env>', 'PlatformIO environment for default firmware path')
962
1204
  .option('-f, --firmware <path>', 'Firmware .bin path')
963
1205
  .option('--build', 'Build the PlatformIO environment before upload')
1206
+ .option('--stage', 'Stage for pull OTA: the board installs it on its next feed pull (no live WS needed)')
964
1207
  .action(async (target, opts) => {
965
1208
  const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
966
1209
  const info = readDaemonInfo();
@@ -974,12 +1217,25 @@ program
974
1217
  execFileSync(pio, ['run', '-e', env], {
975
1218
  cwd: join(projectRootPath(), 'esp32'),
976
1219
  stdio: 'inherit',
1220
+ windowsHide: true,
977
1221
  });
978
1222
  }
979
1223
  // Send the canonical board string, not the raw alias — the daemon matches
980
1224
  // `target` against device_info.board, so `ttgo` must become `ttgo_t_display`.
981
1225
  const daemonTarget = resolveEsp32OtaDaemonTarget(target);
982
1226
  const firmwarePath = resolveEsp32FirmwarePath(target, opts.env, opts.firmware);
1227
+ if (opts.stage) {
1228
+ // Pull-OTA staging: for wake-sync-sleep boards (XTeink X3/X4) that never
1229
+ // hold a live WS. The daemon adverts the build on the board's feed pulls;
1230
+ // the board downloads + flashes itself on its next wake.
1231
+ const { statusCode, body } = await postJsonWithTimeout(`http://127.0.0.1:${port}/esp32/ota`, { target: daemonTarget, firmwarePath, stage: true }, 30_000);
1232
+ if (statusCode < 200 || statusCode >= 300 || body.ok === false) {
1233
+ throw new Error(String(body.error ?? `HTTP ${statusCode}`));
1234
+ }
1235
+ log(`Staged for ${body.board}: ${formatBytes(body.bytes)} md5=${body.md5}`);
1236
+ log('The board installs it on its next feed pull (battery cadence: typically within 15-60 min).');
1237
+ return;
1238
+ }
983
1239
  log(`Uploading ${firmwarePath} to ${daemonTarget} via daemon :${port}...`);
984
1240
  let { statusCode, body } = await postJsonWithTimeout(`http://127.0.0.1:${port}/esp32/ota`, { target: daemonTarget, firmwarePath }, 15 * 60_000);
985
1241
  if (body.ok === false && String(body.error ?? '').startsWith('firmware_unreadable')) {
@@ -1194,7 +1450,11 @@ idotmatrix
1194
1450
  return;
1195
1451
  log('Scanning for BLE devices (5 seconds)...');
1196
1452
  try {
1197
- const { code, stdout } = await runBlePython(runtime.python, [runtime.paths.scripts.idotmatrixScan], true);
1453
+ // Panels are identified by advertised service UUID first and known name
1454
+ // families second; settings.json `idotmatrixNamePrefixes` widens the
1455
+ // latter for a rebranded panel that hides its service.
1456
+ const { loadIDotMatrixNamePrefixes } = await import('./idotmatrix/idotmatrix-settings.js');
1457
+ const { code, stdout } = await runBlePython(runtime.python, [runtime.paths.scripts.idotmatrixScan, ...loadIDotMatrixNamePrefixes()], true);
1198
1458
  if (code !== 0) {
1199
1459
  log('Failed to run scan script.');
1200
1460
  process.exitCode = 1;
@@ -2329,7 +2589,7 @@ export async function runCli(argv = process.argv) {
2329
2589
  await showDefaultStatusOrHelp();
2330
2590
  return;
2331
2591
  }
2332
- program.parse(argv);
2592
+ program.parse(applyGlobalEnvArgs(argv));
2333
2593
  }
2334
2594
  export { program };
2335
2595
  if (isMainModule()) {