@phnx-labs/agents-cli 1.20.73 → 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 +989 -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 +31 -1
- package/dist/commands/exec.js +31 -3
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- 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 +87 -9
- 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/setup.js +6 -1
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +51 -11
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +99 -31
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -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 +56 -0
- package/dist/lib/agents.js +133 -7
- 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.d.ts +16 -0
- package/dist/lib/config-transfer.js +60 -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/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- 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 +25 -0
- package/dist/lib/import.js +107 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- 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/project-resources.js +20 -5
- 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 +27 -1
- package/dist/lib/shims.js +88 -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 +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +15 -36
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +48 -2
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +22 -2
- package/dist/lib/versions.js +98 -10
- 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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ActivityEvent } from './activity.js';
|
|
2
|
+
import { type PidSessionEntry } from './session/pid-registry.js';
|
|
3
|
+
/** Soft cap so a runaway agent can't flood the activity lane with essays. */
|
|
4
|
+
export declare const STATUS_POST_MAX_CHARS = 500;
|
|
5
|
+
export interface FeedPostInput {
|
|
6
|
+
/** Human-readable progress text (required). Domain-agnostic free text. */
|
|
7
|
+
text: string;
|
|
8
|
+
/** Override session id (escape hatch for scripts/tests). Prefer auto-resolve. */
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
/** Override activity root (tests). */
|
|
11
|
+
activityRoot?: string;
|
|
12
|
+
/** Override env for identity resolution (tests). */
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
/** Override cwd stamp (defaults to process.cwd() / env). */
|
|
15
|
+
cwd?: string;
|
|
16
|
+
/** Fixed timestamp (tests). */
|
|
17
|
+
ts?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Starting pid for registry ancestor walk (defaults to process.ppid so the
|
|
20
|
+
* walk begins at the parent of this CLI process — usually the agent/shell).
|
|
21
|
+
* Tests inject a fake pid here.
|
|
22
|
+
*/
|
|
23
|
+
startPid?: number;
|
|
24
|
+
/** Override parent-pid lookup (tests). */
|
|
25
|
+
getParentPid?: (pid: number) => number | undefined;
|
|
26
|
+
/** Override registry read (tests). */
|
|
27
|
+
readEntry?: (pid: number) => PidSessionEntry | undefined;
|
|
28
|
+
/** Override full registry list for launchId match (tests). */
|
|
29
|
+
listEntries?: () => PidSessionEntry[];
|
|
30
|
+
}
|
|
31
|
+
export interface FeedPostResult {
|
|
32
|
+
event: ActivityEvent;
|
|
33
|
+
}
|
|
34
|
+
export interface PostIdentity {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
mailboxId: string;
|
|
37
|
+
host: string;
|
|
38
|
+
runtime: string;
|
|
39
|
+
agent?: string;
|
|
40
|
+
cwd?: string;
|
|
41
|
+
pid?: number;
|
|
42
|
+
launchId?: string;
|
|
43
|
+
terminalId?: string;
|
|
44
|
+
tmuxPane?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve who is posting. Order:
|
|
48
|
+
* 1. Explicit --session flag
|
|
49
|
+
* 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
|
|
50
|
+
* 3. Env AGENT_LAUNCH_ID → match in pid registry
|
|
51
|
+
* 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolvePostIdentity(input: Pick<FeedPostInput, 'sessionId' | 'env' | 'cwd' | 'startPid' | 'getParentPid' | 'readEntry' | 'listEntries'>): PostIdentity | undefined;
|
|
54
|
+
/** Walk up to 16 ancestors looking for a by-pid registry entry with a session. */
|
|
55
|
+
export declare function walkPidRegistry(startPid: number, getParent: (pid: number) => number | undefined, readEntry: (pid: number) => PidSessionEntry | undefined): PidSessionEntry | undefined;
|
|
56
|
+
/** Best-effort parent pid of `pid` (Linux /proc, else `ps`). */
|
|
57
|
+
export declare function parentPidOf(pid: number): number | undefined;
|
|
58
|
+
export declare function normalizeStatusText(text: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Append a `status.posted` milestone for the calling agent.
|
|
61
|
+
* Throws if text is empty or session identity cannot be resolved.
|
|
62
|
+
*/
|
|
63
|
+
export declare function postFeedStatus(input: FeedPostInput): FeedPostResult;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent status posts — deliberate progress messages into the activity stream.
|
|
3
|
+
*
|
|
4
|
+
* Surface: `agents feed post <text>` (agent-callable; humans watch via
|
|
5
|
+
* `agents feed` / `agents activity` / `agents events --module activity`).
|
|
6
|
+
*
|
|
7
|
+
* Identity is automatic: session id, agent, cwd, launch/pid/tmux provenance
|
|
8
|
+
* are resolved from the process environment and the per-pid launch registry
|
|
9
|
+
* (`lib/session/pid-registry.ts`). The agent only authors free-form text —
|
|
10
|
+
* no domain-specific flags (tickets, URLs, tracks).
|
|
11
|
+
*
|
|
12
|
+
* Storage: append-only activity log as a `status.posted` milestone. Does NOT
|
|
13
|
+
* open a feed block (blocks remain "needs you" state only).
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { spawnSync } from 'child_process';
|
|
18
|
+
import { appendActivityEvent, } from './activity.js';
|
|
19
|
+
import { machineId } from './machine-id.js';
|
|
20
|
+
import { isValidMailboxId } from './mailbox.js';
|
|
21
|
+
import { listPidSessionEntries, readPidSessionEntry, } from './session/pid-registry.js';
|
|
22
|
+
/** Soft cap so a runaway agent can't flood the activity lane with essays. */
|
|
23
|
+
export const STATUS_POST_MAX_CHARS = 500;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve who is posting. Order:
|
|
26
|
+
* 1. Explicit --session flag
|
|
27
|
+
* 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
|
|
28
|
+
* 3. Env AGENT_LAUNCH_ID → match in pid registry
|
|
29
|
+
* 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
|
|
30
|
+
*/
|
|
31
|
+
export function resolvePostIdentity(input) {
|
|
32
|
+
const env = input.env ?? process.env;
|
|
33
|
+
const readEntry = input.readEntry ?? readPidSessionEntry;
|
|
34
|
+
const listEntries = input.listEntries ?? listPidSessionEntries;
|
|
35
|
+
const getParent = input.getParentPid ?? parentPidOf;
|
|
36
|
+
const envSession = firstValidId([
|
|
37
|
+
input.sessionId,
|
|
38
|
+
env.AGENT_SESSION_ID,
|
|
39
|
+
env.AGENTS_SESSION_ID,
|
|
40
|
+
mailboxIdFromEnv(env),
|
|
41
|
+
]);
|
|
42
|
+
const launchId = env.AGENT_LAUNCH_ID?.trim() || undefined;
|
|
43
|
+
let registry;
|
|
44
|
+
if (launchId) {
|
|
45
|
+
registry = listEntries().find((e) => e.launchId === launchId);
|
|
46
|
+
}
|
|
47
|
+
if (!registry) {
|
|
48
|
+
const start = input.startPid ?? (typeof process.ppid === 'number' ? process.ppid : undefined);
|
|
49
|
+
if (start && start > 1) {
|
|
50
|
+
registry = walkPidRegistry(start, getParent, readEntry);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Prefer env session (explicit + managed run), fill gaps from registry.
|
|
54
|
+
const sessionId = envSession ?? registry?.sessionId;
|
|
55
|
+
if (!sessionId || !isValidMailboxId(sessionId))
|
|
56
|
+
return undefined;
|
|
57
|
+
const mailboxFromEnv = mailboxIdFromEnv(env);
|
|
58
|
+
const mailboxId = mailboxFromEnv && isValidMailboxId(mailboxFromEnv)
|
|
59
|
+
? mailboxFromEnv
|
|
60
|
+
: sessionId;
|
|
61
|
+
return {
|
|
62
|
+
sessionId,
|
|
63
|
+
mailboxId,
|
|
64
|
+
host: machineIdFromEnv(env),
|
|
65
|
+
runtime: env.AGENTS_RUNTIME?.trim() || 'headless',
|
|
66
|
+
agent: env.AGENTS_AGENT_NAME?.trim()
|
|
67
|
+
|| registry?.agent
|
|
68
|
+
|| detectAgentKind(env),
|
|
69
|
+
cwd: input.cwd
|
|
70
|
+
?? (env.AGENTS_CWD?.trim() || registry?.cwd || process.cwd()),
|
|
71
|
+
pid: registry?.pid,
|
|
72
|
+
launchId: launchId || registry?.launchId,
|
|
73
|
+
terminalId: env.AGENT_TERMINAL_ID?.trim() || registry?.terminalId,
|
|
74
|
+
tmuxPane: env.TMUX_PANE?.trim() || registry?.tmuxPane,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function firstValidId(candidates) {
|
|
78
|
+
for (const raw of candidates) {
|
|
79
|
+
const id = (raw ?? '').trim();
|
|
80
|
+
if (id && isValidMailboxId(id))
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
function mailboxIdFromEnv(env) {
|
|
86
|
+
const dir = env.AGENTS_MAILBOX_DIR?.trim();
|
|
87
|
+
if (!dir)
|
|
88
|
+
return undefined;
|
|
89
|
+
const base = path.basename(dir.replace(/[/\\]+$/, ''));
|
|
90
|
+
return base || undefined;
|
|
91
|
+
}
|
|
92
|
+
/** Walk up to 16 ancestors looking for a by-pid registry entry with a session. */
|
|
93
|
+
export function walkPidRegistry(startPid, getParent, readEntry) {
|
|
94
|
+
let pid = startPid;
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
let firstHit;
|
|
97
|
+
for (let i = 0; i < 16 && pid && pid > 1 && !seen.has(pid); i++) {
|
|
98
|
+
seen.add(pid);
|
|
99
|
+
const entry = readEntry(pid);
|
|
100
|
+
if (entry) {
|
|
101
|
+
if (entry.sessionId && isValidMailboxId(entry.sessionId))
|
|
102
|
+
return entry;
|
|
103
|
+
if (!firstHit)
|
|
104
|
+
firstHit = entry;
|
|
105
|
+
}
|
|
106
|
+
pid = getParent(pid);
|
|
107
|
+
}
|
|
108
|
+
// Entry without sessionId still carries agent/cwd/launchId — usable when
|
|
109
|
+
// session id comes from env.
|
|
110
|
+
return firstHit;
|
|
111
|
+
}
|
|
112
|
+
/** Best-effort parent pid of `pid` (Linux /proc, else `ps`). */
|
|
113
|
+
export function parentPidOf(pid) {
|
|
114
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (process.platform === 'linux') {
|
|
117
|
+
try {
|
|
118
|
+
const status = fs.readFileSync(`/proc/${pid}/status`, 'utf8');
|
|
119
|
+
const m = status.match(/^PPid:\s*(\d+)/m);
|
|
120
|
+
if (m) {
|
|
121
|
+
const pp = Number(m[1]);
|
|
122
|
+
return Number.isInteger(pp) && pp > 0 ? pp : undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* fall through */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const r = spawnSync('ps', ['-o', 'ppid=', '-p', String(pid)], {
|
|
131
|
+
encoding: 'utf8',
|
|
132
|
+
timeout: 1000,
|
|
133
|
+
});
|
|
134
|
+
if (r.status === 0) {
|
|
135
|
+
const pp = Number((r.stdout || '').trim());
|
|
136
|
+
return Number.isInteger(pp) && pp > 0 ? pp : undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* best-effort */
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
export function normalizeStatusText(text) {
|
|
145
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
146
|
+
if (!collapsed)
|
|
147
|
+
return '';
|
|
148
|
+
if (collapsed.length <= STATUS_POST_MAX_CHARS)
|
|
149
|
+
return collapsed;
|
|
150
|
+
return `${collapsed.slice(0, STATUS_POST_MAX_CHARS - 1)}…`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Append a `status.posted` milestone for the calling agent.
|
|
154
|
+
* Throws if text is empty or session identity cannot be resolved.
|
|
155
|
+
*/
|
|
156
|
+
export function postFeedStatus(input) {
|
|
157
|
+
const detail = normalizeStatusText(input.text);
|
|
158
|
+
if (!detail) {
|
|
159
|
+
throw new Error('Status text is empty. Usage: agents feed post "what just happened"');
|
|
160
|
+
}
|
|
161
|
+
const identity = resolvePostIdentity(input);
|
|
162
|
+
if (!identity) {
|
|
163
|
+
throw new Error('No session id. Run from an agents-cli session '
|
|
164
|
+
+ '(AGENT_SESSION_ID / AGENTS_MAILBOX_DIR / pid registry), or pass --session <id>.');
|
|
165
|
+
}
|
|
166
|
+
const event = {
|
|
167
|
+
ts: input.ts ?? new Date().toISOString(),
|
|
168
|
+
event: 'status.posted',
|
|
169
|
+
sessionId: identity.sessionId,
|
|
170
|
+
mailboxId: identity.mailboxId,
|
|
171
|
+
host: identity.host,
|
|
172
|
+
runtime: identity.runtime,
|
|
173
|
+
cwd: identity.cwd,
|
|
174
|
+
agent: identity.agent,
|
|
175
|
+
tool: 'feed.post',
|
|
176
|
+
detail,
|
|
177
|
+
...(identity.pid !== undefined ? { pid: identity.pid } : {}),
|
|
178
|
+
...(identity.launchId ? { launchId: identity.launchId } : {}),
|
|
179
|
+
...(identity.terminalId ? { terminalId: identity.terminalId } : {}),
|
|
180
|
+
...(identity.tmuxPane ? { tmuxPane: identity.tmuxPane } : {}),
|
|
181
|
+
};
|
|
182
|
+
appendActivityEvent(event, input.activityRoot);
|
|
183
|
+
return {
|
|
184
|
+
event: {
|
|
185
|
+
v: 1,
|
|
186
|
+
tier: 'milestone',
|
|
187
|
+
...event,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function machineIdFromEnv(env) {
|
|
192
|
+
const raw = env.AGENTS_SYNC_MACHINE_ID || undefined;
|
|
193
|
+
if (raw) {
|
|
194
|
+
return raw.split('.')[0].trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-') || 'unknown';
|
|
195
|
+
}
|
|
196
|
+
return machineId();
|
|
197
|
+
}
|
|
198
|
+
function detectAgentKind(env) {
|
|
199
|
+
if (env.CLAUDECODE === '1')
|
|
200
|
+
return 'claude';
|
|
201
|
+
if (env.CODEX_CI || env.CODEX_HOME)
|
|
202
|
+
return 'codex';
|
|
203
|
+
return 'agent';
|
|
204
|
+
}
|
|
@@ -11,6 +11,30 @@ import type { DeviceProfile } from '../devices/registry.js';
|
|
|
11
11
|
import type { DeviceDesired, DeviceProbe, DeviceDiff, FleetAction, FleetPlan, AuthFilePayload } from './types.js';
|
|
12
12
|
/** Strip a version suffix from an agent spec: `claude@latest` -> `claude`. */
|
|
13
13
|
export declare function agentIdOf(spec: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* The explicit pinned version of a spec, or undefined for an id-level spec.
|
|
16
|
+
* `claude@2.1.170` -> `2.1.170`; `claude`, `claude@latest`, `claude@oldest`, and
|
|
17
|
+
* `claude@all` all return undefined (the label channels install-latest / are
|
|
18
|
+
* expanded upstream, so they diff at id granularity, not per-version).
|
|
19
|
+
*/
|
|
20
|
+
export declare function pinnedVersion(spec: string): string | undefined;
|
|
21
|
+
/** True when any spec in the roster pins an explicit version (needs a version probe). */
|
|
22
|
+
export declare function rosterNeedsVersions(desired: DeviceDesired[]): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Expand any `<agent>@all` spec into one pinned spec per version installed on the
|
|
25
|
+
* source, so `--agent claude@all` replicates THIS machine's exact version set.
|
|
26
|
+
* `versionsOf` returns the source's installed versions for an agent id. Every
|
|
27
|
+
* other spec passes through unchanged; the result is de-duplicated in order.
|
|
28
|
+
* Throws if `@all` names an agent with no installed versions here (nothing to
|
|
29
|
+
* replicate — a clear misconfig, not a silent no-op).
|
|
30
|
+
*/
|
|
31
|
+
export declare function expandAllSpecs(specs: string[], versionsOf: (id: string) => string[]): string[];
|
|
32
|
+
/**
|
|
33
|
+
* Parse `agents view --json` (the all-agents array form) into a map of agent id
|
|
34
|
+
* -> installed version strings. Tolerant: returns undefined on any parse failure
|
|
35
|
+
* so a version-pinned spec falls back to id-level presence rather than crashing.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseInstalledVersions(stdout: string): Record<string, string[]> | undefined;
|
|
14
38
|
/** Source-side auth availability, computed once from `snapshotAuth`. */
|
|
15
39
|
export interface SourceAuth {
|
|
16
40
|
/** Agent ids the source has a readable, propagatable credential file for. */
|
|
@@ -37,8 +61,14 @@ export interface DiffContext {
|
|
|
37
61
|
}
|
|
38
62
|
/** Pure: desired vs probed -> per-device diff + flat action list. */
|
|
39
63
|
export declare function diffFleet(desired: DeviceDesired[], probes: Map<string, DeviceProbe>, ctx: DiffContext): FleetPlan;
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
export interface ProbeOptions {
|
|
65
|
+
/** Also fetch per-agent installed versions (one extra `agents view --json`
|
|
66
|
+
* round-trip). Enable only when the plan has a version-pinned spec. */
|
|
67
|
+
withVersions?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** Probe one device: reachability + agents-cli version + installed agent ids
|
|
70
|
+
* (and, when `withVersions`, the installed version strings per agent). */
|
|
71
|
+
export declare function probeDevice(device: DeviceProfile, opts?: ProbeOptions): DeviceProbe;
|
|
42
72
|
export interface ApplyStep {
|
|
43
73
|
kind: FleetAction['kind'];
|
|
44
74
|
ok: boolean;
|
package/dist/lib/fleet/apply.js
CHANGED
|
@@ -17,6 +17,76 @@ import { isPropagatableAgent, KEYCHAIN_BOUND_ON_MAC, buildAuthBundle, } from './
|
|
|
17
17
|
export function agentIdOf(spec) {
|
|
18
18
|
return spec.split('@')[0].trim();
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* The explicit pinned version of a spec, or undefined for an id-level spec.
|
|
22
|
+
* `claude@2.1.170` -> `2.1.170`; `claude`, `claude@latest`, `claude@oldest`, and
|
|
23
|
+
* `claude@all` all return undefined (the label channels install-latest / are
|
|
24
|
+
* expanded upstream, so they diff at id granularity, not per-version).
|
|
25
|
+
*/
|
|
26
|
+
export function pinnedVersion(spec) {
|
|
27
|
+
const at = spec.indexOf('@');
|
|
28
|
+
if (at < 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
const v = spec.slice(at + 1).trim();
|
|
31
|
+
if (!v || v === 'latest' || v === 'oldest' || v === 'all')
|
|
32
|
+
return undefined;
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
/** True when any spec in the roster pins an explicit version (needs a version probe). */
|
|
36
|
+
export function rosterNeedsVersions(desired) {
|
|
37
|
+
return desired.some((d) => d.agents.some((s) => pinnedVersion(s) !== undefined));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Expand any `<agent>@all` spec into one pinned spec per version installed on the
|
|
41
|
+
* source, so `--agent claude@all` replicates THIS machine's exact version set.
|
|
42
|
+
* `versionsOf` returns the source's installed versions for an agent id. Every
|
|
43
|
+
* other spec passes through unchanged; the result is de-duplicated in order.
|
|
44
|
+
* Throws if `@all` names an agent with no installed versions here (nothing to
|
|
45
|
+
* replicate — a clear misconfig, not a silent no-op).
|
|
46
|
+
*/
|
|
47
|
+
export function expandAllSpecs(specs, versionsOf) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const spec of specs) {
|
|
50
|
+
const at = spec.indexOf('@');
|
|
51
|
+
const label = at >= 0 ? spec.slice(at + 1).trim() : '';
|
|
52
|
+
if (label !== 'all') {
|
|
53
|
+
out.push(spec);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const id = agentIdOf(spec);
|
|
57
|
+
const versions = versionsOf(id);
|
|
58
|
+
if (versions.length === 0) {
|
|
59
|
+
throw new Error(`--agent ${spec}: no ${id} versions installed on this machine to replicate.`);
|
|
60
|
+
}
|
|
61
|
+
for (const v of versions)
|
|
62
|
+
out.push(`${id}@${v}`);
|
|
63
|
+
}
|
|
64
|
+
return [...new Set(out)];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse `agents view --json` (the all-agents array form) into a map of agent id
|
|
68
|
+
* -> installed version strings. Tolerant: returns undefined on any parse failure
|
|
69
|
+
* so a version-pinned spec falls back to id-level presence rather than crashing.
|
|
70
|
+
*/
|
|
71
|
+
export function parseInstalledVersions(stdout) {
|
|
72
|
+
try {
|
|
73
|
+
const arr = JSON.parse(stdout);
|
|
74
|
+
if (!Array.isArray(arr))
|
|
75
|
+
return undefined;
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const a of arr) {
|
|
78
|
+
if (a && typeof a.agent === 'string' && Array.isArray(a.versions)) {
|
|
79
|
+
out[a.agent] = a.versions
|
|
80
|
+
.map((v) => v?.version)
|
|
81
|
+
.filter((v) => typeof v === 'string');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
20
90
|
/**
|
|
21
91
|
* Can we propagate `agent`'s login to a target on `targetPlatform`? False when
|
|
22
92
|
* the agent has no portable file, the source can't provide it (bound / not
|
|
@@ -53,21 +123,28 @@ export function diffFleet(desired, probes, ctx) {
|
|
|
53
123
|
else if (probe.cliVersion !== ctx.targetCliVersion) {
|
|
54
124
|
rowActions.push({ device: d.device, kind: 'upgrade-cli', detail: `agents-cli ${probe.cliVersion} -> ${ctx.targetCliVersion}` });
|
|
55
125
|
}
|
|
56
|
-
// agents.
|
|
126
|
+
// agents. A version-pinned spec (`claude@2.1.170`, or an expanded
|
|
127
|
+
// `claude@all` member) is present only when that exact version is on the
|
|
128
|
+
// device; a bare/latest spec diffs at id granularity. So `--agent claude@all`
|
|
129
|
+
// installs every missing version even when some claude is already there.
|
|
57
130
|
for (const spec of d.agents) {
|
|
58
131
|
const id = agentIdOf(spec);
|
|
59
|
-
|
|
60
|
-
|
|
132
|
+
const want = pinnedVersion(spec);
|
|
133
|
+
const present = want !== undefined
|
|
134
|
+
? (probe.installedVersions?.[id]?.includes(want) ?? false)
|
|
135
|
+
: probe.installedAgents.includes(id);
|
|
136
|
+
if (!present) {
|
|
137
|
+
rowActions.push({ device: d.device, kind: 'add-agent', agent: id, spec, detail: `install ${spec}` });
|
|
61
138
|
}
|
|
62
139
|
}
|
|
63
140
|
// config.
|
|
64
141
|
if (d.sync.length > 0) {
|
|
65
142
|
rowActions.push({ device: d.device, kind: 'sync-config', detail: `sync config (${d.sync.join(', ')})` });
|
|
66
143
|
}
|
|
67
|
-
// login
|
|
144
|
+
// login — per agent id, not per spec: `claude@all` names one id many times,
|
|
145
|
+
// but login propagates once per agent (its credential is version-shared).
|
|
68
146
|
if (d.login === 'sync') {
|
|
69
|
-
for (const
|
|
70
|
-
const id = agentIdOf(spec);
|
|
147
|
+
for (const id of [...new Set(d.agents.map(agentIdOf))]) {
|
|
71
148
|
if (canPushLogin(id, probe.platform, ctx.sourceAuth)) {
|
|
72
149
|
rowActions.push({ device: d.device, kind: 'push-login', agent: id, detail: `propagate ${id} login` });
|
|
73
150
|
}
|
|
@@ -107,8 +184,9 @@ function osHint(platform) {
|
|
|
107
184
|
function remoteEnv(platform) {
|
|
108
185
|
return platform === 'windows' ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
109
186
|
}
|
|
110
|
-
/** Probe one device: reachability + agents-cli version + installed agent ids
|
|
111
|
-
|
|
187
|
+
/** Probe one device: reachability + agents-cli version + installed agent ids
|
|
188
|
+
* (and, when `withVersions`, the installed version strings per agent). */
|
|
189
|
+
export function probeDevice(device, opts) {
|
|
112
190
|
let target;
|
|
113
191
|
try {
|
|
114
192
|
target = sshTargetFor(device);
|
|
@@ -133,12 +211,20 @@ export function probeDevice(device) {
|
|
|
133
211
|
/* agents-cli present but doctor output unparsable — treat as no agents */
|
|
134
212
|
}
|
|
135
213
|
}
|
|
214
|
+
let installedVersions;
|
|
215
|
+
if (opts?.withVersions) {
|
|
216
|
+
const viewCmd = buildRemoteAgentsInvocation(['view', '--json'], undefined, hint, remoteEnv(device.platform));
|
|
217
|
+
const vres = sshExec(target, viewCmd, { timeoutMs: 30000, multiplex: true });
|
|
218
|
+
if (vres.code === 0)
|
|
219
|
+
installedVersions = parseInstalledVersions(vres.stdout);
|
|
220
|
+
}
|
|
136
221
|
return {
|
|
137
222
|
device: device.name,
|
|
138
223
|
reachable: true,
|
|
139
224
|
platform: device.platform,
|
|
140
225
|
cliVersion: ready.version ?? undefined,
|
|
141
226
|
installedAgents: installed,
|
|
227
|
+
installedVersions,
|
|
142
228
|
};
|
|
143
229
|
}
|
|
144
230
|
/** Execute one device's planned actions in order. Real SSH — no mocks. */
|
|
@@ -165,9 +251,10 @@ export function reconcileDevice(row, device, ctx) {
|
|
|
165
251
|
steps.push({ kind: cliAction.kind, ok: r.ok, detail: cliAction.detail });
|
|
166
252
|
ok = ok && r.ok;
|
|
167
253
|
}
|
|
168
|
-
// 2. agents.
|
|
254
|
+
// 2. agents. Every add-agent action carries the full spec (set in diffFleet);
|
|
255
|
+
// install it directly rather than re-parsing the human-readable detail string.
|
|
169
256
|
for (const a of row.actions.filter((x) => x.kind === 'add-agent')) {
|
|
170
|
-
const spec = a.
|
|
257
|
+
const spec = a.spec;
|
|
171
258
|
const r = sshAgents(['add', spec, '--yes']);
|
|
172
259
|
steps.push({ kind: 'add-agent', ok: r.code === 0, detail: a.detail });
|
|
173
260
|
ok = ok && r.code === 0;
|
|
@@ -87,6 +87,14 @@ export interface DeviceProbe {
|
|
|
87
87
|
cliVersion?: string;
|
|
88
88
|
/** Agent ids currently installed on the device. */
|
|
89
89
|
installedAgents: string[];
|
|
90
|
+
/**
|
|
91
|
+
* Installed version strings per agent id (e.g. `{ claude: ['2.1.170', '2.1.207'] }`),
|
|
92
|
+
* parsed from `agents view --json` on the device. Only populated when the plan
|
|
93
|
+
* involves a version-pinned spec (`claude@2.1.170`, or a `claude@all` expansion)
|
|
94
|
+
* — a bare `claude`/`claude@latest` roster never pays for the extra probe. When
|
|
95
|
+
* undefined, version-pinned specs fall back to id-level presence.
|
|
96
|
+
*/
|
|
97
|
+
installedVersions?: Record<string, string[]>;
|
|
90
98
|
/** Reason string when `reachable` is false or the probe partially failed. */
|
|
91
99
|
note?: string;
|
|
92
100
|
}
|
|
@@ -100,6 +108,9 @@ export interface FleetAction {
|
|
|
100
108
|
kind: FleetActionKind;
|
|
101
109
|
/** Agent id for agent/login actions; undefined for cli/config actions. */
|
|
102
110
|
agent?: string;
|
|
111
|
+
/** Full agent spec for `add-agent` (e.g. `claude@2.1.170`) so the plan can show
|
|
112
|
+
* the exact version being installed; equals the id for a bare/latest spec. */
|
|
113
|
+
spec?: string;
|
|
103
114
|
/** Human, one-line description of the action. */
|
|
104
115
|
detail: string;
|
|
105
116
|
}
|
package/dist/lib/fs-walk.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/** A file surfaced by the walk, carrying the mtime+size from the walk's own stat. */
|
|
2
|
+
export interface WalkedFile {
|
|
3
|
+
path: string;
|
|
4
|
+
mtimeMs: number;
|
|
5
|
+
size: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Walk a directory recursively for files with a given extension, newest first,
|
|
9
|
+
* keeping each match's mtime and size from the walk's own stat. Callers that
|
|
10
|
+
* then compare against the scan ledger reuse these instead of re-stat'ing every
|
|
11
|
+
* path — eliminating a second stat per file on the Codex/Droid/routine scans.
|
|
12
|
+
*/
|
|
13
|
+
export declare function walkForFilesWithStat(dir: string, ext: string, limit: number): WalkedFile[];
|
|
1
14
|
/** Walk a directory recursively for files with a given extension, newest first. */
|
|
2
15
|
export declare function walkForFiles(dir: string, ext: string, limit: number): string[];
|
|
3
16
|
/**
|
package/dist/lib/fs-walk.js
CHANGED
|
@@ -36,20 +36,29 @@ function walkEntries(dir, ext, onFile) {
|
|
|
36
36
|
else if (entry.name.endsWith(ext)) {
|
|
37
37
|
const stat = safeStatSync(full);
|
|
38
38
|
if (stat)
|
|
39
|
-
onFile(full, stat.mtimeMs);
|
|
39
|
+
onFile(full, stat.mtimeMs, stat.size);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
walk(dir, 0);
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Walk a directory recursively for files with a given extension, newest first,
|
|
47
|
+
* keeping each match's mtime and size from the walk's own stat. Callers that
|
|
48
|
+
* then compare against the scan ledger reuse these instead of re-stat'ing every
|
|
49
|
+
* path — eliminating a second stat per file on the Codex/Droid/routine scans.
|
|
50
|
+
*/
|
|
51
|
+
export function walkForFilesWithStat(dir, ext, limit) {
|
|
47
52
|
const results = [];
|
|
48
|
-
walkEntries(dir, ext, (filePath, mtimeMs) => {
|
|
49
|
-
results.push({ path: filePath,
|
|
53
|
+
walkEntries(dir, ext, (filePath, mtimeMs, size) => {
|
|
54
|
+
results.push({ path: filePath, mtimeMs, size });
|
|
50
55
|
});
|
|
51
|
-
results.sort((a, b) => b.
|
|
52
|
-
return results.slice(0, limit)
|
|
56
|
+
results.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
57
|
+
return results.slice(0, limit);
|
|
58
|
+
}
|
|
59
|
+
/** Walk a directory recursively for files with a given extension, newest first. */
|
|
60
|
+
export function walkForFiles(dir, ext, limit) {
|
|
61
|
+
return walkForFilesWithStat(dir, ext, limit).map(r => r.path);
|
|
53
62
|
}
|
|
54
63
|
/**
|
|
55
64
|
* Return the newest mtime (ms) among files with the given extension, or null
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -139,6 +139,7 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
139
139
|
export declare function parseHookManifest(opts?: {
|
|
140
140
|
warn?: boolean;
|
|
141
141
|
}): Record<string, ManifestHook>;
|
|
142
|
+
export declare function selectHookManifest(manifest: Record<string, ManifestHook>, selected: string[]): Record<string, ManifestHook>;
|
|
142
143
|
/**
|
|
143
144
|
* Hook script files present on disk that no manifest entry declares — "dead"
|
|
144
145
|
* hooks. The registrar only wires manifest-declared hooks into an agent's
|