@phnx-labs/agents-cli 1.22.31 → 1.22.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +8 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/daemon.js +52 -12
  5. package/dist/commands/doctor.d.ts +19 -0
  6. package/dist/commands/doctor.js +119 -17
  7. package/dist/commands/routines.js +164 -36
  8. package/dist/commands/sessions-browser.js +2 -2
  9. package/dist/commands/sessions.d.ts +1 -1
  10. package/dist/commands/sessions.js +66 -22
  11. package/dist/commands/update.d.ts +2 -0
  12. package/dist/commands/update.js +148 -0
  13. package/dist/index.js +3 -1
  14. package/dist/lib/catchup.js +4 -1
  15. package/dist/lib/daemon.d.ts +17 -0
  16. package/dist/lib/daemon.js +69 -3
  17. package/dist/lib/devices/doctor-findings.d.ts +7 -2
  18. package/dist/lib/devices/doctor-findings.js +53 -2
  19. package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
  20. package/dist/lib/devices/doctor-overview-cache.js +15 -0
  21. package/dist/lib/devices/fleet-divergence.d.ts +11 -0
  22. package/dist/lib/devices/fleet-divergence.js +6 -0
  23. package/dist/lib/devices/fleet-inventory.js +16 -2
  24. package/dist/lib/drift.d.ts +6 -1
  25. package/dist/lib/drift.js +9 -0
  26. package/dist/lib/hooks/cache.js +20 -1
  27. package/dist/lib/hooks.d.ts +91 -1
  28. package/dist/lib/hooks.js +289 -3
  29. package/dist/lib/hosts/passthrough.js +3 -0
  30. package/dist/lib/installations/index.d.ts +14 -0
  31. package/dist/lib/installations/index.js +14 -0
  32. package/dist/lib/installations/resolve.d.ts +43 -0
  33. package/dist/lib/installations/resolve.js +93 -0
  34. package/dist/lib/installations/store.d.ts +56 -0
  35. package/dist/lib/installations/store.js +196 -0
  36. package/dist/lib/installations/strategies.d.ts +73 -0
  37. package/dist/lib/installations/strategies.js +293 -0
  38. package/dist/lib/installations/types.d.ts +78 -0
  39. package/dist/lib/installations/types.js +8 -0
  40. package/dist/lib/installations/update.d.ts +40 -0
  41. package/dist/lib/installations/update.js +131 -0
  42. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  43. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  44. package/dist/lib/migrate.d.ts +27 -0
  45. package/dist/lib/migrate.js +112 -2
  46. package/dist/lib/routine-context.d.ts +144 -0
  47. package/dist/lib/routine-context.js +268 -0
  48. package/dist/lib/routine-readiness.d.ts +47 -0
  49. package/dist/lib/routine-readiness.js +239 -0
  50. package/dist/lib/routines.d.ts +97 -1
  51. package/dist/lib/routines.js +107 -1
  52. package/dist/lib/runner.d.ts +18 -4
  53. package/dist/lib/runner.js +291 -98
  54. package/dist/lib/scheduler.d.ts +7 -1
  55. package/dist/lib/scheduler.js +5 -2
  56. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  57. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  58. package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
  59. package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
  60. package/dist/lib/self-heal/registry.js +5 -2
  61. package/dist/lib/self-heal/types.d.ts +1 -1
  62. package/dist/lib/session/state.js +4 -1
  63. package/dist/lib/session/team-filter.d.ts +11 -0
  64. package/dist/lib/session/team-filter.js +10 -0
  65. package/dist/lib/startup/command-registry.d.ts +1 -0
  66. package/dist/lib/startup/command-registry.js +2 -0
  67. package/dist/lib/versions.d.ts +24 -0
  68. package/dist/lib/versions.js +49 -16
  69. package/package.json +2 -2
