@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +83 -13
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Branded desktop notifications for daemon-originated events (RUSH-2030).
3
+ *
4
+ * The one place the daemon (overdue routines, heal, routine start/finish/output)
5
+ * emits a native desktop notification. On macOS it routes through the installed
6
+ * `MenubarHelper.app` companion — a one-shot `MenubarHelper --notify` invocation —
7
+ * so the notification is attributed to that bundle and carries the agents-cli /
8
+ * Agentialized mark instead of the generic AppleScript icon. When the companion
9
+ * app is not installed (menu bar disabled, a Linux package, a dev checkout), it
10
+ * degrades to `osascript` so an overdue/heal notice is never silently lost — the
11
+ * generic icon is the acceptable cost of preserving delivery, not a bug hidden by
12
+ * a fallback. On Linux it uses `notify-send`; every other platform is a no-op.
13
+ *
14
+ * All delivery is best-effort and fully detached: a missing binary surfaces as an
15
+ * async 'error' event (not a synchronous throw), so every spawn attaches an
16
+ * 'error' listener — without it Node re-throws ENOENT as an uncaught exception and
17
+ * takes the whole daemon down (the exact crash overdue.test.ts guards).
18
+ */
19
+ export interface DesktopNotification {
20
+ /** Bold first line. */
21
+ title: string;
22
+ /** Notification body text. */
23
+ body: string;
24
+ /** Secondary line under the title (macOS only). */
25
+ subtitle?: string;
26
+ /**
27
+ * Deep-link the companion app runs when the notification is clicked. Encoded
28
+ * as `<verb>:<arg>` — `open:/abs/path` opens a file (a run report or log) in the
29
+ * default app; `routines:list` opens the runs folder (~/.agents/.history/runs)
30
+ * in Finder. Best-effort and
31
+ * macOS-only (osascript / notify-send have no click target). See routine-notify.ts.
32
+ */
33
+ action?: string;
34
+ }
35
+ /** Argv for the MenubarHelper one-shot notify mode. Exported for tests. */
36
+ export declare function buildMenubarNotifyArgs(n: DesktopNotification): string[];
37
+ /** AppleScript for the osascript degradation path. Exported for tests. */
38
+ export declare function buildOsascriptNotifyArgs(n: DesktopNotification): string[];
39
+ /**
40
+ * Fire a native desktop notification, branded via the MenubarHelper companion on
41
+ * macOS. Best-effort — any failure is swallowed so a notification hiccup never
42
+ * blocks or crashes the daemon.
43
+ */
44
+ export declare function notifyDesktop(n: DesktopNotification): void;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Branded desktop notifications for daemon-originated events (RUSH-2030).
3
+ *
4
+ * The one place the daemon (overdue routines, heal, routine start/finish/output)
5
+ * emits a native desktop notification. On macOS it routes through the installed
6
+ * `MenubarHelper.app` companion — a one-shot `MenubarHelper --notify` invocation —
7
+ * so the notification is attributed to that bundle and carries the agents-cli /
8
+ * Agentialized mark instead of the generic AppleScript icon. When the companion
9
+ * app is not installed (menu bar disabled, a Linux package, a dev checkout), it
10
+ * degrades to `osascript` so an overdue/heal notice is never silently lost — the
11
+ * generic icon is the acceptable cost of preserving delivery, not a bug hidden by
12
+ * a fallback. On Linux it uses `notify-send`; every other platform is a no-op.
13
+ *
14
+ * All delivery is best-effort and fully detached: a missing binary surfaces as an
15
+ * async 'error' event (not a synchronous throw), so every spawn attaches an
16
+ * 'error' listener — without it Node re-throws ENOENT as an uncaught exception and
17
+ * takes the whole daemon down (the exact crash overdue.test.ts guards).
18
+ */
19
+ import { spawn } from 'child_process';
20
+ import * as os from 'os';
21
+ import { resolveInstalledMenubarExecutable } from './install-menubar.js';
22
+ /** Argv for the MenubarHelper one-shot notify mode. Exported for tests. */
23
+ export function buildMenubarNotifyArgs(n) {
24
+ const args = ['--notify', '--title', n.title, '--body', n.body];
25
+ if (n.subtitle)
26
+ args.push('--subtitle', n.subtitle);
27
+ if (n.action)
28
+ args.push('--action', n.action);
29
+ return args;
30
+ }
31
+ /** AppleScript for the osascript degradation path. Exported for tests. */
32
+ export function buildOsascriptNotifyArgs(n) {
33
+ const esc = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
34
+ let script = `display notification "${esc(n.body)}" with title "${esc(n.title)}"`;
35
+ if (n.subtitle)
36
+ script += ` subtitle "${esc(n.subtitle)}"`;
37
+ return ['-e', script];
38
+ }
39
+ /** Spawn one detached, best-effort notifier process; swallow async ENOENT. */
40
+ function spawnDetachedQuiet(command, args) {
41
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
42
+ // A missing binary (headless box with no osascript/notify-send, or a helper
43
+ // that vanished) arrives as an async 'error' event. Without this listener Node
44
+ // re-throws it as an uncaught exception and crashes the daemon — swallow it.
45
+ child.on('error', () => { });
46
+ child.unref();
47
+ }
48
+ /**
49
+ * Fire a native desktop notification, branded via the MenubarHelper companion on
50
+ * macOS. Best-effort — any failure is swallowed so a notification hiccup never
51
+ * blocks or crashes the daemon.
52
+ */
53
+ export function notifyDesktop(n) {
54
+ const platform = os.platform();
55
+ try {
56
+ if (platform === 'darwin') {
57
+ const exec = resolveInstalledMenubarExecutable();
58
+ if (exec) {
59
+ // Branded path: the notification is attributed to MenubarHelper.app, so it
60
+ // shows the agents-cli mark and its click action is handled by the running
61
+ // helper's UNUserNotificationCenter delegate.
62
+ spawnDetachedQuiet(exec, buildMenubarNotifyArgs(n));
63
+ return;
64
+ }
65
+ // Companion app absent — degrade to osascript so delivery is preserved
66
+ // (generic icon; no click action). See the module header.
67
+ spawnDetachedQuiet('osascript', buildOsascriptNotifyArgs(n));
68
+ return;
69
+ }
70
+ if (platform === 'linux') {
71
+ spawnDetachedQuiet('notify-send', [n.title, n.body]);
72
+ }
73
+ // Other platforms: no native desktop notifier wired — no-op.
74
+ }
75
+ catch {
76
+ // Notification is best-effort; nothing to do.
77
+ }
78
+ }
@@ -117,5 +117,21 @@ export declare function migrateExtrasExtrasToAgentsExtras(historyDir?: string):
117
117
  * Params default to the real routines dir; injectable for tests.
