@phnx-labs/agents-cli 1.20.74 → 1.20.76
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 +893 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +31 -3
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +58 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +1 -32
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +37 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
* local run. The `[host/<name>]` label mirrors the cloud path's
|
|
13
13
|
* `[cloud/<status>]` convention.
|
|
14
14
|
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
15
16
|
import { upsertSession } from '../session/db.js';
|
|
16
17
|
import { SESSION_AGENTS } from '../session/types.js';
|
|
18
|
+
import { localLogPath, updateTask } from './tasks.js';
|
|
19
|
+
import { parseSessionIdMarker } from './session-marker.js';
|
|
20
|
+
import { deriveShortId } from '../session/short-id.js';
|
|
17
21
|
/**
|
|
18
22
|
* Build the SessionMeta for a host-dispatched run. Returns null when the run has
|
|
19
23
|
* no captured session id (nothing stable to key/resume on) or its agent isn't a
|
|
@@ -27,7 +31,7 @@ export function hostSessionMeta(task, ctx) {
|
|
|
27
31
|
return null;
|
|
28
32
|
return {
|
|
29
33
|
id,
|
|
30
|
-
shortId: id
|
|
34
|
+
shortId: deriveShortId(id),
|
|
31
35
|
agent: task.agent,
|
|
32
36
|
timestamp: task.createdAt,
|
|
33
37
|
cwd: ctx.cwd,
|
|
@@ -57,6 +61,37 @@ export function registerHostSession(task, ctx) {
|
|
|
57
61
|
/* index write is best-effort; the run is already live on the host */
|
|
58
62
|
}
|
|
59
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Relate a remote-created session id back to a followed host dispatch.
|
|
66
|
+
*
|
|
67
|
+
* The remote run (dispatched with `--emit-session-id`) prints its resolved id as
|
|
68
|
+
* a stdout sentinel (see session-marker.ts) that rides the followed log into the
|
|
69
|
+
* task's local mirror. Read that mirror, parse the id, and stamp it on the task
|
|
70
|
+
* so `findTaskBySessionId` and the session-index registration work for agents
|
|
71
|
+
* that never take a forced `--session-id` — closing the gap where every
|
|
72
|
+
* non-Claude host run was orphaned.
|
|
73
|
+
*
|
|
74
|
+
* Returns the updated task when an id was captured (and differs from what's on
|
|
75
|
+
* the record), else null. A task that already carries an id (Claude's forced id,
|
|
76
|
+
* a resume) is left untouched — the marker only fills a genuinely empty slot, so
|
|
77
|
+
* it can never overwrite an authoritative id with a stale echo. Best-effort: a
|
|
78
|
+
* missing/unreadable mirror or absent marker yields null, never an exception.
|
|
79
|
+
*/
|
|
80
|
+
export function captureRemoteSessionId(task) {
|
|
81
|
+
if (task.sessionId)
|
|
82
|
+
return null;
|
|
83
|
+
let text;
|
|
84
|
+
try {
|
|
85
|
+
text = fs.readFileSync(localLogPath(task.id), 'utf8');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null; // no local mirror (unfollowed run, or read raced the follow)
|
|
89
|
+
}
|
|
90
|
+
const captured = parseSessionIdMarker(text);
|
|
91
|
+
if (!captured)
|
|
92
|
+
return null;
|
|
93
|
+
return updateTask(task.id, { sessionId: captured });
|
|
94
|
+
}
|
|
60
95
|
/**
|
|
61
96
|
* Register an interactive host run (no prompt, TTY forwarded over SSH) in the
|
|
62
97
|
* local session index. Unlike detached host runs, there is no remote log/exit
|
|
@@ -69,7 +104,7 @@ export function registerInteractiveHostSession(ctx) {
|
|
|
69
104
|
try {
|
|
70
105
|
upsertSession({
|
|
71
106
|
id: ctx.sessionId,
|
|
72
|
-
shortId: ctx.sessionId
|
|
107
|
+
shortId: deriveShortId(ctx.sessionId),
|
|
73
108
|
agent: ctx.agent,
|
|
74
109
|
timestamp: ctx.createdAt ?? new Date().toISOString(),
|
|
75
110
|
cwd: ctx.cwd,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relate a remote-created session id back to the launching agent.
|
|
3
|
+
*
|
|
4
|
+
* A host dispatch (`agents run --host`) launches `agents run` on the remote box.
|
|
5
|
+
* Only Claude accepts a forced `--session-id`, so for every OTHER agent the
|
|
6
|
+
* remote run coins its OWN session id (via its SessionStart hook) and the
|
|
7
|
+
* launcher never learns it — the run is orphaned in `agents sessions`.
|
|
8
|
+
*
|
|
9
|
+
* This module closes that gap without an extra SSH round-trip: the dispatch
|
|
10
|
+
* forwards `--emit-session-id`, which makes the remote `agents run` print its
|
|
11
|
+
* resolved session id as a stable one-line sentinel to stdout. That line rides
|
|
12
|
+
* the same combined log the follow already mirrors locally, so the launcher
|
|
13
|
+
* parses the id straight out of the streamed output and stamps it on the task.
|
|
14
|
+
*
|
|
15
|
+
* The marker is a fixed ASCII frame with the id on its own line. A real session
|
|
16
|
+
* id (`[A-Za-z0-9._-]`) never contains the marker's own bytes, so the parser can
|
|
17
|
+
* scan for the LAST occurrence and stay robust to an agent echoing the token in
|
|
18
|
+
* its own output.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The single line the remote run prints to stdout so the launcher can capture
|
|
22
|
+
* the session id the remote coined. Newline-framed on both sides so it lands on
|
|
23
|
+
* its own line regardless of what the agent last wrote.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sessionIdMarkerLine(sessionId: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Extract the session id from a chunk of followed remote output, or null when no
|
|
28
|
+
* marker is present. Scans for the LAST marker (so an id echoed into the log
|
|
29
|
+
* earlier can never mask the real trailing sentinel) and validates the captured
|
|
30
|
+
* token against the session-id charset — a malformed frame yields null rather
|
|
31
|
+
* than a bogus id that could never match a real session.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseSessionIdMarker(text: string): string | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relate a remote-created session id back to the launching agent.
|
|
3
|
+
*
|
|
4
|
+
* A host dispatch (`agents run --host`) launches `agents run` on the remote box.
|
|
5
|
+
* Only Claude accepts a forced `--session-id`, so for every OTHER agent the
|
|
6
|
+
* remote run coins its OWN session id (via its SessionStart hook) and the
|
|
7
|
+
* launcher never learns it — the run is orphaned in `agents sessions`.
|
|
8
|
+
*
|
|
9
|
+
* This module closes that gap without an extra SSH round-trip: the dispatch
|
|
10
|
+
* forwards `--emit-session-id`, which makes the remote `agents run` print its
|
|
11
|
+
* resolved session id as a stable one-line sentinel to stdout. That line rides
|
|
12
|
+
* the same combined log the follow already mirrors locally, so the launcher
|
|
13
|
+
* parses the id straight out of the streamed output and stamps it on the task.
|
|
14
|
+
*
|
|
15
|
+
* The marker is a fixed ASCII frame with the id on its own line. A real session
|
|
16
|
+
* id (`[A-Za-z0-9._-]`) never contains the marker's own bytes, so the parser can
|
|
17
|
+
* scan for the LAST occurrence and stay robust to an agent echoing the token in
|
|
18
|
+
* its own output.
|
|
19
|
+
*/
|
|
20
|
+
const MARKER_PREFIX = '@@AGENTS_SESSION_ID ';
|
|
21
|
+
const MARKER_SUFFIX = '@@';
|
|
22
|
+
/** Only characters a real agent session id can hold — no marker bytes, no spaces. */
|
|
23
|
+
const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
|
|
24
|
+
/**
|
|
25
|
+
* The single line the remote run prints to stdout so the launcher can capture
|
|
26
|
+
* the session id the remote coined. Newline-framed on both sides so it lands on
|
|
27
|
+
* its own line regardless of what the agent last wrote.
|
|
28
|
+
*/
|
|
29
|
+
export function sessionIdMarkerLine(sessionId) {
|
|
30
|
+
return `\n${MARKER_PREFIX}${sessionId}${MARKER_SUFFIX}\n`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extract the session id from a chunk of followed remote output, or null when no
|
|
34
|
+
* marker is present. Scans for the LAST marker (so an id echoed into the log
|
|
35
|
+
* earlier can never mask the real trailing sentinel) and validates the captured
|
|
36
|
+
* token against the session-id charset — a malformed frame yields null rather
|
|
37
|
+
* than a bogus id that could never match a real session.
|
|
38
|
+
*/
|
|
39
|
+
export function parseSessionIdMarker(text) {
|
|
40
|
+
const last = text.lastIndexOf(MARKER_PREFIX);
|
|
41
|
+
if (last === -1)
|
|
42
|
+
return null;
|
|
43
|
+
const start = last + MARKER_PREFIX.length;
|
|
44
|
+
const end = text.indexOf(MARKER_SUFFIX, start);
|
|
45
|
+
if (end === -1)
|
|
46
|
+
return null;
|
|
47
|
+
const id = text.slice(start, end).trim();
|
|
48
|
+
if (!SESSION_ID_RE.test(id))
|
|
49
|
+
return null;
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
@@ -24,10 +24,14 @@ export interface HostTask {
|
|
|
24
24
|
*/
|
|
25
25
|
name?: string;
|
|
26
26
|
/**
|
|
27
|
-
* The agent session id
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* The remote run's agent session id, so `agents sessions`/resume-by-id can map
|
|
28
|
+
* a discovered session back to the host it lives on. Two sources: for Claude
|
|
29
|
+
* (the only agent that accepts `--session-id`) it's the id we FORCED at
|
|
30
|
+
* dispatch; for every other agent it's the id the remote COINED, captured from
|
|
31
|
+
* the run's stdout sentinel (`--emit-session-id`, see session-marker.ts) once
|
|
32
|
+
* the follow returns and stamped on the record via `captureRemoteSessionId`.
|
|
33
|
+
* Absent only until that capture lands — e.g. an unfollowed (`--no-follow`)
|
|
34
|
+
* non-Claude run whose id the reconcile path fills in later.
|
|
31
35
|
*/
|
|
32
36
|
sessionId?: string;
|
|
33
37
|
/** Remote paths (under the host's ~/.agents/.cache/hosts/). */
|
package/dist/lib/import.d.ts
CHANGED
|
@@ -127,10 +127,12 @@ export declare function resolvePackageDirFromBinary(binaryPath: string): string
|
|
|
127
127
|
*/
|
|
128
128
|
export declare function seedIsolatedConfigFromLocal(agentId: AgentId, version: string, opts?: {
|
|
129
129
|
withAuth?: boolean;
|
|
130
|
+
all?: boolean;
|
|
130
131
|
}): {
|
|
131
132
|
seeded: boolean;
|
|
132
133
|
from: string;
|
|
133
134
|
to: string;
|
|
134
135
|
skippedAuth: string[];
|
|
136
|
+
skippedRuntime: string[];
|
|
135
137
|
error?: string;
|
|
136
138
|
};
|
package/dist/lib/import.js
CHANGED
|
@@ -263,13 +263,35 @@ export function seedIsolatedConfigFromLocal(agentId, version, opts = {}) {
|
|
|
263
263
|
const versionHome = path.join(getVersionsDir(), agentId, version, 'home');
|
|
264
264
|
// Mirror importAgentConfig's derivation so nested config dirs (e.g. Antigravity's
|
|
265
265
|
// ~/.gemini/antigravity-cli) land where the shim expects them.
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
let dest = path.join(versionHome, path.relative(os.homedir(), configDir));
|
|
267
|
+
// Codex uses a SUN_LEN-safe CODEX_HOME: `home/.codex` is a SYMLINK to
|
|
268
|
+
// `~/.agents/.codex-homes/<version>/.codex`, because the real path is too long for a
|
|
269
|
+
// unix socket. cpSync cannot overwrite a symlink with a directory (it fails with
|
|
270
|
+
// "Cannot overwrite non-directory"), and writing beside it would put settings
|
|
271
|
+
// somewhere the agent never reads. Follow the link and seed the actual home.
|
|
272
|
+
try {
|
|
273
|
+
if (fs.lstatSync(dest).isSymbolicLink())
|
|
274
|
+
dest = fs.realpathSync(dest);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
/* not created yet — the plain path is correct */
|
|
278
|
+
}
|
|
279
|
+
const result = { seeded: false, from: configDir, to: dest, skippedAuth: [], skippedRuntime: [] };
|
|
268
280
|
if (!fs.existsSync(configDir))
|
|
269
281
|
return result;
|
|
270
282
|
// Known credential paths, relative to the config dir. `authFiles` covers the agents
|
|
271
283
|
// that declare them for cross-version carry; the rest are verified filenames for
|
|
272
284
|
// agents that do not declare any (codex `auth.json`, claude `.credentials.json`).
|
|
285
|
+
// Runtime state, not settings. Seeding a sandbox with the user's session history,
|
|
286
|
+
// logs and caches duplicated 757MB on a real machine — 349MB of `sessions` alone —
|
|
287
|
+
// for a copy that wants config. These are regenerated by the agent as it runs, and
|
|
288
|
+
// an isolated copy keeps its own; carrying them over also drags conversation history
|
|
289
|
+
// into a sandbox the user may have created precisely to keep separate.
|
|
290
|
+
const RUNTIME_PREFIXES = ['sessions', 'log', 'logs', 'cache', '.tmp', 'tmp', 'generated_images'];
|
|
291
|
+
const RUNTIME_FILES = ['history.jsonl', 'session_index.jsonl'];
|
|
292
|
+
const isRuntime = (rel) => !opts.all && (RUNTIME_FILES.includes(rel) ||
|
|
293
|
+
/\.sqlite(-shm|-wal)?$/.test(rel) ||
|
|
294
|
+
RUNTIME_PREFIXES.some((d) => rel === d || rel.startsWith(d + path.sep)));
|
|
273
295
|
const authRel = new Set([
|
|
274
296
|
...(agent.authFiles ?? []),
|
|
275
297
|
'auth.json',
|
|
@@ -282,12 +304,23 @@ export function seedIsolatedConfigFromLocal(agentId, version, opts = {}) {
|
|
|
282
304
|
const inside = agentsDir + path.sep;
|
|
283
305
|
fs.cpSync(configDir, dest, {
|
|
284
306
|
recursive: true,
|
|
307
|
+
// `force: true` is Node's default, but Bun drops it when a `filter` is supplied —
|
|
308
|
+
// existing files are then silently left alone. `dist/bin/agents` is bun-compiled,
|
|
309
|
+
// so this is a production path, not just a test artifact. State it explicitly.
|
|
310
|
+
force: true,
|
|
285
311
|
filter: (src) => {
|
|
286
312
|
const rel = path.relative(configDir, src);
|
|
287
313
|
if (!opts.withAuth && rel && (authRel.has(rel) || rel.startsWith('credentials' + path.sep))) {
|
|
288
314
|
result.skippedAuth.push(rel);
|
|
289
315
|
return false;
|
|
290
316
|
}
|
|
317
|
+
if (rel && isRuntime(rel)) {
|
|
318
|
+
// Record only the top-level name so the report stays one line per item.
|
|
319
|
+
const top = rel.split(path.sep)[0];
|
|
320
|
+
if (!result.skippedRuntime.includes(top))
|
|
321
|
+
result.skippedRuntime.push(top);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
291
324
|
// Same rule as the export path: a link into ~/.agents would dangle for a copy
|
|
292
325
|
// that is supposed to stand on its own.
|
|
293
326
|
try {
|
|
@@ -18,6 +18,26 @@
|
|
|
18
18
|
export declare function menubarDisabledByUser(): boolean;
|
|
19
19
|
/** True if the launchd plist for the menu-bar service is installed. */
|
|
20
20
|
export declare function menubarServiceInstalled(): boolean;
|
|
21
|
+
/** True when the bundle carries a signature the kernel will accept at launch. */
|
|
22
|
+
export declare function codesignVerifies(appPath: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Guarantee the installed bundle has a valid signature on THIS machine.
|
|
25
|
+
*
|
|
26
|
+
* npm's pack/extract strips the ad-hoc/linker signature the release baked into
|
|
27
|
+
* the helper, leaving `code object is not signed at all`. On macOS 26+ the
|
|
28
|
+
* kernel's code-signing monitor SIGKILLs an unsigned/invalid binary at launch
|
|
29
|
+
* (`SIGKILL (Code Signature Invalid)`), so under the launchd `KeepAlive` service
|
|
30
|
+
* an ad-hoc release helper crash-loops forever and its unstable identity makes
|
|
31
|
+
* the Accessibility grant (needed for the clip→paste keystroke in Clip.swift)
|
|
32
|
+
* re-prompt every time. A fresh ad-hoc re-sign gives the on-disk bytes a
|
|
33
|
+
* matching cdhash, which the kernel accepts.
|
|
34
|
+
*
|
|
35
|
+
* A Developer-ID-signed helper survives npm untouched — its embedded signature
|
|
36
|
+
* still verifies — so we leave it alone and only re-sign when verification
|
|
37
|
+
* fails. Returns whether the bundle ends up validly signed. No-op cost on the
|
|
38
|
+
* common (already-valid) path is a single `codesign --verify`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function ensureValidSignature(appPath: string): boolean;
|
|
21
41
|
/**
|
|
22
42
|
* Copy the bundled `.app` to the stable user path (idempotent unless forced).
|
|
23
43
|
* Returns the installed executable path, or null if no source bundle ships
|
|
@@ -135,6 +135,37 @@ function copyAppBundle(src, dest) {
|
|
|
135
135
|
throw new Error(`Failed to copy ${src} -> ${dest}: ${msg || 'unknown error'}`);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
/** True when the bundle carries a signature the kernel will accept at launch. */
|
|
139
|
+
export function codesignVerifies(appPath) {
|
|
140
|
+
const r = spawnSync('codesign', ['--verify', '--strict', appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
141
|
+
return r.status === 0;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Guarantee the installed bundle has a valid signature on THIS machine.
|
|
145
|
+
*
|
|
146
|
+
* npm's pack/extract strips the ad-hoc/linker signature the release baked into
|
|
147
|
+
* the helper, leaving `code object is not signed at all`. On macOS 26+ the
|
|
148
|
+
* kernel's code-signing monitor SIGKILLs an unsigned/invalid binary at launch
|
|
149
|
+
* (`SIGKILL (Code Signature Invalid)`), so under the launchd `KeepAlive` service
|
|
150
|
+
* an ad-hoc release helper crash-loops forever and its unstable identity makes
|
|
151
|
+
* the Accessibility grant (needed for the clip→paste keystroke in Clip.swift)
|
|
152
|
+
* re-prompt every time. A fresh ad-hoc re-sign gives the on-disk bytes a
|
|
153
|
+
* matching cdhash, which the kernel accepts.
|
|
154
|
+
*
|
|
155
|
+
* A Developer-ID-signed helper survives npm untouched — its embedded signature
|
|
156
|
+
* still verifies — so we leave it alone and only re-sign when verification
|
|
157
|
+
* fails. Returns whether the bundle ends up validly signed. No-op cost on the
|
|
158
|
+
* common (already-valid) path is a single `codesign --verify`.
|
|
159
|
+
*/
|
|
160
|
+
export function ensureValidSignature(appPath) {
|
|
161
|
+
if (codesignVerifies(appPath))
|
|
162
|
+
return true;
|
|
163
|
+
// Drop any quarantine/xattrs the tarball round-trip added (they can break
|
|
164
|
+
// codesign), then re-sign ad-hoc under the helper's stable bundle identifier.
|
|
165
|
+
spawnSync('xattr', ['-cr', appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
166
|
+
spawnSync('codesign', ['--force', '--sign', '-', '--identifier', SERVICE_LABEL, appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
167
|
+
return codesignVerifies(appPath);
|
|
168
|
+
}
|
|
138
169
|
/**
|
|
139
170
|
* Copy the bundled `.app` to the stable user path (idempotent unless forced).
|
|
140
171
|
* Returns the installed executable path, or null if no source bundle ships
|
|
@@ -147,9 +178,14 @@ export function ensureMenubarAppInstalled(opts = {}) {
|
|
|
147
178
|
if (!src)
|
|
148
179
|
return null;
|
|
149
180
|
const dest = installedAppPath();
|
|
150
|
-
if (!opts.forceReinstall && fs.existsSync(dest))
|
|
181
|
+
if (!opts.forceReinstall && fs.existsSync(dest)) {
|
|
182
|
+
// Self-heal an already-installed bundle whose signature npm stripped on a
|
|
183
|
+
// prior upgrade (macOS 26+ SIGKILLs it otherwise) without a forced recopy.
|
|
184
|
+
ensureValidSignature(dest);
|
|
151
185
|
return installedExecutablePath();
|
|
186
|
+
}
|
|
152
187
|
copyAppBundle(src, dest);
|
|
188
|
+
ensureValidSignature(dest);
|
|
153
189
|
return installedExecutablePath();
|
|
154
190
|
}
|
|
155
191
|
function xmlEscape(s) {
|
|
@@ -241,6 +277,14 @@ export function enableMenubarService(opts = { clearOptOut: true }) {
|
|
|
241
277
|
const exec = ensureMenubarAppInstalled({ forceReinstall: true });
|
|
242
278
|
if (!exec)
|
|
243
279
|
return false;
|
|
280
|
+
// Never bootstrap a helper the kernel will kill on launch: an invalid
|
|
281
|
+
// signature under launchd KeepAlive is an infinite crash loop. If the bundle
|
|
282
|
+
// can't be made valid (re-sign already attempted in ensureMenubarAppInstalled),
|
|
283
|
+
// skip the service rather than spin the loop.
|
|
284
|
+
if (!codesignVerifies(installedAppPath())) {
|
|
285
|
+
process.stderr.write('agents: menu-bar helper has no valid code signature; skipping launch to avoid a crash loop.\n');
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
244
288
|
if (opts.clearOptOut) {
|
|
245
289
|
try {
|
|
246
290
|
fs.rmSync(disabledSentinelPath(), { force: true });
|
package/dist/lib/migrate.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/migrate.js
CHANGED
|
@@ -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
|
package/dist/lib/models.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/lib/models.js
CHANGED
|
@@ -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
|
|
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)];
|
package/dist/lib/overdue.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ 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
|
-
/**
|
|
25
|
-
*
|
|
24
|
+
/**
|
|
25
|
+
* Fire a native desktop notification. Best-effort — failures (missing
|
|
26
|
+
* `osascript`/`notify-send`, no display, headless box) are swallowed so a
|
|
27
|
+
* notification attempt can never take the daemon down.
|
|
28
|
+
*/
|
|
29
|
+
export declare function notifyDesktop(title: string, body: string): void;
|
|
30
|
+
/** Fire a native desktop notification listing the overdue jobs. Best-effort. */
|
|
26
31
|
export declare function notifyOverdue(jobs: OverdueJob[]): void;
|
package/dist/lib/overdue.js
CHANGED
|
@@ -76,17 +76,12 @@ export function detectOverdueJobs(now = new Date()) {
|
|
|
76
76
|
}
|
|
77
77
|
return overdue;
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
? `Routine overdue: ${jobs[0].name}`
|
|
86
|
-
: `${jobs.length} routines overdue`;
|
|
87
|
-
const body = jobs.length === 1
|
|
88
|
-
? `Missed ${jobs[0].expectedAt.toLocaleString()}. Run: agents routines catchup`
|
|
89
|
-
: `${jobs.map((j) => j.name).join(', ')} — agents routines catchup`;
|
|
79
|
+
/**
|
|
80
|
+
* Fire a native desktop notification. Best-effort — failures (missing
|
|
81
|
+
* `osascript`/`notify-send`, no display, headless box) are swallowed so a
|
|
82
|
+
* notification attempt can never take the daemon down.
|
|
83
|
+
*/
|
|
84
|
+
export function notifyDesktop(title, body) {
|
|
90
85
|
const platform = os.platform();
|
|
91
86
|
try {
|
|
92
87
|
if (platform === 'darwin') {
|
|
@@ -117,3 +112,15 @@ export function notifyOverdue(jobs) {
|
|
|
117
112
|
// Notification is best-effort; nothing to do.
|
|
118
113
|
}
|
|
119
114
|
}
|
|
115
|
+
/** Fire a native desktop notification listing the overdue jobs. Best-effort. */
|
|
116
|
+
export function notifyOverdue(jobs) {
|
|
117
|
+
if (jobs.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
const title = jobs.length === 1
|
|
120
|
+
? `Routine overdue: ${jobs[0].name}`
|
|
121
|
+
: `${jobs.length} routines overdue`;
|
|
122
|
+
const body = jobs.length === 1
|
|
123
|
+
? `Missed ${jobs[0].expectedAt.toLocaleString()}. Run: agents routines catchup`
|
|
124
|
+
: `${jobs.map((j) => j.name).join(', ')} — agents routines catchup`;
|
|
125
|
+
notifyDesktop(title, body);
|
|
126
|
+
}
|
package/dist/lib/picker.js
CHANGED
|
@@ -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
|
-
|
|
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;
|