@phnx-labs/agents-cli 1.22.14 → 1.22.15

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 (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.js +4 -6
  5. package/dist/commands/packages.js +90 -22
  6. package/dist/commands/projects.d.ts +2 -5
  7. package/dist/commands/projects.js +92 -65
  8. package/dist/commands/routines.js +53 -71
  9. package/dist/commands/setup-watchdog.d.ts +3 -0
  10. package/dist/commands/setup-watchdog.js +54 -0
  11. package/dist/commands/setup.js +8 -1
  12. package/dist/commands/ssh.d.ts +11 -0
  13. package/dist/commands/ssh.js +18 -4
  14. package/dist/commands/watchdog.d.ts +3 -4
  15. package/dist/commands/watchdog.js +40 -28
  16. package/dist/lib/auto-dispatch.d.ts +8 -10
  17. package/dist/lib/auto-dispatch.js +20 -42
  18. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  19. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  20. package/dist/lib/migrate.d.ts +1 -1
  21. package/dist/lib/migrate.js +4 -3
  22. package/dist/lib/project-import.d.ts +7 -46
  23. package/dist/lib/project-import.js +9 -108
  24. package/dist/lib/projects.d.ts +17 -0
  25. package/dist/lib/projects.js +17 -1
  26. package/dist/lib/registry.d.ts +1 -1
  27. package/dist/lib/registry.js +11 -0
  28. package/dist/lib/routine-activation.d.ts +14 -0
  29. package/dist/lib/routine-activation.js +78 -0
  30. package/dist/lib/routines-project.d.ts +1 -1
  31. package/dist/lib/routines-project.js +3 -9
  32. package/dist/lib/routines.d.ts +3 -1
  33. package/dist/lib/routines.js +32 -19
  34. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  35. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  36. package/dist/lib/state.js +17 -4
  37. package/dist/lib/types.d.ts +14 -1
  38. package/dist/lib/watchdog/rotate.d.ts +1 -1
  39. package/dist/lib/watchdog/rotate.js +1 -1
  40. package/package.json +1 -1
  41. package/dist/lib/watchdog/routine.d.ts +0 -44
  42. package/dist/lib/watchdog/routine.js +0 -69
@@ -17,6 +17,7 @@ import { ALL_AGENT_IDS } from './agents.js';
17
17
  import { machineId, normalizeHost } from './machine-id.js';
18
18
  import { resolveActor } from './actor.js';
19
19
  import { percentile } from './percentile.js';
20
+ import { enabledRoutineNames, replaceEnabledRoutines, routineEnabledOnThisDevice, setRoutineEnabledOnThisDevice, } from './routine-activation.js';
20
21
  export const HOST_STRATEGIES = ['local', 'host', 'fleet', 'cloud'];
21
22
  /** Canonical set of accepted GitHub trigger events — single source for validation. */
22
23
  export const GITHUB_TRIGGER_EVENTS = [
@@ -82,6 +83,9 @@ export function finalizeRunMeta(meta, status, exitCode, opts) {
82
83
  * catchup/overdue, manual run) gates on this.
83
84
  */
84
85
  export function jobRunsOnThisDevice(config) {
86
+ const activated = routineEnabledOnThisDevice(config.name);
87
+ if (activated !== null)
88
+ return activated;
85
89
  const owner = routineOwnerDevice(config);
86
90
  // Unrestricted: no pin means fleet-wide by design (`watchdog`, `check-updates`).
87
91
  if (owner === null)
@@ -260,7 +264,7 @@ export function listJobs(cwd) {
260
264
  jobs.push(job);
261
265
  }
262
266
  }
263
- return jobs;
267
+ return jobs.map(applyDeviceActivation);
264
268
  }
265
269
  /**
266
270
  * Read a single job config by name, checking project > user > system.
@@ -286,12 +290,16 @@ export function readJob(name, cwd) {
286
290
  const job = readJobFromDir(dir, name);
287
291
  if (job) {
288
292
  if (scope === 'project')
289
- return overlayUserRoutineDevices(job, readJobFromDir(userDir, name));
290
- return job;
293
+ return applyDeviceActivation(overlayUserRoutineDevices(job, readJobFromDir(userDir, name)));
294
+ return applyDeviceActivation(job);
291
295
  }
292
296
  }
293
297
  return null;
294
298
  }
299
+ function applyDeviceActivation(job) {
300
+ const activated = routineEnabledOnThisDevice(job.name);
301
+ return activated === null ? job : { ...job, enabled: activated };
302
+ }
295
303
  function readJobFromDir(dir, name) {
296
304
  for (const ext of ['.yml', '.yaml']) {
297
305
  const filePath = safeJoin(dir, name + ext);
@@ -327,6 +335,10 @@ function readJobFile(filePath) {
327
335
  ...JOB_DEFAULTS,
328
336
  ...parsed,
329
337
  name: parsed.name || path.basename(filePath).replace(/\.ya?ml$/, ''),
338
+ // Before a device manifest exists, preserve only explicit legacy state.
339
+ // A new built-in definition with no `enabled:` field stays opt-in until
340
+ // setup materializes this host's routines list.
341
+ enabled: Object.prototype.hasOwnProperty.call(parsed, 'enabled') ? parsed.enabled !== false : false,
330
342
  };
331
343
  }
332
344
  catch {
@@ -368,15 +380,12 @@ export function writeJob(config) {
368
380
  delete output.effort;
369
381
  if (output.timeout === '10m')
370
382
  delete output.timeout;
371
- if (output.enabled === true)
372
- delete output.enabled;
383
+ delete output.enabled;
373
384
  if (output.runOnce === false || output.runOnce === undefined)
374
385
  delete output.runOnce;
375
386
  if (output.catchup === true || output.catchup === undefined)
376
387
  delete output.catchup;
377
- const devArr = output.devices;
378
- if (!devArr || devArr.length === 0)
379
- delete output.devices;
388
+ delete output.devices;
380
389
  let existingText = null;
381
390
  if (ymlExists || yamlExists) {
382
391
  try {
@@ -441,8 +450,20 @@ export function setJobEnabled(name, enabled) {
441
450
  const job = readJob(name);
442
451
  if (!job)
443
452
  throw new Error(`Job '${name}' not found`);
444
- job.enabled = enabled;
445
- writeJob(job);
453
+ const legacyEnabled = enabledRoutineNames() === null
454
+ ? listJobs().filter((candidate) => candidate.enabled && jobRunsOnThisDevice(candidate)).map((candidate) => candidate.name)
455
+ : [];
456
+ setRoutineEnabledOnThisDevice(name, enabled, legacyEnabled);
457
+ }
458
+ /** Materialize legacy definition state into this device's activation manifest. */
459
+ export function migrateLegacyRoutineActivation() {
460
+ if (enabledRoutineNames() !== null)
461
+ return false;
462
+ const jobs = listJobs();
463
+ if (!jobs.some((job) => job.enabled || Array.isArray(job.devices)))
464
+ return false;
465
+ replaceEnabledRoutines(jobs.filter((job) => job.enabled && jobRunsOnThisDevice(job)).map((job) => job.name));
466
+ return true;
446
467
  }
447
468
  /** Validate a partial job config, returning a list of human-readable errors. */
448
469
  export function validateJob(config) {
@@ -606,15 +627,6 @@ export function validateJob(config) {
606
627
  if (config.catchup !== undefined && typeof config.catchup !== 'boolean') {
607
628
  errors.push('catchup must be a boolean (false to skip running a missed fire late)');
608
629
  }
609
- // Off-box placement without a devices pin fires on every fleet daemon and
610
- // each dispatches once (RUSH-1980). Enforce the pin at validation so hand
611
- // edits and devices --clear cannot re-open the hole.
612
- if (placementRequiresFiringPin(strategy)) {
613
- if (!config.devices || config.devices.length === 0) {
614
- errors.push(`hostStrategy: ${strategy} requires devices: [<name>] to pin which daemon may fire ` +
615
- '(otherwise every fleet daemon dispatches once). Pin to one machine, e.g. devices: [this-host]');
616
- }
617
- }
618
630
  return errors;
619
631
  }
620
632
  /** Validate a job trigger block, returning a list of human-readable errors. */
@@ -1155,6 +1167,7 @@ export function installJobFromSource(sourcePath, name) {
1155
1167
  return { success: false, error: errors.join(', ') };
1156
1168
  }
1157
1169
  writeJob(config);
1170
+ setJobEnabled(config.name, config.enabled);
1158
1171
  return { success: true };
1159
1172
  }
1160
1173
  catch (err) {
package/dist/lib/state.js CHANGED
@@ -812,6 +812,7 @@ const META_KEY_SCOPE = {
812
812
  versions: 'device',
813
813
  defaultBrowserProfile: 'device',
814
814
  deviceConfig: 'device',
815
+ deviceRoutines: 'device',
815
816
  // Central — synced via agents.yaml.
816
817
  run: 'central',
817
818
  model: 'central',
@@ -909,7 +910,7 @@ function serializeCentral(central) {
909
910
  return isEmpty ? META_HEADER : doc.toString({ collectionStyle: 'block' });
910
911
  }
911
912
  function writeMetaUnlocked(meta) {
912
- const { agents, isolatedAgents, versions, defaultBrowserProfile, deviceConfig, ...central } = meta;
913
+ const { agents, isolatedAgents, versions, defaultBrowserProfile, deviceConfig, deviceRoutines, ...central } = meta;
913
914
  // Write the machine-local files FIRST, then strip central — so a crash mid-write
914
915
  // never removes pins/versions from central before they're persisted elsewhere.
915
916
  const devicePath = getDeviceMetaPath();
@@ -923,7 +924,8 @@ function writeMetaUnlocked(meta) {
923
924
  // Device-scope config (`maxAgents`, `schedulerEnabled`, …) is per-machine, so it
924
925
  // rides the device doc under `config:` — never the central doc that syncs.
925
926
  const hasDeviceConfig = !!deviceConfig && Object.keys(deviceConfig).length > 0;
926
- if (hasAgents || hasIsolatedAgents || hasDefaultBrowser || hasDeviceConfig) {
927
+ const hasDeviceRoutines = Array.isArray(deviceRoutines);
928
+ if (hasAgents || hasIsolatedAgents || hasDefaultBrowser || hasDeviceConfig || hasDeviceRoutines) {
927
929
  // Device-local doc carries `agents:` pins, `defaultBrowserProfile:`, and the
928
930
  // device-scope `config:` — all per-machine and must never land in central
929
931
  // agents.yaml (which syncs).
@@ -936,6 +938,8 @@ function writeMetaUnlocked(meta) {
936
938
  deviceDoc.defaultBrowserProfile = defaultBrowserProfile;
937
939
  if (hasDeviceConfig)
938
940
  deviceDoc.config = deviceConfig;
941
+ if (hasDeviceRoutines)
942
+ deviceDoc.routines = deviceRoutines;
939
943
  fs.mkdirSync(path.dirname(devicePath), { recursive: true });
940
944
  writeIfChanged(devicePath, META_HEADER + yaml.stringify(deviceDoc));
941
945
  }
@@ -971,8 +975,12 @@ function writeMetaUnlocked(meta) {
971
975
  function overlayMachineLocal(meta) {
972
976
  const devicePath = getDeviceMetaPath();
973
977
  if (fs.existsSync(devicePath)) {
978
+ let dm = null;
974
979
  try {
975
- const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
980
+ dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
981
+ }
982
+ catch { /* preserve the existing tolerance for malformed legacy device YAML */ }
983
+ if (dm) {
976
984
  if (dm?.agents)
977
985
  meta.agents = { ...meta.agents, ...dm.agents };
978
986
  if (dm?.isolatedAgents)
@@ -981,8 +989,13 @@ function overlayMachineLocal(meta) {
981
989
  meta.defaultBrowserProfile = dm.defaultBrowserProfile;
982
990
  if (dm?.config)
983
991
  meta.deviceConfig = dm.config;
992
+ if (dm && Object.prototype.hasOwnProperty.call(dm, 'routines')) {
993
+ if (!Array.isArray(dm.routines) || dm.routines.some((name) => typeof name !== 'string')) {
994
+ throw new Error(`Device config corrupted at ${devicePath}: routines must be a string list.`);
995
+ }
996
+ meta.deviceRoutines = dm.routines;
997
+ }
984
998
  }
985
- catch { /* ignore malformed device file */ }
986
999
  }
987
1000
  const vrPath = getVersionResourcesPath();
988
1001
  if (fs.existsSync(vrPath)) {
@@ -511,10 +511,16 @@ export interface RegistrySearchResult {
511
511
  }
512
512
  /** A package that has been resolved from a registry and is ready to install. */
513
513
  export interface ResolvedPackage {
514
- type: 'mcp' | 'skill' | 'git';
514
+ /** `plugin` is Phase 5 packaging: `agents install plugin:<spec>` → plugins install. */
515
+ type: 'mcp' | 'skill' | 'git' | 'plugin';
515
516
  source: string;
516
517
  mcpEntry?: McpServerEntry;
517
518
  skillEntry?: SkillEntry;
519
+ /**
520
+ * Plugin install spec (`name@url`, path, or bare source) when `type === 'plugin'`.
521
+ * Same grammar as `agents plugins install <spec>`.
522
+ */
523
+ pluginSpec?: string;
518
524
  }
519
525
  /** Categories of resources that can be synced into an agent version home. */
520
526
  export type ResourceType = 'commands' | 'skills' | 'hooks' | 'memory' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'workflows';
@@ -937,6 +943,13 @@ export interface Meta {
937
943
  * device-config registry (`maxAgents`, `schedulerEnabled`, `notes`). Per-machine by design — unset = today's behavior.
938
944
  */
939
945
  deviceConfig?: Record<string, unknown>;
946
+ /**
947
+ * Routine names enabled on this machine. In memory this stays distinct from
948
+ * portable user config; state.ts writes it as top-level `routines:` in
949
+ * `~/.agents/devices/<machine>/agents.yaml`. Presence in the list is the whole
950
+ * activation state: absent means disabled on this device.
951
+ */
952
+ deviceRoutines?: string[];
940
953
  /**
941
954
  * Agent-host registry keyed by host name (`agents hosts`). Portable user
942
955
  * config synced with `agents repo push/pull`. For `ssh-config` hosts this is
@@ -166,7 +166,7 @@ export declare function isWatchdogRotateEnabled(): boolean;
166
166
  * Persist `watchdog.rotate: on|off`. Called by the `agents watchdog rotate
167
167
  * on|off` subcommand (commands/watchdog.ts) — the rotate-only switch the
168
168
  * Factory migration uses so a user who opted out of autoRotate keeps nudging
169
- * (rather than `agents watchdog disable`, which kills the whole watchdog).
169
+ * (rather than `agents watchdog off`, which disables the whole watchdog here).
170
170
  */
171
171
  export declare function setWatchdogRotateEnabled(on: boolean): void;
172
172
  export interface RotateGateResult {
@@ -291,7 +291,7 @@ export function isWatchdogRotateEnabled() {
291
291
  * Persist `watchdog.rotate: on|off`. Called by the `agents watchdog rotate
292
292
  * on|off` subcommand (commands/watchdog.ts) — the rotate-only switch the
293
293
  * Factory migration uses so a user who opted out of autoRotate keeps nudging
294
- * (rather than `agents watchdog disable`, which kills the whole watchdog).
294
+ * (rather than `agents watchdog off`, which disables the whole watchdog here).
295
295
  */
296
296
  export function setWatchdogRotateEnabled(on) {
297
297
  const meta = readMeta();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.14",
3
+ "version": "1.22.15",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,44 +0,0 @@
1
- /**
2
- * The watchdog, expressed as a routine.
3
- *
4
- * The always-on watchdog is not a bespoke subsystem — it is a plain `command`
5
- * routine the daemon's existing scheduler fires: `agents watchdog --nudge`, once
6
- * every couple of minutes. Enabling / disabling the watchdog is enabling /
7
- * disabling that routine, so there is ONE durable, inspectable concept
8
- * (`agents routines list` shows it; the scheduler catches it up if the daemon was
9
- * down) instead of a private sentinel file and a hand-rolled `--watch` loop.
10
- *
11
- * A `command` routine — not `agent`/`workflow` — because the tick is deterministic
12
- * housekeeping: no LLM, no auth, no sandbox. Each fire runs ONE bounded tick (the
13
- * cron is the loop), so the daemon's per-fire model fits without a long-lived
14
- * process.
15
- */
16
- import { type JobConfig } from '../routines.js';
17
- /** The routine name. `agents routines list` / `readJob` key on this. */
18
- export declare const WATCHDOG_ROUTINE_NAME = "watchdog";
19
- /**
20
- * Fire every 2 minutes. Mirrors the extension's 120s tick, and is well inside the
21
- * 5m stall threshold and 20m nudge cooldown, so a stall is noticed promptly
22
- * without the daemon spawning a tick more often than the cooldown can matter.
23
- */
24
- export declare const WATCHDOG_ROUTINE_SCHEDULE = "*/2 * * * *";
25
- /** The plain shell each fire runs: one bounded, deterministic nudge tick. */
26
- export declare const WATCHDOG_ROUTINE_COMMAND = "agents watchdog --nudge";
27
- /**
28
- * Build the watchdog routine config. Pure — touches no disk. `mode`/`effort`/
29
- * `timeout` are required by JobConfig but ignored for `command` routines;
30
- * writeJob() drops the default ('auto'/'10m') values so the on-disk file stays
31
- * to the point (name + schedule + command + enabled).
32
- */
33
- export declare function buildWatchdogRoutine(enabled: boolean): JobConfig;
34
- /** Is the watchdog routine present on disk (user or system layer)? */
35
- export declare function watchdogRoutineExists(): boolean;
36
- /** Is the watchdog routine present AND enabled? */
37
- export declare function isWatchdogRoutineEnabled(): boolean;
38
- /**
39
- * Ensure the watchdog routine exists with the requested enabled state, preserving
40
- * any user edits. Absent → create it; present → only flip `enabled` (never clobber
41
- * a schedule or command the user tuned). Idempotent: calling it twice with the
42
- * same state is a no-op after the first.
43
- */
44
- export declare function ensureWatchdogRoutine(enabled: boolean): void;
@@ -1,69 +0,0 @@
1
- /**
2
- * The watchdog, expressed as a routine.
3
- *
4
- * The always-on watchdog is not a bespoke subsystem — it is a plain `command`
5
- * routine the daemon's existing scheduler fires: `agents watchdog --nudge`, once
6
- * every couple of minutes. Enabling / disabling the watchdog is enabling /
7
- * disabling that routine, so there is ONE durable, inspectable concept
8
- * (`agents routines list` shows it; the scheduler catches it up if the daemon was
9
- * down) instead of a private sentinel file and a hand-rolled `--watch` loop.
10
- *
11
- * A `command` routine — not `agent`/`workflow` — because the tick is deterministic
12
- * housekeeping: no LLM, no auth, no sandbox. Each fire runs ONE bounded tick (the
13
- * cron is the loop), so the daemon's per-fire model fits without a long-lived
14
- * process.
15
- */
16
- import { readJob, writeJob, setJobEnabled } from '../routines.js';
17
- /** The routine name. `agents routines list` / `readJob` key on this. */
18
- export const WATCHDOG_ROUTINE_NAME = 'watchdog';
19
- /**
20
- * Fire every 2 minutes. Mirrors the extension's 120s tick, and is well inside the
21
- * 5m stall threshold and 20m nudge cooldown, so a stall is noticed promptly
22
- * without the daemon spawning a tick more often than the cooldown can matter.
23
- */
24
- export const WATCHDOG_ROUTINE_SCHEDULE = '*/2 * * * *';
25
- /** The plain shell each fire runs: one bounded, deterministic nudge tick. */
26
- export const WATCHDOG_ROUTINE_COMMAND = 'agents watchdog --nudge';
27
- /**
28
- * Build the watchdog routine config. Pure — touches no disk. `mode`/`effort`/
29
- * `timeout` are required by JobConfig but ignored for `command` routines;
30
- * writeJob() drops the default ('auto'/'10m') values so the on-disk file stays
31
- * to the point (name + schedule + command + enabled).
32
- */
33
- export function buildWatchdogRoutine(enabled) {
34
- return {
35
- name: WATCHDOG_ROUTINE_NAME,
36
- schedule: WATCHDOG_ROUTINE_SCHEDULE,
37
- command: WATCHDOG_ROUTINE_COMMAND,
38
- enabled,
39
- mode: 'auto',
40
- effort: 'auto',
41
- timeout: '10m',
42
- prompt: '',
43
- };
44
- }
45
- /** Is the watchdog routine present on disk (user or system layer)? */
46
- export function watchdogRoutineExists() {
47
- return readJob(WATCHDOG_ROUTINE_NAME) !== null;
48
- }
49
- /** Is the watchdog routine present AND enabled? */
50
- export function isWatchdogRoutineEnabled() {
51
- const job = readJob(WATCHDOG_ROUTINE_NAME);
52
- return job !== null && job.enabled === true;
53
- }
54
- /**
55
- * Ensure the watchdog routine exists with the requested enabled state, preserving
56
- * any user edits. Absent → create it; present → only flip `enabled` (never clobber
57
- * a schedule or command the user tuned). Idempotent: calling it twice with the
58
- * same state is a no-op after the first.
59
- */
60
- export function ensureWatchdogRoutine(enabled) {
61
- const existing = readJob(WATCHDOG_ROUTINE_NAME);
62
- if (existing === null) {
63
- writeJob(buildWatchdogRoutine(enabled));
64
- return;
65
- }
66
- if (existing.enabled !== enabled) {
67
- setJobEnabled(WATCHDOG_ROUTINE_NAME, enabled);
68
- }
69
- }