118
118
  */
119
119
  export declare function migrateRoutineDeviceToDevices(routinesDir?: string): void;
120
+ /**
121
+ * Fold the legacy watchdog enable sentinel into the watchdog routine.
122
+ *
123
+ * The always-on watchdog used to be gated by a presence sentinel at
124
+ * `<runtime-state>/watchdog/enabled`; it is now a daemon routine. A user who had
125
+ * run `agents watchdog enable` under the old build has that file on disk — without
126
+ * this, upgrading would silently drop them back to OFF (the always-on nudge just
127
+ * stops), the worst failure mode for a "survives reboots" feature. If the sentinel
128
+ * exists, ensure the `watchdog` routine exists AND is enabled, then delete the
129
+ * sentinel so this runs exactly once. If ensuring the routine fails, the sentinel
130
+ * is left in place so a later run retries rather than silently losing the opt-in.
131
+ *
132
+ * Both seams are injectable so the migration is unit-testable without touching the
133
+ * real routines dir (and without racing the routine module's own tests).
134
+ */
135
+ export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string, ensure?: (enabled: boolean) => void): void;
120
136
  /** Run all idempotent migrations. Safe to call multiple times. */
121
137
  export declare function runMigration(): Promise<void>;
@@ -13,6 +13,7 @@ import { atomicWriteFileSync } from './fs-atomic.js';
13
13
  import { machineId } from './machine-id.js';
14
14
  import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
15
15
  import { createLink } from './platform/index.js';