@@ -0,0 +1,93 @@
1
+ import { resolveAccountLabel } from '../account-labels.js';
2
+ import { AGENTS } from '../agents.js';
3
+ import { getGlobalDefault } from '../versions.js';
4
+ import { listInstallations } from './store.js';
5
+ /**
6
+ * Addressing a frozen installation.
7
+ *
8
+ * A selector matches either the installation's stable {@link Installation.label}
9
+ * or the vendor release it currently carries — the label because that is what
10
+ * every persisted reference and every `agents add` invocation uses, the release
11
+ * because after an update the two differ and a user reading `agents view` may
12
+ * name either. Matching both is also precisely what makes duplicate same-release
13
+ * installations addressable at all: two installs can share a release, so the
14
+ * release alone is not an identifier and the ambiguity has to be reported rather
15
+ * than silently resolved to whichever sorted first.
16
+ */
17
+ export class InstallationNotFoundError extends Error {
18
+ agent;
19
+ selector;
20
+ available;
21
+ constructor(agent, selector, available) {
22
+ // Nothing installed is a different problem from "your selector missed", and
23
+ // the remedy differs — say which one it is rather than printing an empty list.
24
+ super(available.length === 0
25
+ ? `No ${AGENTS[agent].name} installations are managed by agents-cli. Install one with: agents add ${agent}@latest`
26
+ : `No ${AGENTS[agent].name} installation matches '${selector}'. Installed: ${available.map((i) => describeInstallation(i)).join(', ')}`);
27
+ this.agent = agent;
28
+ this.selector = selector;
29
+ this.available = available;
30
+ this.name = 'InstallationNotFoundError';
31
+ }
32
+ }
33
+ export class InstallationAmbiguousError extends Error {
34
+ agent;
35
+ selector;
36
+ candidates;
37
+ constructor(agent, selector, candidates) {
38
+ super(`'${selector ?? agent}' matches ${candidates.length} ${AGENTS[agent].name} installations `
39
+ + `(${candidates.map((i) => describeInstallation(i)).join(', ')}). `
40
+ + `Name one by its installation label, or disambiguate with --account <label>.`);
41
+ this.agent = agent;
42
+ this.selector = selector;
43
+ this.candidates = candidates;
44
+ this.name = 'InstallationAmbiguousError';
45
+ }
46
+ }
47
+ /** `2.0.65` when frozen at its original release, `2.0.65 (release 2.0.71)` after an update. */
48
+ export function describeInstallation(installation) {
49
+ return installation.releaseVersion === installation.label
50
+ ? installation.label
51
+ : `${installation.label} (release ${installation.releaseVersion})`;
52
+ }
53
+ /**
54
+ * Resolve `<agent>[@<selector>]` to exactly one installation.
55
+ *
56
+ * With no selector: the agent's default installation when one is pinned, else
57
+ * the sole installation. Never a "newest wins" guess — picking for the user
58
+ * across several installs is how an update lands on the wrong one.
59
+ */
60
+ export async function resolveInstallation(agent, selector, options = {}) {
61
+ const all = listInstallations(agent);
62
+ if (all.length === 0)
63
+ throw new InstallationNotFoundError(agent, selector, all);
64
+ let candidates = all;
65
+ if (options.account) {
66
+ // resolveAccountLabel answers with the version-dir label of the install that
67
+ // is signed into that account — i.e. an installation label.
68
+ const label = await resolveAccountLabel(agent, options.account);
69
+ candidates = candidates.filter((i) => i.label === label);
70
+ if (candidates.length === 0)
71
+ throw new InstallationNotFoundError(agent, selector, all);
72
+ }
73
+ if (selector) {
74
+ const byLabel = candidates.filter((i) => i.label === selector);
75
+ // A label is unique by construction (it is a directory name), so a label hit
76
+ // is decisive and never competes with a release hit on another installation.
77
+ if (byLabel.length === 1)
78
+ return byLabel[0];
79
+ const byRelease = candidates.filter((i) => i.releaseVersion === selector);
80
+ if (byRelease.length === 1)
81
+ return byRelease[0];
82
+ if (byRelease.length > 1)
83
+ throw new InstallationAmbiguousError(agent, selector, byRelease);
84
+ throw new InstallationNotFoundError(agent, selector, all);
85
+ }
86
+ if (candidates.length === 1)
87
+ return candidates[0];
88
+ const defaultLabel = getGlobalDefault(agent);
89
+ const pinned = defaultLabel ? candidates.find((i) => i.label === defaultLabel) : undefined;
90
+ if (pinned)
91
+ return pinned;
92
+ throw new InstallationAmbiguousError(agent, selector, candidates);
93
+ }
@@ -0,0 +1,56 @@
1
+ import type { AgentId } from '../types.js';
2
+ import { type Installation } from './types.js';
3
+ /**
4
+ * Persistence for {@link Installation} records.
5
+ *
6
+ * The record lives at `<versionDir>/installation.json` rather than in one
7
+ * central index: the version dir is what `agents trash`/`agents prune` move,
8
+ * copy and restore wholesale, so keeping identity inside it means identity
9
+ * travels with the install instead of dangling in a registry that forgets to
10
+ * follow. It is also why the file name is registered in versions.ts's
11
+ * `PRESERVED_ON_CLEAN_REINSTALL` — a repair reinstall must not mint a new id.
12
+ *
13
+ * Deliberately depends on nothing but `state`/`fs-atomic`/`primitives` so
14
+ * versions.ts can import it without an import cycle.
15
+ */
16
+ /** Directory holding one installation. Mirrors versions.ts `getVersionDir`. */
17
+ export declare function installationDir(agent: AgentId, label: string): string;
18
+ export declare function installationRecordPath(agent: AgentId, label: string): string;
19
+ /** Mint an opaque installation id. Random, never derived from the release. */
20
+ export declare function mintInstallationId(): string;
21
+ /**
22
+ * Read the record for one installation, or null when the version dir has none.
23
+ * Never mints — use {@link ensureInstallation} for the migrating read.
24
+ */
25
+ export declare function readInstallation(agent: AgentId, label: string): Installation | null;
26
+ export declare function writeInstallation(installation: Installation): void;
27
+ /**
28
+ * Read the record for an existing version dir, minting and persisting one on
29
+ * first sight. This is the migration path for every installation created before
30
+ * frozen identity existed: their directory name IS their release, so the
31
+ * migrated record seeds `label === releaseVersion` and dates the install from
32
+ * the directory's own mtime rather than pretending it was created now.
33
+ *
34
+ * Throws when the version dir does not exist — an installation record must never
35
+ * describe an install that isn't there.
36
+ */
37
+ export declare function ensureInstallation(agent: AgentId, label: string): Installation;
38
+ /**
39
+ * Create the record for a freshly-installed version dir. Idempotent: a repeat
40
+ * `agents add` of the same label keeps the original id (identity is frozen) and
41
+ * only records the release if it actually moved.
42
+ */
43
+ export declare function createInstallation(agent: AgentId, label: string, releaseVersion: string): Installation;
44
+ /**
45
+ * Move an installation's recorded release forward, preserving identity. Returns
46
+ * the persisted record. Call only AFTER the new release is live on disk — the
47
+ * record is the claim that it is.
48
+ */
49
+ export declare function recordRelease(installation: Installation, releaseVersion: string): Installation;
50
+ /** Version-dir basenames present for an agent, oldest-first by directory name. */
51
+ export declare function listInstallationLabels(agent: AgentId): string[];
52
+ /**
53
+ * Every installation of an agent, migrating records as needed. A version dir
54
+ * that disappears mid-scan is skipped rather than failing the whole listing.
55
+ */
56
+ export declare function listInstallations(agent: AgentId): Installation[];
@@ -0,0 +1,196 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { atomicWriteFileSync } from '../fs-atomic.js';
5
+ import { getVersionsDir } from '../state.js';
6
+ import { VERSION_RE } from '../agent-spec/primitives.js';
7
+ import { INSTALLATION_RECORD_FILE, INSTALLATION_SCHEMA } from './types.js';
8
+ /**
9
+ * Persistence for {@link Installation} records.
10
+ *
11
+ * The record lives at `<versionDir>/installation.json` rather than in one
12
+ * central index: the version dir is what `agents trash`/`agents prune` move,
13
+ * copy and restore wholesale, so keeping identity inside it means identity
14
+ * travels with the install instead of dangling in a registry that forgets to
15
+ * follow. It is also why the file name is registered in versions.ts's
16
+ * `PRESERVED_ON_CLEAN_REINSTALL` — a repair reinstall must not mint a new id.
17
+ *
18
+ * Deliberately depends on nothing but `state`/`fs-atomic`/`primitives` so
19
+ * versions.ts can import it without an import cycle.
20
+ */
21
+ /** Directory holding one installation. Mirrors versions.ts `getVersionDir`. */
22
+ export function installationDir(agent, label) {
23
+ return path.join(getVersionsDir(), agent, label);
24
+ }
25
+ export function installationRecordPath(agent, label) {
26
+ return path.join(installationDir(agent, label), INSTALLATION_RECORD_FILE);
27
+ }
28
+ /** Mint an opaque installation id. Random, never derived from the release. */
29
+ export function mintInstallationId() {
30
+ return `ins_${crypto.randomBytes(12).toString('hex')}`;
31
+ }
32
+ function nowIso() {
33
+ return new Date().toISOString();
34
+ }
35
+ function assertValidRecord(value, file) {
36
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
37
+ throw new Error(`Installation record corrupted at ${file}: expected a JSON object.`);
38
+ }
39
+ const record = value;
40
+ if (typeof record.schema !== 'number') {
41
+ throw new Error(`Installation record corrupted at ${file}: missing numeric "schema".`);
42
+ }
43
+ if (record.schema > INSTALLATION_SCHEMA) {
44
+ throw new Error(`Installation record at ${file} was written by a newer agents-cli (schema ${record.schema} > ${INSTALLATION_SCHEMA}). Upgrade agents-cli.`);
45
+ }
46
+ for (const key of ['id', 'agent', 'label', 'releaseVersion', 'createdAt', 'updatedAt']) {
47
+ if (typeof record[key] !== 'string' || !record[key]) {
48
+ throw new Error(`Installation record corrupted at ${file}: missing string "${key}".`);
49
+ }
50
+ }
51
+ if (!Array.isArray(record.history) || record.history.length === 0) {
52
+ throw new Error(`Installation record corrupted at ${file}: "history" must be a non-empty array.`);
53
+ }
54
+ return record;
55
+ }
56
+ /**
57
+ * Read the record for one installation, or null when the version dir has none.
58
+ * Never mints — use {@link ensureInstallation} for the migrating read.
59
+ */
60
+ export function readInstallation(agent, label) {
61
+ const file = installationRecordPath(agent, label);
62
+ let raw;
63
+ try {
64
+ raw = fs.readFileSync(file, 'utf-8');
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(raw);
72
+ }
73
+ catch {
74
+ throw new Error(`Installation record corrupted at ${file}: not valid JSON.`);
75
+ }
76
+ return assertValidRecord(parsed, file);
77
+ }
78
+ export function writeInstallation(installation) {
79
+ const file = installationRecordPath(installation.agent, installation.label);
80
+ fs.mkdirSync(path.dirname(file), { recursive: true });
81
+ atomicWriteFileSync(file, `${JSON.stringify(installation, null, 2)}\n`);
82
+ }
83
+ /**
84
+ * Read the record for an existing version dir, minting and persisting one on
85
+ * first sight. This is the migration path for every installation created before
86
+ * frozen identity existed: their directory name IS their release, so the
87
+ * migrated record seeds `label === releaseVersion` and dates the install from
88
+ * the directory's own mtime rather than pretending it was created now.
89
+ *
90
+ * Throws when the version dir does not exist — an installation record must never
91
+ * describe an install that isn't there.
92
+ */
93
+ export function ensureInstallation(agent, label) {
94
+ const existing = readInstallation(agent, label);
95
+ if (existing)
96
+ return existing;
97
+ const dir = installationDir(agent, label);
98
+ if (!fs.existsSync(dir)) {
99
+ throw new Error(`No installation directory for ${agent}@${label} at ${dir}.`);
100
+ }
101
+ let createdAt;
102
+ try {
103
+ createdAt = fs.statSync(dir).mtime.toISOString();
104
+ }
105
+ catch {
106
+ createdAt = nowIso();
107
+ }
108
+ const migrated = {
109
+ schema: INSTALLATION_SCHEMA,
110
+ id: mintInstallationId(),
111
+ agent,
112
+ label,
113
+ releaseVersion: label,
114
+ createdAt,
115
+ updatedAt: createdAt,
116
+ history: [{ releaseVersion: label, at: createdAt }],
117
+ };
118
+ writeInstallation(migrated);
119
+ return migrated;
120
+ }
121
+ /**
122
+ * Create the record for a freshly-installed version dir. Idempotent: a repeat
123
+ * `agents add` of the same label keeps the original id (identity is frozen) and
124
+ * only records the release if it actually moved.
125
+ */
126
+ export function createInstallation(agent, label, releaseVersion) {
127
+ if (!VERSION_RE.test(label)) {
128
+ throw new Error(`Invalid installation label: ${JSON.stringify(label)}`);
129
+ }
130
+ const existing = readInstallation(agent, label);
131
+ if (existing) {
132
+ return existing.releaseVersion === releaseVersion
133
+ ? existing
134
+ : recordRelease(existing, releaseVersion);
135
+ }
136
+ const at = nowIso();
137
+ const created = {
138
+ schema: INSTALLATION_SCHEMA,
139
+ id: mintInstallationId(),
140
+ agent,
141
+ label,
142
+ releaseVersion,
143
+ createdAt: at,
144
+ updatedAt: at,
145
+ history: [{ releaseVersion, at }],
146
+ };
147
+ writeInstallation(created);
148
+ return created;
149
+ }
150
+ /**
151
+ * Move an installation's recorded release forward, preserving identity. Returns
152
+ * the persisted record. Call only AFTER the new release is live on disk — the
153
+ * record is the claim that it is.
154
+ */
155
+ export function recordRelease(installation, releaseVersion) {
156
+ const at = nowIso();
157
+ const next = {
158
+ ...installation,
159
+ releaseVersion,
160
+ updatedAt: at,
161
+ history: [...installation.history, { releaseVersion, at }],
162
+ };
163
+ writeInstallation(next);
164
+ return next;
165
+ }
166
+ /** Version-dir basenames present for an agent, oldest-first by directory name. */
167
+ export function listInstallationLabels(agent) {
168
+ const agentDir = path.join(getVersionsDir(), agent);
169
+ let entries;
170
+ try {
171
+ entries = fs.readdirSync(agentDir, { withFileTypes: true });
172
+ }
173
+ catch {
174
+ return [];
175
+ }
176
+ return entries
177
+ .filter((entry) => entry.isDirectory() && VERSION_RE.test(entry.name))
178
+ .map((entry) => entry.name)
179
+ .sort();
180
+ }
181
+ /**
182
+ * Every installation of an agent, migrating records as needed. A version dir
183
+ * that disappears mid-scan is skipped rather than failing the whole listing.
184
+ */
185
+ export function listInstallations(agent) {
186
+ const out = [];
187
+ for (const label of listInstallationLabels(agent)) {
188
+ try {
189
+ out.push(ensureInstallation(agent, label));
190
+ }
191
+ catch {
192
+ /* dir vanished or unreadable — not an installation we can act on */
193
+ }
194
+ }
195
+ return out;
196
+ }
@@ -0,0 +1,73 @@
1
+ import type { AgentId } from '../types.js';
2
+ import type { Installation, UpdateStrategyId } from './types.js';
3
+ export interface UpdateContext {
4
+ agent: AgentId;
5
+ installation: Installation;
6
+ /** What the user asked for: `latest`, `oldest`, or a concrete release. */
7
+ requested: string;
8
+ onProgress?: (message: string) => void;
9
+ }
10
+ /** A release fetched but not yet live. */
11
+ export interface StagedRelease {
12
+ release: string;
13
+ /** Extensionless launch target to probe. Windows appends `.cmd` (see verifyBinaryLaunches). */
14
+ binary: string;
15
+ /** HOME the probe runs under — always the installation's own home. */
16
+ home: string;
17
+ /** Scratch dir to delete once the run finishes, or null when nothing was staged. */
18
+ stagingDir: string | null;
19
+ }
20
+ /** Undo/finish handles returned by a commit, so update.ts owns the transaction. */
21
+ export interface CommitHandles {
22
+ /** Put the previous release back. Must be safe to call once, immediately after commit. */
23
+ undo: () => void;
24
+ /** Discard the undo material. Called only once the update is durable. */
25
+ finalize: () => void;
26
+ }
27
+ /**
28
+ * How one class of harness replaces the release inside a frozen installation.
29
+ *
30
+ * Chosen from the registry's declared capabilities — never from an agent id — so
31
+ * a harness added to `AGENTS` is covered the day it lands. See
32
+ * {@link selectUpdateStrategy}.
33
+ */
34
+ export interface UpdateStrategy {
35
+ readonly id: UpdateStrategyId;
36
+ /**
37
+ * True only when `undo` can restore the PREVIOUS RELEASE in full — i.e. the
38
+ * vendor artifact lives inside this installation's own directory and was
39
+ * fetched without mutating anything global.
40
+ *
41
+ * It is not a switch for whether the orchestrator rolls back: `undo` always
42
+ * runs on a post-commit failure, because every strategy that displaces
43
+ * something must put it back. What this flag changes is what the user is
44
+ * told, since for an installer-driven harness the global binary the vendor
45
+ * replaced is not ours to restore.
46
+ */
47
+ readonly transactional: boolean;
48
+ /** True when several installations of this agent share one binary on disk. */
49
+ readonly sharedBinary: boolean;
50
+ /** Turn `requested` into the concrete release this run will install. */
51
+ resolveTarget(ctx: UpdateContext): Promise<string>;
52
+ /** Fetch the target release into a place that is not yet live. */
53
+ stage(ctx: UpdateContext, target: string): Promise<StagedRelease>;
54
+ /** Make the staged release the live one. */
55
+ commit(ctx: UpdateContext, staged: StagedRelease): Promise<CommitHandles>;
56
+ }
57
+ /**
58
+ * Pick the update strategy for an agent from the registry's declared shape.
59
+ *
60
+ * The ordering mirrors `installVersion`: an npm package wins whenever one is
61
+ * declared (kimi declares both a package and a script, and its package is what
62
+ * `agents add` installs), then a single shared global binary, then a per-install
63
+ * script. Anything else is an integration boundary we do not handle — it throws
64
+ * rather than silently no-opping and reporting success.
65
+ */
66
+ export declare function selectUpdateStrategy(agent: AgentId): UpdateStrategy;
67
+ /**
68
+ * Whether a concrete release can be requested for this agent at all. False for
69
+ * every self-updating harness — their installers carry no version token.
70
+ */
71
+ export declare function supportsPinnedUpdate(agent: AgentId): boolean;
72
+ /** Guard a user-supplied release token before it reaches a path or a package spec. */
73
+ export declare function assertValidRelease(requested: string): void;