@phnx-labs/agents-cli 1.20.76 → 1.20.78
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/CHANGELOG.md +264 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +143 -9
- package/dist/commands/exec.js +87 -19
- package/dist/commands/repo.js +8 -0
- package/dist/commands/routines.js +275 -15
- package/dist/commands/secrets.js +65 -17
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +128 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-picker.d.ts +28 -1
- package/dist/commands/sessions-picker.js +222 -15
- package/dist/commands/sessions.d.ts +117 -4
- package/dist/commands/sessions.js +350 -62
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +48 -3
- package/dist/index.js +5 -6
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/agents.d.ts +0 -21
- package/dist/lib/agents.js +0 -37
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +9 -32
- package/dist/lib/daemon.js +70 -84
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +27 -4
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/git.d.ts +13 -0
- package/dist/lib/git.js +102 -4
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hosts/dispatch.d.ts +9 -0
- package/dist/lib/hosts/dispatch.js +24 -4
- package/dist/lib/hosts/passthrough.js +7 -2
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +33 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +4 -0
- package/dist/lib/hosts/run-target.js +5 -1
- package/dist/lib/hosts/session-index.js +3 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +9 -0
- package/dist/lib/menubar/install-menubar.js +14 -0
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/overdue.d.ts +4 -5
- package/dist/lib/overdue.js +8 -41
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +68 -0
- package/dist/lib/routines.js +74 -7
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +156 -28
- package/dist/lib/sandbox.js +0 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +11 -4
- package/dist/lib/secrets/agent.js +46 -24
- package/dist/lib/secrets/bundles.d.ts +21 -7
- package/dist/lib/secrets/bundles.js +82 -38
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- package/dist/lib/secrets/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +16 -2
- package/dist/lib/secrets/session-store.js +118 -15
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/session/active.d.ts +17 -0
- package/dist/lib/session/active.js +105 -7
- package/dist/lib/session/db.d.ts +43 -0
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +28 -3
- package/dist/lib/session/discover.js +110 -14
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/parse.js +11 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.js +18 -16
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +94 -12
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/state.d.ts +7 -3
- package/dist/lib/state.js +20 -5
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a remote-created session id back to an interactive `--host` launcher.
|
|
3
|
+
*
|
|
4
|
+
* A headless `--host` run captures the remote-coined id from the followed log
|
|
5
|
+
* (the `--emit-session-id` marker — see session-marker.ts). An INTERACTIVE run
|
|
6
|
+
* has no followed log: the local TTY is wired straight through `sshStream`
|
|
7
|
+
* (stdio:'inherit'), so nothing can tap the stream for a marker without breaking
|
|
8
|
+
* the agent's raw-mode TUI. Instead we correlate by AGENT_LAUNCH_ID.
|
|
9
|
+
*
|
|
10
|
+
* The launcher forwards a launch id it controls (`--env AGENT_LAUNCH_ID=<id>`);
|
|
11
|
+
* the remote `agents run` adopts it (exec.ts `resolveLaunchId`), so the remote
|
|
12
|
+
* SessionStart hook records the agent's REAL session id under that exact key in
|
|
13
|
+
* `~/.agents/.cache/terminals/sessions/<pid>.json`. After the stream returns we
|
|
14
|
+
* do ONE ssh read of that dir and pick the record whose `launch_id` matches —
|
|
15
|
+
* the same launch-id join `agents sessions --active` uses locally
|
|
16
|
+
* (session/hook-sessions.ts), just across the SSH hop.
|
|
17
|
+
*
|
|
18
|
+
* This gives the interactive path a real per-agent id to register in the local
|
|
19
|
+
* session index and to reconnect against, for Codex/Kimi/Grok/Gemini — closing
|
|
20
|
+
* the gap RUSH-2033 left for every non-Claude agent (Claude still forces its own
|
|
21
|
+
* id up front and never reaches here).
|
|
22
|
+
*/
|
|
23
|
+
import { sshExec } from '../ssh-exec.js';
|
|
24
|
+
/**
|
|
25
|
+
* Pick the real session id for `launchId` from the remote hook records.
|
|
26
|
+
*
|
|
27
|
+
* `recordsJson` is the newline-delimited JSON the remote read emits — one record
|
|
28
|
+
* per line (a `cat` of every `sessions/*.json`). Scan for the record whose
|
|
29
|
+
* `launch_id` matches and return its `session_id`; when several match (pid reuse,
|
|
30
|
+
* a lingering file) the NEWEST by `ts` wins, mirroring the local reader's
|
|
31
|
+
* keep-newest tie-break (session/hook-sessions.ts). Pure so the correlation is
|
|
32
|
+
* unit-testable from fixtures with no SSH. Returns undefined when no record
|
|
33
|
+
* carries the launch id (the hook hasn't landed yet, or a hookless harness).
|
|
34
|
+
*/
|
|
35
|
+
export function pickRemoteSessionId(recordsJson, launchId) {
|
|
36
|
+
if (!launchId)
|
|
37
|
+
return undefined;
|
|
38
|
+
let best;
|
|
39
|
+
for (const line of recordsJson.split('\n')) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
continue;
|
|
43
|
+
let rec;
|
|
44
|
+
try {
|
|
45
|
+
rec = JSON.parse(trimmed);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue; // a partial/garbled line — skip, never throw
|
|
49
|
+
}
|
|
50
|
+
if (rec.launch_id !== launchId)
|
|
51
|
+
continue;
|
|
52
|
+
if (typeof rec.session_id !== 'string' || !rec.session_id)
|
|
53
|
+
continue;
|
|
54
|
+
const ts = typeof rec.ts === 'number' ? rec.ts : 0;
|
|
55
|
+
// Strict `>` so the FIRST record wins a tie — matches keepNewest (hook-sessions.ts).
|
|
56
|
+
if (!best || ts > best.ts)
|
|
57
|
+
best = { sid: rec.session_id, ts };
|
|
58
|
+
}
|
|
59
|
+
return best?.sid;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read the remote hook-session dir over ssh and resolve the real session id the
|
|
63
|
+
* remote run recorded under `launchId`. One round-trip, best-effort: any ssh
|
|
64
|
+
* failure, a missing dir, or an absent record yields undefined — the caller then
|
|
65
|
+
* keeps the un-mapped run rather than a fabricated id.
|
|
66
|
+
*
|
|
67
|
+
* `$HOME` expands on the REMOTE login shell (never the local box). The glob is a
|
|
68
|
+
* literal path with no user input, so it is injection-safe; a `2>/dev/null` on a
|
|
69
|
+
* globless dir keeps the command quiet when the dir is empty or absent.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveRemoteSessionId(target, launchId, timeoutMs = 6000) {
|
|
72
|
+
if (!launchId)
|
|
73
|
+
return undefined;
|
|
74
|
+
// `cat` every record onto its own line. `head -c` caps a runaway file; the
|
|
75
|
+
// trailing `true` keeps the pipeline's exit 0 even when the glob matches nothing.
|
|
76
|
+
const cmd = 'cat "$HOME"/.agents/.cache/terminals/sessions/*.json 2>/dev/null | head -c 1048576 || true';
|
|
77
|
+
const res = sshExec(target, cmd, { timeoutMs, multiplex: true });
|
|
78
|
+
if (res.code !== 0 && !res.stdout)
|
|
79
|
+
return undefined;
|
|
80
|
+
// Each record is a single-line JSON object; `cat` may run them together, so
|
|
81
|
+
// normalise `}{` boundaries onto separate lines before scanning.
|
|
82
|
+
const normalised = res.stdout.replace(/\}\s*\{/g, '}\n{');
|
|
83
|
+
return pickRemoteSessionId(normalised, launchId);
|
|
84
|
+
}
|
|
@@ -45,6 +45,8 @@ export interface HostPromptRun {
|
|
|
45
45
|
name?: string;
|
|
46
46
|
/** Resume an existing session on the host by concrete id. */
|
|
47
47
|
resume?: string;
|
|
48
|
+
/** Explicit id for a new Claude session. */
|
|
49
|
+
sessionId?: string;
|
|
48
50
|
/** Working directory on the host, already made remote-portable by the caller. */
|
|
49
51
|
remoteCwd?: string;
|
|
50
52
|
/** Stream progress and block until completion (default true). */
|
|
@@ -74,6 +76,8 @@ export interface HostPromptRun {
|
|
|
74
76
|
/** Copy runtime credentials to the host before the run and shred them after. */
|
|
75
77
|
copyCreds?: HostCredentials;
|
|
76
78
|
}
|
|
79
|
+
/** Resolve the id the remote host will adopt for a fresh Claude session. */
|
|
80
|
+
export declare function resolveHostSessionId(agent: string, resume?: string, sessionId?: string): string | undefined;
|
|
77
81
|
/**
|
|
78
82
|
* Dispatch a headless prompt run onto a resolved host, then relate the run's
|
|
79
83
|
* session id back to this launcher for EVERY agent — not just Claude.
|
|
@@ -53,6 +53,10 @@ export async function resolveHostRunTarget(name, opts = {}) {
|
|
|
53
53
|
throw new HostResolutionError(`Unknown host "${name}". List hosts: agents hosts list`);
|
|
54
54
|
return host;
|
|
55
55
|
}
|
|
56
|
+
/** Resolve the id the remote host will adopt for a fresh Claude session. */
|
|
57
|
+
export function resolveHostSessionId(agent, resume, sessionId) {
|
|
58
|
+
return agent === 'claude' && !resume ? sessionId ?? randomUUID() : undefined;
|
|
59
|
+
}
|
|
56
60
|
/**
|
|
57
61
|
* Dispatch a headless prompt run onto a resolved host, then relate the run's
|
|
58
62
|
* session id back to this launcher for EVERY agent — not just Claude.
|
|
@@ -69,7 +73,7 @@ export async function resolveHostRunTarget(name, opts = {}) {
|
|
|
69
73
|
* while the run continues).
|
|
70
74
|
*/
|
|
71
75
|
export async function dispatchPromptToHost(host, opts) {
|
|
72
|
-
const forcedSessionId = opts.agent
|
|
76
|
+
const forcedSessionId = resolveHostSessionId(opts.agent, opts.resume, opts.sessionId);
|
|
73
77
|
// Ask the remote to print its resolved id whenever we did NOT force one (every
|
|
74
78
|
// non-Claude agent, and Claude-on-resume where the id is already known). No-op
|
|
75
79
|
// when the run isn't followed — nothing tails the log to catch the marker.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import { upsertSession } from '../session/db.js';
|
|
17
|
-
import {
|
|
17
|
+
import { isSessionTrackedAgent } from '../session/types.js';
|
|
18
18
|
import { localLogPath, updateTask } from './tasks.js';
|
|
19
19
|
import { parseSessionIdMarker } from './session-marker.js';
|
|
20
20
|
import { deriveShortId } from '../session/short-id.js';
|
|
@@ -27,7 +27,7 @@ export function hostSessionMeta(task, ctx) {
|
|
|
27
27
|
const id = task.sessionId;
|
|
28
28
|
if (!id)
|
|
29
29
|
return null;
|
|
30
|
-
if (!
|
|
30
|
+
if (!isSessionTrackedAgent(task.agent))
|
|
31
31
|
return null;
|
|
32
32
|
return {
|
|
33
33
|
id,
|
|
@@ -99,7 +99,7 @@ export function captureRemoteSessionId(task) {
|
|
|
99
99
|
* surface and resume it by id.
|
|
100
100
|
*/
|
|
101
101
|
export function registerInteractiveHostSession(ctx) {
|
|
102
|
-
if (!
|
|
102
|
+
if (!isSessionTrackedAgent(ctx.agent))
|
|
103
103
|
return;
|
|
104
104
|
try {
|
|
105
105
|
upsertSession({
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
<string>com.phnx-labs.agents-menubar</string>
|
|
9
9
|
<key>CFBundleName</key>
|
|
10
10
|
<string>Agents Menu Bar</string>
|
|
11
|
+
<key>CFBundleIconFile</key>
|
|
12
|
+
<string>AppIcon</string>
|
|
11
13
|
<key>CFBundlePackageType</key>
|
|
12
14
|
<string>APPL</string>
|
|
13
15
|
<key>CFBundleShortVersionString</key>
|
|
Binary file
|
|
@@ -3,9 +3,22 @@
|
|
|
3
3
|
<plist version="1.0">
|
|
4
4
|
<dict>
|
|
5
5
|
<key>files</key>
|
|
6
|
-
<dict
|
|
6
|
+
<dict>
|
|
7
|
+
<key>Resources/AppIcon.icns</key>
|
|
8
|
+
<data>
|
|
9
|
+
Vd5nfogg74zSRNspluv4uvDecUA=
|
|
10
|
+
</data>
|
|
11
|
+
</dict>
|
|
7
12
|
<key>files2</key>
|
|
8
|
-
<dict
|
|
13
|
+
<dict>
|
|
14
|
+
<key>Resources/AppIcon.icns</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>hash2</key>
|
|
17
|
+
<data>
|
|
18
|
+
DznVe0VgYOux7+B/aiHNgGYLCvSJKzgXDTPJn2jNkNg=
|
|
19
|
+
</data>
|
|
20
|
+
</dict>
|
|
21
|
+
</dict>
|
|
9
22
|
<key>rules</key>
|
|
10
23
|
<dict>
|
|
11
24
|
<key>^Resources/</key>
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
* migration (`installMenubarLaunchAgent` in migrate.ts) honors, so a disabled
|
|
15
15
|
* menu bar never silently comes back on the next release.
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Absolute path to the installed MenubarHelper executable if it exists on disk,
|
|
19
|
+
* else null. The desktop notifier (notify-desktop.ts) routes daemon
|
|
20
|
+
* notifications through this one-shot (`MenubarHelper --notify ...`) so they
|
|
21
|
+
* carry the agents-cli mark rather than the generic osascript icon. Null on
|
|
22
|
+
* non-darwin or when the helper is not installed (menu bar disabled, a Linux
|
|
23
|
+
* package, or a dev checkout without a built bundle).
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveInstalledMenubarExecutable(): string | null;
|
|
17
26
|
/** True if the user explicitly disabled the menu bar (don't auto-enable on upgrade). */
|
|
18
27
|
export declare function menubarDisabledByUser(): boolean;
|
|
19
28
|
/** True if the launchd plist for the menu-bar service is installed. */
|
|
@@ -57,6 +57,20 @@ function readInstalledMenubarVersion() {
|
|
|
57
57
|
function installedExecutablePath() {
|
|
58
58
|
return path.join(installedAppPath(), 'Contents', 'MacOS', 'MenubarHelper');
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Absolute path to the installed MenubarHelper executable if it exists on disk,
|
|
62
|
+
* else null. The desktop notifier (notify-desktop.ts) routes daemon
|
|
63
|
+
* notifications through this one-shot (`MenubarHelper --notify ...`) so they
|
|
64
|
+
* carry the agents-cli mark rather than the generic osascript icon. Null on
|
|
65
|
+
* non-darwin or when the helper is not installed (menu bar disabled, a Linux
|
|
66
|
+
* package, or a dev checkout without a built bundle).
|
|
67
|
+
*/
|
|
68
|
+
export function resolveInstalledMenubarExecutable() {
|
|
69
|
+
if (!onDarwin())
|
|
70
|
+
return null;
|
|
71
|
+
const exec = installedExecutablePath();
|
|
72
|
+
return fs.existsSync(exec) ? exec : null;
|
|
73
|
+
}
|
|
60
74
|
/** ~/Library/LaunchAgents/com.phnx-labs.agents-menubar.plist */
|
|
61
75
|
function servicePlistPath() {
|
|
62
76
|
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
|
|
@@ -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
|
+
}
|
package/dist/lib/overdue.d.ts
CHANGED
|
@@ -22,10 +22,9 @@ export interface OverdueJob {
|
|
|
22
22
|
* missed. One-shot jobs are excluded — they fire at most once. */
|
|
23
23
|
export declare function detectOverdueJobs(now?: Date): OverdueJob[];
|
|
24
24
|
/**
|
|
25
|
-
* Fire a
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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.
|
|
28
29
|
*/
|
|
29
|
-
export declare function notifyDesktop(title: string, body: string): void;
|
|
30
|
-
/** Fire a native desktop notification listing the overdue jobs. Best-effort. */
|
|
31
30
|
export declare function notifyOverdue(jobs: OverdueJob[]): void;
|
package/dist/lib/overdue.js
CHANGED
|
@@ -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;
|
|
@@ -77,50 +76,18 @@ export function detectOverdueJobs(now = new Date()) {
|
|
|
77
76
|
return overdue;
|
|
78
77
|
}
|
|
79
78
|
/**
|
|
80
|
-
* Fire a
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
83
|
*/
|
|
84
|
-
export function notifyDesktop(title, body) {
|
|
85
|
-
const platform = os.platform();
|
|
86
|
-
try {
|
|
87
|
-
if (platform === 'darwin') {
|
|
88
|
-
const safeTitle = title.replace(/"/g, '\\"');
|
|
89
|
-
const safeBody = body.replace(/"/g, '\\"');
|
|
90
|
-
const child = spawn('osascript', ['-e', `display notification "${safeBody}" with title "${safeTitle}"`], { detached: true, stdio: 'ignore' });
|
|
91
|
-
// A missing binary surfaces as an async 'error' event, NOT a synchronous
|
|
92
|
-
// throw the try/catch would catch. Without a listener Node re-throws it as
|
|
93
|
-
// an uncaught exception and takes the whole daemon down. Swallow it — the
|
|
94
|
-
// notification is best-effort.
|
|
95
|
-
child.on('error', () => { });
|
|
96
|
-
child.unref();
|
|
97
|
-
}
|
|
98
|
-
else if (platform === 'linux') {
|
|
99
|
-
const child = spawn('notify-send', [title, body], {
|
|
100
|
-
detached: true,
|
|
101
|
-
stdio: 'ignore',
|
|
102
|
-
});
|
|
103
|
-
// Headless Linux boxes have no `notify-send` (libnotify-bin); its ENOENT
|
|
104
|
-
// arrives as an async 'error' event, not a throw. Without this listener
|
|
105
|
-
// the daemon crashes on every overdue routine and systemd restart-loops it
|
|
106
|
-
// (which also tears down the browser IPC socket). Swallow — best-effort.
|
|
107
|
-
child.on('error', () => { });
|
|
108
|
-
child.unref();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// Notification is best-effort; nothing to do.
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/** Fire a native desktop notification listing the overdue jobs. Best-effort. */
|
|
116
84
|
export function notifyOverdue(jobs) {
|
|
117
85
|
if (jobs.length === 0)
|
|
118
86
|
return;
|
|
119
|
-
const title = jobs.length === 1
|
|
120
|
-
|
|
121
|
-
: `${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;
|
|
122
89
|
const body = jobs.length === 1
|
|
123
90
|
? `Missed ${jobs[0].expectedAt.toLocaleString()}. Run: agents routines catchup`
|
|
124
91
|
: `${jobs.map((j) => j.name).join(', ')} — agents routines catchup`;
|
|
125
|
-
notifyDesktop(title, body);
|
|
92
|
+
notifyDesktop({ title, subtitle, body, action: 'routines:list' });
|
|
126
93
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routine lifecycle desktop notifications (RUSH-2030).
|
|
3
|
+
*
|
|
4
|
+
* The daemon fires a branded notification when a scheduled routine starts and
|
|
5
|
+
* when it finishes (success or failure), routed through the MenubarHelper
|
|
6
|
+
* companion (notify-desktop.ts) so it carries the agents-cli mark.
|
|
7
|
+
*
|
|
8
|
+
* Anti-spam threshold (Acceptance Criteria #4 — "define a sensible threshold so
|
|
9
|
+
* users are not spammed"):
|
|
10
|
+
* - Agent / workflow routines: notify on BOTH start and finish. These are the
|
|
11
|
+
* runs whose output a user actually wants surfaced.
|
|
12
|
+
* - Command routines (deterministic housekeeping — version checks, `git pull`,
|
|
13
|
+
* notify shims that can fire every minute): notify only on FAILURE. A green
|
|
14
|
+
* housekeeping run is noise; a broken one is worth a ping. No start ping.
|
|
15
|
+
* - "Notable output" is folded into the single finish notification rather than
|
|
16
|
+
* sent as a third message: on failure the error reason, on success the first
|
|
17
|
+
* line of the run's report (the user-facing result) when the routine produced
|
|
18
|
+
* one. One start + one finish per run — never a stream.
|
|
19
|
+
*
|
|
20
|
+
* The pure builders (`routineStartNotification` / `routineFinishNotification`)
|
|
21
|
+
* return `null` when the threshold says "don't notify", and are unit-tested; the
|
|
22
|
+
* `notifyRoutine*` wrappers do the filesystem read + dispatch for the daemon.
|
|
23
|
+
*/
|
|
24
|
+
import type { JobConfig, RunMeta } from './routines.js';
|
|
25
|
+
import { type DesktopNotification } from './menubar/notify-desktop.js';
|
|
26
|
+
type RoutineKind = 'agent' | 'workflow' | 'command';
|
|
27
|
+
/** Which flavor of routine a config/meta describes — drives the notify threshold. */
|
|
28
|
+
export declare function routineKind(r: Pick<JobConfig, 'agent' | 'workflow' | 'command'>): RoutineKind;
|
|
29
|
+
/** "1m 20s" / "45s" / "2h 3m" from a millisecond duration, or null when unknown. */
|
|
30
|
+
export declare function formatDuration(ms: number | undefined): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* First non-empty line of a run report, trimmed to a notification-sized snippet.
|
|
33
|
+
* This is the "notable output" surfaced on a successful finish — the routine's
|
|
34
|
+
* own user-facing result. Returns null for an empty/whitespace report so the
|
|
35
|
+
* caller falls back to a plain "Completed" body.
|
|
36
|
+
*/
|
|
37
|
+
export declare function notableSnippet(report: string | null | undefined, maxLen?: number): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Notification for a routine START, or null when the threshold suppresses it
|
|
40
|
+
* (command-mode housekeeping). Clicking opens the runs folder
|
|
41
|
+
* (~/.agents/.history/runs).
|
|
42
|
+
*/
|
|
43
|
+
export declare function routineStartNotification(config: Pick<JobConfig, 'name' | 'agent' | 'workflow' | 'command'>): DesktopNotification | null;
|
|
44
|
+
/**
|
|
45
|
+
* Notification for a routine that failed to even START — `executeJobDetached`
|
|
46
|
+
* threw before the child was spawned, so no run record and no finish will ever
|
|
47
|
+
* exist. The daemon fires the START notification unconditionally, so this closes
|
|
48
|
+
* the "exactly one start + one finish" invariant: the orphaned start gets its
|
|
49
|
+
* matching failure banner (RUSH-2030). Unlike a green finish this is never
|
|
50
|
+
* suppressed — a broken start is worth a ping for every routine kind, including
|
|
51
|
+
* command housekeeping. Clicking opens the runs folder (~/.agents/.history/runs)
|
|
52
|
+
* since there is no run report to open.
|
|
53
|
+
*/
|
|
54
|
+
export declare function routineStartFailedNotification(config: Pick<JobConfig, 'name' | 'agent' | 'workflow' | 'command'>, error: string): DesktopNotification;
|
|
55
|
+
/**
|
|
56
|
+
* Notification for a routine FINISH, or null when the threshold suppresses it
|
|
57
|
+
* (a successful command-mode housekeeping run). Success carries the report's
|
|
58
|
+
* first line when present (the notable output); failure carries the reason.
|
|
59
|
+
* Clicking opens the run report/log when one is available, else the runs folder
|
|
60
|
+
* (~/.agents/.history/runs).
|
|
61
|
+
*/
|
|
62
|
+
export declare function routineFinishNotification(meta: Pick<RunMeta, 'jobName' | 'status' | 'exitCode' | 'errorMessage' | 'duration' | 'agent' | 'workflow' | 'command'>, opts?: {
|
|
63
|
+
report?: string | null;
|
|
64
|
+
artifactPath?: string | null;
|
|
65
|
+
}): DesktopNotification | null;
|
|
66
|
+
/** Daemon glue: fire the START notification for a triggered routine. Best-effort. */
|
|
67
|
+
export declare function notifyRoutineStart(config: JobConfig): void;
|
|
68
|
+
/**
|
|
69
|
+
* Daemon glue: fire the "failed to start" notification when a routine trigger
|
|
70
|
+
* threw before spawning a child. Pairs with the unconditional START ping so a
|
|
71
|
+
* pre-spawn failure never leaves an orphaned "Routine started". Best-effort.
|
|
72
|
+
*/
|
|
73
|
+
export declare function notifyRoutineStartFailed(config: JobConfig, error: string): void;
|
|
74
|
+
/** Daemon glue: fire the FINISH notification for a completed run. Best-effort. */
|
|
75
|
+
export declare function notifyRoutineFinish(meta: RunMeta): void;
|
|
76
|
+
export {};
|