16
+ import { ensureWatchdogRoutine } from './watchdog/routine.js';
16
17
  const HOME = process.env.HOME ?? os.homedir();
17
18
  const USER_DIR = path.join(HOME, '.agents');
18
19
  /** Canonical system-repo location (post-fold). */
@@ -1981,6 +1982,37 @@ export function migrateRoutineDeviceToDevices(routinesDir) {
1981
1982
  console.error(`Migrated ${migrated} routine${migrated === 1 ? '' : 's'}: device → devices`);
1982
1983
  }
1983
1984
  }
1985
+ /**
1986
+ * Fold the legacy watchdog enable sentinel into the watchdog routine.
1987
+ *
1988
+ * The always-on watchdog used to be gated by a presence sentinel at
1989
+ * `<runtime-state>/watchdog/enabled`; it is now a daemon routine. A user who had
1990
+ * run `agents watchdog enable` under the old build has that file on disk — without
1991
+ * this, upgrading would silently drop them back to OFF (the always-on nudge just
1992
+ * stops), the worst failure mode for a "survives reboots" feature. If the sentinel
1993
+ * exists, ensure the `watchdog` routine exists AND is enabled, then delete the
1994
+ * sentinel so this runs exactly once. If ensuring the routine fails, the sentinel
1995
+ * is left in place so a later run retries rather than silently losing the opt-in.
1996
+ *
1997
+ * Both seams are injectable so the migration is unit-testable without touching the
1998
+ * real routines dir (and without racing the routine module's own tests).
1999
+ */
2000
+ export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_DIR, 'state', 'watchdog', 'enabled'), ensure = ensureWatchdogRoutine) {
2001
+ if (!fs.existsSync(sentinelPath))
2002
+ return;
2003
+ try {
2004
+ ensure(true);
2005
+ }
2006
+ catch (err) {
2007
+ console.error(`watchdog sentinel migration: could not create the routine (${err.message}); leaving the sentinel for a later retry`);
2008
+ return;
2009
+ }
2010
+ try {
2011
+ fs.rmSync(sentinelPath);
2012
+ }
2013
+ catch { /* already gone */ }
2014
+ console.error('Migrated watchdog: legacy enable sentinel → watchdog routine (kept enabled)');
2015
+ }
1984
2016
  /** Run all idempotent migrations. Safe to call multiple times. */
1985
2017
  export async function runMigration() {
1986
2018
  // MUST run first: every other migrator reads SYSTEM_DIR (the new path).
@@ -2039,6 +2071,10 @@ export async function runMigration() {
2039
2071
  migrateExtrasExtrasToAgentsExtras();
2040
2072
  // Rewrite routine YAML files: singular `device:` -> plural `devices: []`.
2041
2073
  migrateRoutineDeviceToDevices();
2074
+ // Fold the legacy watchdog enable sentinel into the watchdog routine so a user
2075
+ // who opted in under the old build stays opted in after upgrading. After the
2076
+ // routine rewrites above so the routines dir is in its canonical shape.
2077
+ migrateWatchdogSentinelToRoutine();
2042
2078
  // Symlink repair runs LAST so it can find the post-move version homes.
2043
2079
  repairAgentConfigSymlinks();
2044
2080
  // Repair self-referential node_modules/.bin/<cli> symlinks (the droid
@@ -96,6 +96,33 @@ export declare function resolveModel(agent: AgentId, version: string, requested:
96
96
  * like `${agent}-default`.
97
97
  */
98
98
  export declare function resolveEffectiveModel(agent: AgentId, version: string, requested?: string): string | null;
99
+ /** Where the model a given agent+version will actually run with came from. */
100
+ export type ConfiguredModelSource = 'run-default' | 'config' | 'cli-default';
101
+ export interface ConfiguredModel {
102
+ /** The model id the agent will run with (e.g. `opus`, `gpt-5-codex`). */
103
+ model: string;
104
+ source: ConfiguredModelSource;
105
+ }
106
+ /**
107
+ * The model a given agent+version is actually configured to use right now, with
108
+ * where that selection comes from. First hit wins:
109
+ * 1. run-default — the user's agents-cli `run.defaults` in agents.yaml
110
+ * 2. config — the agent's OWN native settings.json `model` field
111
+ * 3. cli-default — the CLI's built-in default: the catalog's `isDefault` model
112
+ * if one is flagged (e.g. Kimi), otherwise the literal
113
+ * `default` for a model-capable agent whose runtime picks its
114
+ * own default (Claude/Codex don't flag one — Claude's own UI
115
+ * calls this "Default").
116
+ * Each layer is a real source the agent consults; `version` must be concrete.
117
+ * Returns null only when the agent exposes no model catalog at all.
118
+ */
119
+ export declare function resolveConfiguredModel(agent: AgentId, version: string): ConfiguredModel | null;
120
+ /**
121
+ * Join the identity cluster — `agent@version · model · account` — with a dim
122
+ * separator, dropping empty pieces. Pieces are pre-colored by the caller so the
123
+ * same cluster reads identically across `view`, `use`, `add`, and `status`.
124
+ */
125
+ export declare function formatAgentIdentity(...parts: Array<string | null | undefined>): string;
99
126
  /**
100
127
  * Build the per-agent CLI flags for a unified reasoning effort knob.
101
128
  *
@@ -10,8 +10,11 @@
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { execFileSync } from 'child_process';
13
- import { getVersionDir } from './versions.js';
13
+ import chalk from 'chalk';
14
+ import { getVersionDir, getVersionHomePath } from './versions.js';
14
15
  import { getModelsCachePath } from './state.js';
16
+ import { agentConfigDirName } from './agents.js';
17
+ import { resolveRunDefaults } from './run-defaults.js';
15
18
  const CACHE_PATH = getModelsCachePath();
16
19
  /**
17
20
  * Bump when the extractor logic changes shape in an incompatible way so cached
@@ -864,6 +867,56 @@ export function resolveEffectiveModel(agent, version, requested) {
864
867
  const def = catalog.models.find((m) => m.isDefault);
865
868
  return def?.id ?? null;
866
869
  }
870
+ /**
871
+ * The model a given agent+version is actually configured to use right now, with
872
+ * where that selection comes from. First hit wins:
873
+ * 1. run-default — the user's agents-cli `run.defaults` in agents.yaml
874
+ * 2. config — the agent's OWN native settings.json `model` field
875
+ * 3. cli-default — the CLI's built-in default: the catalog's `isDefault` model
876
+ * if one is flagged (e.g. Kimi), otherwise the literal
877
+ * `default` for a model-capable agent whose runtime picks its
878
+ * own default (Claude/Codex don't flag one — Claude's own UI
879
+ * calls this "Default").
880
+ * Each layer is a real source the agent consults; `version` must be concrete.
881
+ * Returns null only when the agent exposes no model catalog at all.
882
+ */
883
+ export function resolveConfiguredModel(agent, version) {
884
+ const runModel = resolveRunDefaults(agent, version).model;
885
+ if (runModel && runModel.trim() !== '')
886
+ return { model: runModel, source: 'run-default' };
887
+ const nativeModel = readNativeConfigModel(agent, version);
888
+ if (nativeModel)
889
+ return { model: nativeModel, source: 'config' };
890
+ const catalog = getModelCatalog(agent, version);
891
+ if (catalog) {
892
+ const flagged = catalog.models.find((m) => m.isDefault);
893
+ return { model: flagged?.id ?? 'default', source: 'cli-default' };
894
+ }
895
+ return null;
896
+ }
897
+ /**
898
+ * Best-effort read of the agent's own `model` from its native settings.json
899
+ * (e.g. `~/.agents/.history/versions/claude/<ver>/home/.claude/settings.json`).
900
+ * A missing/malformed file is a fall-through, not an error.
901
+ */
902
+ function readNativeConfigModel(agent, version) {
903
+ try {
904
+ const settingsPath = path.join(getVersionHomePath(agent, version), agentConfigDirName(agent), 'settings.json');
905
+ const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
906
+ return typeof parsed.model === 'string' && parsed.model.trim() !== '' ? parsed.model : null;
907
+ }
908
+ catch {
909
+ return null;
910
+ }
911
+ }
912
+ /**
913
+ * Join the identity cluster — `agent@version · model · account` — with a dim
914
+ * separator, dropping empty pieces. Pieces are pre-colored by the caller so the
915
+ * same cluster reads identically across `view`, `use`, `add`, and `status`.
916
+ */
917
+ export function formatAgentIdentity(...parts) {
918
+ return parts.filter((p) => !!p && p.length > 0).join(` ${chalk.gray('·')} `);
919
+ }
867
920
  /** Find the closest matching model ids/aliases using edit distance. */
868
921
  function pickSuggestions(requested, catalog) {
869
922
  const all = [...catalog.models.map((m) => m.id), ...Object.keys(catalog.aliases)];
@@ -21,6 +21,10 @@ export interface OverdueJob {
21
21
  /** Return every enabled, recurring job whose most recent expected fire was
22
22
  * missed. One-shot jobs are excluded — they fire at most once. */
23
23
  export declare function detectOverdueJobs(now?: Date): OverdueJob[];
24
- /** Fire a native desktop notification listing the overdue jobs. Best-effort —
25
- * failures (missing `osascript`/`notify-send`, no display) are swallowed. */
24
+ /**
25
+ * Fire a branded desktop notification listing the overdue jobs. Routed through
26
+ * the MenubarHelper companion (notify-desktop.ts) so it carries the agents-cli
27
+ * mark; clicking opens the runs folder (~/.agents/.history/runs). Best-effort —
28
+ * a missing notifier or absent display is swallowed and never crashes the daemon.
29
+ */
26
30
  export declare function notifyOverdue(jobs: OverdueJob[]): void;
@@ -12,9 +12,8 @@
12
12
  * `agents routines catchup` command that runs them on demand.
13
13
  */
14
14
  import { Cron } from 'croner';
15
- import * as os from 'os';
16
- import { spawn } from 'child_process';
17
15
  import { listJobs, getLatestRun, jobRunsOnThisDevice } from './routines.js';
16
+ import { notifyDesktop } from './menubar/notify-desktop.js';
18
17
  // Tolerance between "expected fire" and "recorded run start" — accounts for
19
18
  // the small gap between the cron tick and when the runner writes meta.json.
20
19
  const GRACE_MS = 60_000;
@@ -76,44 +75,19 @@ export function detectOverdueJobs(now = new Date()) {
76
75
  }
77
76
  return overdue;
78
77
  }
79
- /** Fire a native desktop notification listing the overdue jobs. Best-effort —
80
- * failures (missing `osascript`/`notify-send`, no display) are swallowed. */
78
+ /**
79
+ * Fire a branded desktop notification listing the overdue jobs. Routed through
80
+ * the MenubarHelper companion (notify-desktop.ts) so it carries the agents-cli
81
+ * mark; clicking opens the runs folder (~/.agents/.history/runs). Best-effort —
82
+ * a missing notifier or absent display is swallowed and never crashes the daemon.
83
+ */
81
84
  export function notifyOverdue(jobs) {
82
85
  if (jobs.length === 0)
83
86
  return;
84
- const title = jobs.length === 1
85
- ? `Routine overdue: ${jobs[0].name}`
86
- : `${jobs.length} routines overdue`;
87
+ const title = jobs.length === 1 ? 'Routine overdue' : `${jobs.length} routines overdue`;
88
+ const subtitle = jobs.length === 1 ? jobs[0].name : undefined;
87
89
  const body = jobs.length === 1
88
90
  ? `Missed ${jobs[0].expectedAt.toLocaleString()}. Run: agents routines catchup`
89
91
  : `${jobs.map((j) => j.name).join(', ')} — agents routines catchup`;
90
- const platform = os.platform();
91
- try {
92
- if (platform === 'darwin') {
93
- const safeTitle = title.replace(/"/g, '\\"');
94
- const safeBody = body.replace(/"/g, '\\"');
95
- const child = spawn('osascript', ['-e', `display notification "${safeBody}" with title "${safeTitle}"`], { detached: true, stdio: 'ignore' });
96
- // A missing binary surfaces as an async 'error' event, NOT a synchronous
97
- // throw the try/catch would catch. Without a listener Node re-throws it as
98
- // an uncaught exception and takes the whole daemon down. Swallow it — the
99
- // notification is best-effort.
100
- child.on('error', () => { });
101
- child.unref();
102
- }
103
- else if (platform === 'linux') {
104
- const child = spawn('notify-send', [title, body], {
105
- detached: true,
106
- stdio: 'ignore',
107
- });
108
- // Headless Linux boxes have no `notify-send` (libnotify-bin); its ENOENT
109
- // arrives as an async 'error' event, not a throw. Without this listener
110
- // the daemon crashes on every overdue routine and systemd restart-loops it
111
- // (which also tears down the browser IPC socket). Swallow — best-effort.
112
- child.on('error', () => { });
113
- child.unref();
114
- }
115
- }
116
- catch {
117
- // Notification is best-effort; nothing to do.
118
- }
92
+ notifyDesktop({ title, subtitle, body, action: 'routines:list' });
119
93
  }
@@ -343,7 +343,10 @@ export function dynamicPicker(config) {
343
343
  const [loading, setLoading] = useState(true);
344
344
  const [query, setQuery] = useState('');
345
345
  const [mode, setMode] = useState('nav');
346
- const [previewOpen, setPreviewOpen] = useState(false);
346
+ // Default the preview pane open when the caller supplies a preview builder —
347
+ // matches the static `itemPicker`, so the session browser shows a live preview
348
+ // as you arrow through rows instead of hiding it behind `tab`.
349
+ const [previewOpen, setPreviewOpen] = useState(Boolean(cfg.buildPreview));
347
350
  const [active, setActive] = useState(0);
348
351
  const [flash, setFlash] = useState('');
349
352
  const prefix = usePrefix({ status, theme });
@@ -49,3 +49,20 @@ export declare function backgroundSpawnOptions(opts?: {
49
49
  * throw from `process.kill(pid, 0)` as "not running".
50
50
  */
51
51
  export declare function isAlive(pid: number): boolean;
52
+ /**
53
+ * A stable identifier for the process at `pid` as of when it started, or null if
54
+ * unknowable. Used to defeat PID reuse: acting on a pid is only safe when the
55
+ * process still occupies the slot we observed earlier. The value is only ever
56
+ * compared for equality against an earlier capture of the SAME pid, so the format
57
+ * need only be stable, not parseable.
58
+ *
59
+ * Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
60
+ * macOS: `ps -o lstart= -p <pid>`.
61
+ * Windows: CreationDate from Win32_Process, as a culture-independent FILETIME.
62
+ *
63
+ * This is the single source of truth. `pty-server.ts` and `teams/agents.ts` each
64
+ * carried their own copy; the Windows branch was missing from both, so every
65
+ * caller there silently ran with NO pid-reuse protection — including
66
+ * `agents teams stop`, which is how it could SIGKILL an unrelated process group.
67
+ */
68
+ export declare function captureProcessStartTime(pid: number): string | null;
@@ -2,6 +2,7 @@
2
2
  * Process liveness / control, platform-aware.
3
3
  */
4
4
  import { execFileSync } from 'child_process';
5
+ import { readFileSync } from 'fs';
5
6
  /**
6
7
  * Forcefully terminate a process AND its descendant tree.
7
8
  *
@@ -80,3 +81,72 @@ export function isAlive(pid) {
80
81
  return false;
81
82
  }
82
83
  }
84
+ /** Memoized per pid: a process's start time cannot change while it lives, and a
85
+ * recycled pid is precisely what the caller is trying to detect — so a stale
86
+ * hit still compares unequal against the recorded value. Bounded by the pids a
87
+ * single CLI invocation asks about. */
88
+ const startTimeByPid = new Map();
89
+ /**
90
+ * A stable identifier for the process at `pid` as of when it started, or null if
91
+ * unknowable. Used to defeat PID reuse: acting on a pid is only safe when the
92
+ * process still occupies the slot we observed earlier. The value is only ever
93
+ * compared for equality against an earlier capture of the SAME pid, so the format
94
+ * need only be stable, not parseable.
95
+ *
96
+ * Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
97
+ * macOS: `ps -o lstart= -p <pid>`.
98
+ * Windows: CreationDate from Win32_Process, as a culture-independent FILETIME.
99
+ *
100
+ * This is the single source of truth. `pty-server.ts` and `teams/agents.ts` each
101
+ * carried their own copy; the Windows branch was missing from both, so every
102
+ * caller there silently ran with NO pid-reuse protection — including
103
+ * `agents teams stop`, which is how it could SIGKILL an unrelated process group.
104
+ */
105
+ export function captureProcessStartTime(pid) {
106
+ if (!Number.isInteger(pid) || pid <= 0)
107
+ return null;
108
+ const cached = startTimeByPid.get(pid);
109
+ if (cached !== undefined)
110
+ return cached;
111
+ const value = readProcessStartTime(pid);
112
+ startTimeByPid.set(pid, value);
113
+ return value;
114
+ }
115
+ function readProcessStartTime(pid) {
116
+ try {
117
+ if (process.platform === 'win32') {
118
+ // ToFileTimeUtc() rather than the raw DateTime: the default string form is
119
+ // rendered in the current culture, so a persisted fingerprint would stop
120
+ // comparing equal across a locale change.
121
+ const out = execFileSync('powershell.exe', [
122
+ '-NoProfile',
123
+ '-NonInteractive',
124
+ '-Command',
125
+ `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToFileTimeUtc()`,
126
+ ], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, timeout: 5000 });
127
+ const trimmed = out.trim();
128
+ return trimmed.length > 0 ? trimmed : null;
129
+ }
130
+ if (process.platform === 'linux') {
131
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
132
+ // The comm field (#2) is parenthesized and may contain spaces, so split
133
+ // off everything after the last ')' to get a clean field list.
134
+ const lastParen = stat.lastIndexOf(')');
135
+ if (lastParen < 0)
136
+ return null;
137
+ const fields = stat.slice(lastParen + 2).split(' ');
138
+ // After comm we are at field 3; starttime is field 22, so index 19 here.
139
+ return fields[19] || null;
140
+ }
141
+ const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
142
+ encoding: 'utf-8',
143
+ stdio: ['ignore', 'pipe', 'ignore'],
144
+ timeout: 5000,
145
+ });
146
+ const trimmed = out.trim();
147
+ return trimmed.length > 0 ? trimmed : null;
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
@@ -4,7 +4,9 @@
4
4
  * Each preset bundles a host CLI, API base URL, default model, and provider
5
5
  * name so users can `agents profiles add kimi` without manual configuration.
6
6
  */
7
- // Model IDs verified against openrouter.ai/api/v1/models on 2026-04-20.
7
+ // Model IDs verified against openrouter.ai/api/v1/models on 2026-07-31
8
+ // (spark presets corrected from the never-served meta/claude-spark-1.1 to the
9
+ // live meta/muse-spark-1.1, confirmed on both OpenRouter and OpenCode id:meta).
8
10
  // Presets target the top-ranked open-source model per provider based on
9
11
  // SWE-bench Verified, LiveCodeBench, HumanEval, and Chatbot Arena rankings.
10
12
  //
@@ -100,18 +102,18 @@ export const PRESETS = [
100
102
  },
101
103
  {
102
104
  name: 'claude-spark',
103
- description: 'Meta Claude Spark 1.1 via OpenRouter inside Claude Code (open alternative). Model: meta/claude-spark-1.1 — free via opencode, now usable in Claude Code UI. HEADLESS-SAFE. For open-claude spark usage.',
105
+ description: 'Meta Muse Spark 1.1 via OpenRouter inside Claude Code (open alternative). Model: meta/muse-spark-1.1 — now usable in the Claude Code UI. HEADLESS-SAFE. For open-claude spark usage.',
104
106
  ...OPENROUTER_AUTH,
105
107
  env: {
106
108
  ANTHROPIC_BASE_URL: OPENROUTER_BASE,
107
- ANTHROPIC_MODEL: 'meta/claude-spark-1.1',
108
- ANTHROPIC_SMALL_FAST_MODEL: 'meta/claude-spark-1.1',
109
+ ANTHROPIC_MODEL: 'meta/muse-spark-1.1',
110
+ ANTHROPIC_SMALL_FAST_MODEL: 'meta/muse-spark-1.1',
109
111
  },
110
112
  },
111
113
  // ----- OpenCode CLI (open-claude harness) -----
112
114
  {
113
115
  name: 'opencode',
114
- description: 'OpenCode default — uses your configured model via opencode auth. Run `opencode auth` to login, then `agents run opencode --model meta/claude-spark-1.1 "prompt"` for spark usage.',
116
+ description: 'OpenCode default — uses your configured model via opencode auth. Run `opencode auth` to login, then `agents run opencode --model meta/muse-spark-1.1 "prompt"` for spark usage.',
115
117
  provider: 'opencode',
116
118
  host: 'opencode',
117
119
  authEnvVar: 'OPENCODE_API_KEY',
@@ -120,13 +122,13 @@ export const PRESETS = [
120
122
  },
121
123
  {
122
124
  name: 'opencode-spark',
123
- description: 'Meta Claude Spark 1.1 via OpenCode (free, headless-safe). Pinned to meta/claude-spark-1.1 — best for open-claude usage with opencode harness.',
125
+ description: 'Meta Muse Spark 1.1 via OpenCode (free, headless-safe). Pinned to meta/muse-spark-1.1 — best for open-claude usage with opencode harness.',
124
126
  provider: 'opencode',
125
127
  host: 'opencode',
126
128
  authEnvVar: 'OPENCODE_API_KEY',
127
129
  authOptional: true,
128
130
  env: {
129
- OPENCODE_MODEL: 'meta/claude-spark-1.1',
131
+ OPENCODE_MODEL: 'meta/muse-spark-1.1',
130
132
  },
131
133
  },
132
134
  {
@@ -19,6 +19,13 @@ export interface Profile {
19
19
  envVar: string;
20
20
  keychainItem: string;
21
21
  };
22
+ /**
23
+ * When true, the host manages its own login and the keychain token named by
24
+ * `auth` is optional: if it is not stored, no auth env var is injected and the
25
+ * run proceeds on the host's own credentials (e.g. `opencode auth`). Without
26
+ * this flag a missing keychain item is a hard error at exec time.
27
+ */
28
+ authOptional?: boolean;
22
29
  description?: string;
23
30
  preset?: string;
24
31
  provider?: string;
@@ -92,6 +99,30 @@ export declare function profileSummary(profile: Profile): ProfileSummary;
92
99
  * for the OpenRouter key exactly once.
93
100
  */
94
101
  export declare function profileFromPreset(profileName: string, preset: Preset, version?: string): Profile;
102
+ /** Return the model-override env var for a host (known hosts mapped; else `<HOST>_MODEL`). */
103
+ export declare function modelEnvKeyForHost(host: AgentId): string;
104
+ /** Return the base-URL env var for a host, or null when the host has no known override var. */
105
+ export declare function baseUrlEnvKeyForHost(host: AgentId): string | null;
106
+ /** Return the auth-token env var for a host, or null when unknown. */
107
+ export declare function authEnvKeyForHost(host: AgentId): string | null;
108
+ /** Options for {@link profileFromHostModel}. */
109
+ export interface HostModelOptions {
110
+ version?: string;
111
+ /** Base URL for OpenAI/Anthropic-compatible hosts (claude, codex). Ignored for hosts without a known var. */
112
+ baseUrl?: string;
113
+ /** Provider label + keychain namespace; only needed when the host requires a token. */
114
+ provider?: string;
115
+ /** Env var the host reads its auth token from; pair with `provider` to attach keychain auth. */
116
+ authEnvVar?: string;
117
+ description?: string;
118
+ }
119
+ /**
120
+ * Build a custom-harness profile from a host CLI + model in one shot, without a
121
+ * preset. The model lands on the host's model env var ({@link modelEnvKeyForHost});
122
+ * auth is attached only when both `provider` and `authEnvVar` are supplied
123
+ * (hosts that manage their own login — e.g. opencode — need neither).
124
+ */
125
+ export declare function profileFromHostModel(name: string, host: AgentId, model: string, opts?: HostModelOptions): Profile;
95
126
  /**
96
127
  * Resolve a profile into the env block that should be injected into the
97
128
  * spawned agent process. Reads the token from keychain at exec time so the