@phnx-labs/agents-cli 1.22.53 → 1.22.55

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 (166) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +59 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +219 -173
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +10 -8
  16. package/dist/commands/focus.d.ts +1 -0
  17. package/dist/commands/focus.js +2 -2
  18. package/dist/commands/go.d.ts +5 -4
  19. package/dist/commands/go.js +7 -7
  20. package/dist/commands/insights.js +9 -0
  21. package/dist/commands/monitors.js +85 -30
  22. package/dist/commands/output.js +8 -2
  23. package/dist/commands/repo.js +18 -0
  24. package/dist/commands/routines.js +31 -2
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions.d.ts +20 -12
  27. package/dist/commands/sessions.js +64 -20
  28. package/dist/commands/setup-accounts.d.ts +8 -0
  29. package/dist/commands/setup-accounts.js +47 -0
  30. package/dist/commands/setup.d.ts +1 -1
  31. package/dist/commands/setup.js +11 -2
  32. package/dist/commands/share.d.ts +79 -3
  33. package/dist/commands/share.js +347 -18
  34. package/dist/commands/ssh.d.ts +7 -0
  35. package/dist/commands/ssh.js +18 -2
  36. package/dist/commands/status.js +14 -0
  37. package/dist/commands/view.d.ts +11 -1
  38. package/dist/commands/view.js +35 -7
  39. package/dist/lib/account-registry.js +15 -3
  40. package/dist/lib/accounting/rotate.d.ts +20 -6
  41. package/dist/lib/accounting/rotate.js +38 -7
  42. package/dist/lib/accounting/usage.d.ts +68 -1
  43. package/dist/lib/accounting/usage.js +116 -10
  44. package/dist/lib/agent-spec/agents.d.ts +5 -2
  45. package/dist/lib/agent-spec/agents.js +25 -7
  46. package/dist/lib/analytics/mix-commands.js +12 -6
  47. package/dist/lib/auth-mint.d.ts +150 -0
  48. package/dist/lib/auth-mint.js +434 -0
  49. package/dist/lib/browser/cdp.d.ts +1 -1
  50. package/dist/lib/browser/cdp.js +1 -1
  51. package/dist/lib/browser/ffmpeg.d.ts +12 -0
  52. package/dist/lib/browser/ffmpeg.js +184 -0
  53. package/dist/lib/browser/remote-control.d.ts +9 -7
  54. package/dist/lib/browser/remote-control.js +9 -7
  55. package/dist/lib/browser/service.js +119 -25
  56. package/dist/lib/claude-account-token.d.ts +10 -0
  57. package/dist/lib/claude-account-token.js +14 -4
  58. package/dist/lib/config-drift.d.ts +37 -0
  59. package/dist/lib/config-drift.js +72 -0
  60. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  61. package/dist/lib/daemon/auth-sync-service.js +34 -0
  62. package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
  63. package/dist/lib/daemon/browser-task-reap-service.js +26 -0
  64. package/dist/lib/daemon/daemon.js +87 -176
  65. package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
  66. package/dist/lib/daemon/heartbeat-service.js +26 -0
  67. package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
  68. package/dist/lib/daemon/monitor-engine-service.js +15 -7
  69. package/dist/lib/daemon/runner.d.ts +15 -0
  70. package/dist/lib/daemon/runner.js +23 -0
  71. package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
  72. package/dist/lib/daemon/secrets-broker-service.js +17 -32
  73. package/dist/lib/daemon/service.d.ts +2 -2
  74. package/dist/lib/daemon/service.js +1 -1
  75. package/dist/lib/daemon/session-state-service.d.ts +21 -0
  76. package/dist/lib/daemon/session-state-service.js +34 -0
  77. package/dist/lib/daemon/supervisor.d.ts +17 -7
  78. package/dist/lib/daemon/supervisor.js +87 -14
  79. package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
  80. package/dist/lib/daemon/tmux-reap-service.js +28 -0
  81. package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
  82. package/dist/lib/daemon/webhook-receiver-service.js +17 -0
  83. package/dist/lib/daemon-services.d.ts +1 -1
  84. package/dist/lib/daemon-services.js +25 -0
  85. package/dist/lib/device-config.d.ts +3 -3
  86. package/dist/lib/device-config.js +5 -5
  87. package/dist/lib/devices/connect.d.ts +26 -0
  88. package/dist/lib/devices/connect.js +48 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  90. package/dist/lib/devices/doctor-findings.js +19 -1
  91. package/dist/lib/devices/harness-inventory.js +5 -2
  92. package/dist/lib/exec.d.ts +28 -0
  93. package/dist/lib/exec.js +73 -7
  94. package/dist/lib/feed/feed.d.ts +1 -1
  95. package/dist/lib/feed/feed.js +23 -1
  96. package/dist/lib/feed-broadcast.js +1 -1
  97. package/dist/lib/fleet/apply.d.ts +11 -0
  98. package/dist/lib/fleet/apply.js +23 -3
  99. package/dist/lib/fleet/auth-sync.js +5 -3
  100. package/dist/lib/help.d.ts +9 -0
  101. package/dist/lib/help.js +29 -1
  102. package/dist/lib/hosts/passthrough.d.ts +1 -10
  103. package/dist/lib/hosts/passthrough.js +1 -13
  104. package/dist/lib/installations/versions.js +9 -1
  105. package/dist/lib/linux-userns.d.ts +58 -0
  106. package/dist/lib/linux-userns.js +116 -0
  107. package/dist/lib/memory.d.ts +26 -0
  108. package/dist/lib/memory.js +80 -1
  109. package/dist/lib/monitors/config.d.ts +11 -0
  110. package/dist/lib/monitors/config.js +8 -0
  111. package/dist/lib/monitors/engine.d.ts +5 -1
  112. package/dist/lib/monitors/engine.js +13 -4
  113. package/dist/lib/monitors/state.d.ts +37 -1
  114. package/dist/lib/monitors/state.js +79 -4
  115. package/dist/lib/permissions-registry.d.ts +2 -0
  116. package/dist/lib/permissions-registry.js +116 -14
  117. package/dist/lib/permissions.d.ts +5 -3
  118. package/dist/lib/permissions.js +25 -27
  119. package/dist/lib/profiles.d.ts +8 -7
  120. package/dist/lib/profiles.js +12 -0
  121. package/dist/lib/project-key.d.ts +9 -0
  122. package/dist/lib/project-key.js +11 -0
  123. package/dist/lib/scheduling/routines.d.ts +47 -0
  124. package/dist/lib/scheduling/routines.js +70 -1
  125. package/dist/lib/secrets/bundles.d.ts +35 -0
  126. package/dist/lib/secrets/bundles.js +78 -1
  127. package/dist/lib/secrets/push.d.ts +3 -8
  128. package/dist/lib/secrets/push.js +18 -14
  129. package/dist/lib/secrets/remote.d.ts +9 -18
  130. package/dist/lib/secrets/remote.js +11 -26
  131. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  132. package/dist/lib/secrets/reserved-sync.js +129 -0
  133. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  134. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  135. package/dist/lib/self-heal/registry.js +4 -0
  136. package/dist/lib/self-heal/types.d.ts +1 -1
  137. package/dist/lib/session/active.js +1 -4
  138. package/dist/lib/session/db.d.ts +41 -5
  139. package/dist/lib/session/db.js +132 -30
  140. package/dist/lib/session/discover.d.ts +32 -4
  141. package/dist/lib/session/discover.js +119 -25
  142. package/dist/lib/session/insights.d.ts +14 -0
  143. package/dist/lib/session/insights.js +25 -2
  144. package/dist/lib/session/linear.js +1 -1
  145. package/dist/lib/session/shell-programs.d.ts +17 -0
  146. package/dist/lib/session/shell-programs.js +21 -0
  147. package/dist/lib/session/state.js +2 -1
  148. package/dist/lib/session/stream-render.js +2 -1
  149. package/dist/lib/session/tool-calls.js +2 -5
  150. package/dist/lib/session/trajectory-html.js +2 -1
  151. package/dist/lib/session/trajectory.js +3 -12
  152. package/dist/lib/session/types.d.ts +8 -0
  153. package/dist/lib/share/capture.js +11 -2
  154. package/dist/lib/share/publish.d.ts +56 -5
  155. package/dist/lib/share/publish.js +126 -18
  156. package/dist/lib/share/worker-template.d.ts +3 -12
  157. package/dist/lib/share/worker-template.js +860 -59
  158. package/dist/lib/startup/root-command.js +2 -1
  159. package/dist/lib/state.d.ts +16 -0
  160. package/dist/lib/state.js +178 -46
  161. package/dist/lib/sync-status.d.ts +4 -0
  162. package/dist/lib/sync-status.js +3 -0
  163. package/dist/lib/traces/classify.js +24 -19
  164. package/dist/lib/usage-refresh.js +2 -1
  165. package/dist/lib/view-types.d.ts +7 -0
  166. package/package.json +9 -2
@@ -19,7 +19,8 @@
19
19
  export function configureRootCommand(program, name, version) {
20
20
  return program
21
21
  .name(name)
22
- .description('Environment manager for AI agents')
22
+ .description('Install, configure, run, and dispatch AI coding agents from one place.\n' +
23
+ 'Works with Claude, Codex, Antigravity, Cursor, OpenCode, OpenClaw, and Droid.')
23
24
  .version(version)
24
25
  .option('--verbose', 'Show startup self-heal details on stderr')
25
26
  .helpOption('-h, --help', 'Show help')
@@ -391,6 +391,22 @@ export declare function getDeviceMetaPath(): string;
391
391
  */
392
392
  export declare function getVersionResourcesPath(): string;
393
393
  export declare function withMetaLock<T>(fn: () => T): T;
394
+ /**
395
+ * True when the top-level `~/.agents/agents.yaml` carries a metadata header that
396
+ * is NOT the canonical {@link META_HEADER} — the P1 frozen-header case a box only
397
+ * heals on its next central write (serializeCentral). An absent or headerless
398
+ * file is NOT stale (a headerless central file is deliberately left alone).
399
+ * Surfaced as config drift by `agents sync status` (PHNX-3315).
400
+ */
401
+ export declare function hasStaleMetaHeader(): boolean;
402
+ /**
403
+ * Parse the top-level user `agents.yaml` (this box's synced central file) WITHOUT
404
+ * the system-repo merge, machine-local overlay, or cache — the raw on-disk central
405
+ * map, or null when the file is absent/unparseable. Used by config-drift detection
406
+ * to see the central blocks that should have folded into the device doc, without
407
+ * mis-attributing a system-repo default as this box's own leak (PHNX-3315).
408
+ */
409
+ export declare function readTopLevelUserMeta(): Record<string, unknown> | null;
394
410
  /**
395
411
  * Write `meta` to disk (central + device docs + pins) WITHOUT taking the meta
396
412
  * lock — the caller must already hold it via {@link withMetaLock}. Exported so a
package/dist/lib/state.js CHANGED
@@ -858,52 +858,99 @@ function writeIfChanged(filePath, content) {
858
858
  * `agents:` / `versions:` are not written (no empty committed files).
859
859
  */
860
860
  /**
861
- * Every `Meta` key, classified as `central` (synced via agents.yaml) or `device`
862
- * (routed to the per-machine device file by {@link writeMetaUnlocked}). The
863
- * `Record<keyof Meta, …>` type makes this EXHAUSTIVE: adding a field to `Meta`
864
- * without classifying it here is a compile error. That guarantee is what lets
865
- * `serializeCentral` delete only keys THIS version models — a key a newer CLI
866
- * version added (absent from this map) is preserved verbatim instead of being
867
- * dropped and synced fleet-wide as data loss. This is the fix for the recurring
868
- * agents.yaml key-loss (beta flags, `notify.owner`, and `feed:` all vanished
869
- * this way when an older-versioned CLI on the fleet rewrote the file).
861
+ * Fleet-shared (`central`) Meta keys — the OPT-IN allowlist. A key listed here is
862
+ * written to the synced `~/.agents/agents.yaml`. Device-scope is the DEFAULT:
863
+ * {@link metaKeyScope} returns `'device'` for every Meta key NOT listed here, so a
864
+ * newly-added key can never SILENTLY churn the shared file the way a
865
+ * central-by-default would — the very trap behind the recurring agents.yaml churn.
866
+ * Making a key fleet-shared is now a deliberate edit HERE, not the path of least
867
+ * resistance (PHNX-3315).
870
868
  */
871
- const META_KEY_SCOPE = {
872
- // Device-local — writeMetaUnlocked destructures these out; never in central.
873
- agents: 'device',
874
- isolatedAgents: 'device',
875
- versions: 'device',
876
- deviceRoutines: 'device',
877
- deviceConfig: 'device',
878
- deviceBrowser: 'device',
879
- deviceFleet: 'device',
880
- deviceHosts: 'device',
881
- deviceAccounts: 'device',
882
- // Central — synced via agents.yaml.
883
- accounts: 'central',
884
- run: 'central',
885
- model: 'central',
886
- watchdog: 'central',
887
- lease: 'central',
888
- secrets: 'central',
889
- budget: 'central',
890
- feed: 'central',
891
- beta: 'central',
892
- registries: 'central',
893
- profiles: 'central',
894
- source: 'central',
895
- projectRoot: 'device',
896
- extraRepos: 'central',
897
- brands: 'central',
898
- actors: 'central',
899
- seededPresets: 'central',
900
- hooks: 'central',
901
- config: 'central',
902
- hosts: 'central',
903
- fleet: 'central',
904
- share: 'central',
905
- notify: 'central',
906
- };
869
+ const CENTRAL_META_KEYS = [
870
+ 'accounts',
871
+ 'run',
872
+ 'model',
873
+ 'watchdog',
874
+ 'lease',
875
+ 'secrets',
876
+ 'budget',
877
+ 'feed',
878
+ 'beta',
879
+ 'registries',
880
+ 'profiles',
881
+ 'source',
882
+ 'extraRepos',
883
+ 'brands',
884
+ 'actors',
885
+ 'seededPresets',
886
+ 'hooks',
887
+ 'config',
888
+ 'hosts',
889
+ 'fleet',
890
+ 'share',
891
+ 'notify',
892
+ ];
893
+ /**
894
+ * Device-scoped Meta keys with BESPOKE routing — each lands in a special file
895
+ * (pins JSON / version-resources JSON) or a REMAPPED device-doc sub-block
896
+ * (`deviceHosts`->`hosts:`, `deviceFleet`->`fleet:`, ...), so their handling is
897
+ * hand-written in {@link writeMetaUnlocked} / {@link overlayMachineLocal} and kept
898
+ * behavior-identical. A device-scoped key NOT listed here is a GENERIC device key:
899
+ * it round-trips through `devices/<host>/agents.yaml` under its OWN name with no
900
+ * bespoke wiring (see the generic loops in those two functions). The `browser`
901
+ * tombstone is bespoke too — drained by lib/browser/registry.ts.
902
+ */
903
+ const BESPOKE_DEVICE_KEYS = [
904
+ 'agents',
905
+ 'isolatedAgents',
906
+ 'versions',
907
+ 'deviceRoutines',
908
+ 'deviceConfig',
909
+ 'deviceBrowser',
910
+ 'deviceFleet',
911
+ 'deviceHosts',
912
+ 'deviceAccounts',
913
+ 'projectRoot',
914
+ ];
915
+ const CENTRAL_KEY_SET = new Set(CENTRAL_META_KEYS);
916
+ const _metaKeysAreExhaustive = true;
917
+ void _metaKeysAreExhaustive;
918
+ /**
919
+ * Sync-domain of a Meta key. `'central'` ONLY for the opt-in allowlist; `'device'`
920
+ * by DEFAULT for everything else — so the classification is authoritative (it
921
+ * DRIVES the generic device-doc router below) rather than a decorative string map,
922
+ * and forgetting to classify a new key routes it to the SAFE per-box file.
923
+ */
924
+ function metaKeyScope(key) {
925
+ return CENTRAL_KEY_SET.has(key) ? 'central' : 'device';
926
+ }
927
+ /** Bespoke device keys as a runtime Set (the generic router skips these). */
928
+ const BESPOKE_DEVICE_KEY_SET = new Set([
929
+ ...BESPOKE_DEVICE_KEYS,
930
+ // The `browser` tombstone is device-scoped but bespoke: lib/browser/registry.ts
931
+ // drains it (collision-checked) into deviceBrowser, so the generic router must
932
+ // never blindly relocate it.
933
+ 'browser',
934
+ ]);
935
+ /**
936
+ * Device-doc sub-block names the read/write paths handle BESPOKE-ly (each maps to
937
+ * a different `device*` Meta key, or lives in a separate file). The generic
938
+ * device-doc overlay skips these so it only surfaces genuine generic keys.
939
+ */
940
+ const BESPOKE_DEVICE_DOC_KEYS = new Set([
941
+ 'agents',
942
+ 'isolatedAgents',
943
+ 'routines',
944
+ 'config',
945
+ 'browser',
946
+ 'fleet',
947
+ 'hosts',
948
+ 'accounts',
949
+ 'projectRoot',
950
+ // Legacy top-level doc key the config migration folds into `config:`; never
951
+ // surfaced onto Meta before, so keep the generic overlay from doing so.
952
+ 'defaultBrowserProfile',
953
+ ]);
907
954
  /**
908
955
  * Every key this version models (central + device). serializeCentral deletes an
909
956
  * on-disk key only when it is KNOWN and absent from the write's in-memory object:
@@ -913,7 +960,8 @@ const META_KEY_SCOPE = {
913
960
  * a newer CLI version added — is preserved verbatim, never dropped + synced away.
914
961
  */
915
962
  const KNOWN_META_KEYS = new Set([
916
- ...Object.keys(META_KEY_SCOPE),
963
+ ...CENTRAL_META_KEYS,
964
+ ...BESPOKE_DEVICE_KEYS,
917
965
  // Removed browser-profile store. Kept only as a serializer tombstone so the
918
966
  // first registry read can migrate it into this device's file and delete it.
919
967
  'browser',
@@ -947,6 +995,56 @@ function healMetaHeader(serialized) {
947
995
  // real central change). A current header round-trips to the identical bytes.
948
996
  return stripped === serialized ? serialized : META_HEADER + stripped;
949
997
  }
998
+ /**
999
+ * True when the top-level `~/.agents/agents.yaml` carries a metadata header that
1000
+ * is NOT the canonical {@link META_HEADER} — the P1 frozen-header case a box only
1001
+ * heals on its next central write (serializeCentral). An absent or headerless
1002
+ * file is NOT stale (a headerless central file is deliberately left alone).
1003
+ * Surfaced as config drift by `agents sync status` (PHNX-3315).
1004
+ */
1005
+ export function hasStaleMetaHeader() {
1006
+ let content;
1007
+ try {
1008
+ content = fs.readFileSync(META_FILE, 'utf-8');
1009
+ }
1010
+ catch {
1011
+ return false;
1012
+ }
1013
+ return healMetaHeader(content) !== content;
1014
+ }
1015
+ /**
1016
+ * Parse the top-level user `agents.yaml` (this box's synced central file) WITHOUT
1017
+ * the system-repo merge, machine-local overlay, or cache — the raw on-disk central
1018
+ * map, or null when the file is absent/unparseable. Used by config-drift detection
1019
+ * to see the central blocks that should have folded into the device doc, without
1020
+ * mis-attributing a system-repo default as this box's own leak (PHNX-3315).
1021
+ */
1022
+ export function readTopLevelUserMeta() {
1023
+ let content;
1024
+ try {
1025
+ content = fs.readFileSync(META_FILE, 'utf-8');
1026
+ }
1027
+ catch {
1028
+ return null;
1029
+ }
1030
+ try {
1031
+ const parsed = yaml.parse(content);
1032
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1033
+ return parsed;
1034
+ }
1035
+ }
1036
+ catch { /* malformed — nothing to report */ }
1037
+ return null;
1038
+ }
1039
+ /**
1040
+ * Top-level keys currently on disk in the central `agents.yaml` ({} when the file
1041
+ * is absent or unparseable). The generic device-doc router uses this to leave a
1042
+ * FOREIGN key — one this version does not model, already written to central by a
1043
+ * newer CLI — in place instead of relocating it to this box's device doc.
1044
+ */
1045
+ function readCentralKeys() {
1046
+ return new Set(Object.keys(readTopLevelUserMeta() ?? {}));
1047
+ }
950
1048
  /**
951
1049
  * Serialize the central (synced) meta to `agents.yaml` WITHOUT destroying the
952
1050
  * hand-written comments in the committed file.
@@ -1035,6 +1133,10 @@ function serializeCentral(central) {
1035
1133
  * read-snapshot-then-separately-lock race that {@link updateMeta} would impose.
1036
1134
  */
1037
1135
  export function writeMetaUnlocked(meta) {
1136
+ // INVARIANT: every key destructured here must also be in BESPOKE_DEVICE_KEYS (and
1137
+ // vice versa) — a bespoke device key that is classified but NOT pulled out here
1138
+ // would fall into `central`, and the generic router skips it (BESPOKE_DEVICE_KEY_SET),
1139
+ // so it would silently sync to the shared file. Keep the two lists in lockstep.
1038
1140
  const { agents, isolatedAgents, versions, deviceRoutines, deviceConfig, deviceBrowser, deviceFleet, deviceHosts, deviceAccounts, projectRoot, ...central } = meta;
1039
1141
  // Write the machine-local files FIRST, then strip central — so a crash mid-write
1040
1142
  // never removes pins/versions from central before they're persisted elsewhere.
@@ -1140,6 +1242,27 @@ export function writeMetaUnlocked(meta) {
1140
1242
  doc.projectRoot = projectRoot;
1141
1243
  else
1142
1244
  delete doc.projectRoot;
1245
+ // Generic device-scoped keys (PHNX-3315): any key left in `central` that this
1246
+ // version classifies as device but does NOT bespoke-route round-trips through
1247
+ // this box's device doc under its OWN name — so a NEWLY DECLARED device-scoped
1248
+ // key lands per-box BY DEFAULT, with no bespoke wiring, and never reaches the
1249
+ // synced central file. Today the bespoke set covers every device key, so this
1250
+ // loop moves nothing (behavior-identical). A key that is UNKNOWN to this version
1251
+ // AND already present in the on-disk central file is a FOREIGN key a newer CLI
1252
+ // wrote as central — left in `central` and preserved verbatim by serializeCentral
1253
+ // (the fleet-wide config-loss guard), never relocated to this box's doc.
1254
+ const centralRecord = central;
1255
+ const onDiskCentralKeys = readCentralKeys();
1256
+ for (const k of Object.keys(centralRecord)) {
1257
+ if (metaKeyScope(k) !== 'device')
1258
+ continue; // fleet-shared: stays central
1259
+ if (BESPOKE_DEVICE_KEY_SET.has(k))
1260
+ continue; // bespoke (incl. browser tombstone)
1261
+ if (!KNOWN_META_KEYS.has(k) && onDiskCentralKeys.has(k))
1262
+ continue; // foreign — preserve
1263
+ doc[k] = centralRecord[k];
1264
+ delete centralRecord[k];
1265
+ }
1143
1266
  if (Object.keys(doc).length > 0) {
1144
1267
  fs.mkdirSync(path.dirname(devicePath), { recursive: true });
1145
1268
  writeIfChanged(devicePath, META_HEADER + yaml.stringify(doc));
@@ -1259,6 +1382,15 @@ function overlayMachineLocal(meta) {
1259
1382
  }
1260
1383
  meta.deviceRoutines = dm.routines;
1261
1384
  }
1385
+ // Generic device-scoped keys (PHNX-3315): device-doc keys this overlay does
1386
+ // NOT bespoke-handle map straight back onto Meta under their own name —
1387
+ // symmetry with the generic write in writeMetaUnlocked. The bespoke sub-blocks
1388
+ // are handled above and skipped, so a stock device doc surfaces nothing extra.
1389
+ for (const [k, v] of Object.entries(dm)) {
1390
+ if (BESPOKE_DEVICE_DOC_KEYS.has(k))
1391
+ continue;
1392
+ meta[k] = v;
1393
+ }
1262
1394
  }
1263
1395
  }
1264
1396
  const vrPath = getVersionResourcesPath();
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { AgentId } from './types.js';
22
22
  import { type DoctorKind } from './doctor-diff.js';
23
+ import { type ConfigDrift } from './config-drift.js';
23
24
  /**
24
25
  * One stable status per resource, unified across every surface.
25
26
  * - `synced` — installed copy matches the resolved source (DiffStatus 'ok').
@@ -76,6 +77,9 @@ export interface UserRepoStatus {
76
77
  export interface UnifiedSyncStatus {
77
78
  system: SystemRepoStatus;
78
79
  user: UserRepoStatus;
80
+ /** Config drift: has this box drained its device-scoped state, or is it still
81
+ * carrying per-box state in the shared top-level agents.yaml? (PHNX-3315) */
82
+ config: ConfigDrift;
79
83
  agents: AgentVersionStatus[];
80
84
  totals: {
81
85
  drifted: number;
@@ -26,6 +26,7 @@ import { loadManifest } from './staleness/index.js';
26
26
  import { getSystemAgentsDir, getUserAgentsDir } from './state.js';
27
27
  import * as fs from 'fs';
28
28
  import { isGitRepo, readOriginUrl } from './git.js';
29
+ import { detectConfigDrift } from './config-drift.js';
29
30
  const STATUS_MAP = {
30
31
  ok: 'synced',
31
32
  diff: 'drifted',
@@ -125,6 +126,7 @@ export async function computeSyncStatus(options = {}) {
125
126
  }
126
127
  const system = await getSystemRepoStatus();
127
128
  const user = await getUserRepoStatus();
129
+ const config = detectConfigDrift();
128
130
  const agentsNeedingSync = new Set();
129
131
  let drifted = 0, missing = 0, orphan = 0, versionsNeedingSync = 0, versionsNeverSynced = 0;
130
132
  for (const v of agents) {
@@ -141,6 +143,7 @@ export async function computeSyncStatus(options = {}) {
141
143
  return {
142
144
  system,
143
145
  user,
146
+ config,
144
147
  agents,
145
148
  totals: {
146
149
  drifted,
@@ -4,29 +4,34 @@ function normalizedEvidence(input) {
4
4
  .join(' ')
5
5
  .toLowerCase();
6
6
  }
7
+ const TOPIC_RULES = [
8
+ { group: 'review', key: 'code-review', label: 'Code review',
9
+ text: /\b(review|audit|pr[-_/ ]?review|code[-_/ ]?review)\b/, tool: /review|comment/ },
10
+ { group: 'ops', key: 'release', label: 'Release',
11
+ text: /\b(release|deploy|publish|rollout|changelog)\b/ },
12
+ { group: 'research', key: 'debugging', label: 'Debugging',
13
+ text: /\b(debug|investigat|repro|root[-_ ]?cause|traceback|stack ?trace)\b/ },
14
+ { group: 'content', key: 'content', label: 'Blog & docs',
15
+ text: /\b(blog|docs?|documentation|readme|post|article|content|caption)\b/ },
16
+ { group: 'research', key: 'research', label: 'Research',
17
+ text: /\b(research|analysis|benchmark|paper|explore|spike)\b/, tool: /web_search|search_query|webfetch|browser/ },
18
+ { group: 'code', key: 'bugfix', label: 'Bug fixes',
19
+ text: /\b(fix|bug|hotfix|patch)\b/ },
20
+ { group: 'code', key: 'refactor', label: 'Refactor',
21
+ text: /\b(refactor|cleanup|clean[-_ ]?up|restructure|rename|tidy)\b/ },
22
+ { group: 'ops', key: 'operations', label: 'Fleet / ops',
23
+ text: /\b(fleet|ci|infra|daemon|provision|ops|pipeline)\b/, tool: /ssh|deploy|computer/ },
24
+ { group: 'code', key: 'feature', label: 'Feature work',
25
+ text: /\b(feat|feature|implement|add|build|code|test)\b/, tool: /edit|write|apply_patch|exec|bash|shell/ },
26
+ ];
7
27
  /** Classify a session from metadata and aggregate tool names; never reads transcript text. */
8
28
  export function classifyTopic(input) {
9
29
  const text = normalizedEvidence(input);
10
30
  const tools = Object.keys(input.toolMix ?? {}).map((tool) => tool.toLowerCase());
11
- const hasTool = (pattern) => tools.some((tool) => pattern.test(tool));
12
- if (/\b(review|audit|pr[-_/ ]?review|code[-_/ ]?review)\b/.test(text)
13
- || hasTool(/review|comment/)) {
14
- return { group: 'review', key: 'code-review', label: 'Code review' };
15
- }
16
- if (/\b(research|investigat|analysis|benchmark|paper|docs?)\b/.test(text)
17
- || hasTool(/web_search|search_query|webfetch|browser/)) {
18
- return { group: 'research', key: 'research', label: 'Research' };
19
- }
20
- if (/\b(blog|copy|content|post|article|script|caption)\b/.test(text)) {
21
- return { group: 'content', key: 'content', label: 'Content' };
22
- }
23
- if (/\b(deploy|release|infra|ci|ops|fleet|daemon|provision)\b/.test(text)
24
- || hasTool(/ssh|deploy|computer/)) {
25
- return { group: 'ops', key: 'operations', label: 'Operations' };
26
- }
27
- if (/\b(test|fix|bug|feat|refactor|implement|code)\b/.test(text)
28
- || hasTool(/edit|write|apply_patch|exec|bash|shell/)) {
29
- return { group: 'code', key: 'engineering', label: 'Engineering' };
31
+ for (const rule of TOPIC_RULES) {
32
+ if ((rule.text && rule.text.test(text)) || (rule.tool && tools.some((tool) => rule.tool.test(tool)))) {
33
+ return { group: rule.group, key: rule.key, label: rule.label };
34
+ }
30
35
  }
31
36
  return { group: 'research', key: 'general', label: 'General' };
32
37
  }
@@ -245,7 +245,8 @@ export async function buildLocalUsageAccounts() {
245
245
  // fileOnly: never open the ACL-bound keychain item from the daemon —
246
246
  // that path is the Touch ID storm. Usage reads the file-based setup-token
247
247
  // only, never the interactive login (see loadClaudeOauth); no setup-token
248
- // reads as "usage pending".
248
+ // reads as "usage unavailable (no usage credential)" — "usage pending"
249
+ // is now the cold-cache state only (#2987).
249
250
  fetch: async () => {
250
251
  const { getUsageInfoForIdentity } = await import('./accounting/usage.js');
251
252
  return getUsageInfoForIdentity({
@@ -21,6 +21,13 @@ export interface ViewJsonVersion {
21
21
  amount: number;
22
22
  currency: string;
23
23
  } | null;
24
+ /**
25
+ * Human-readable reason a usage snapshot is absent: a live-refresh failure, or
26
+ * (without `--refresh`, on a never-cached account) a plain "not collected yet"
27
+ * pending state. Always a full sentence — never the internal `'stale'` cache
28
+ * sentinel, which `usageErrorForDisplay` normalizes away (PHNX-3348).
29
+ */
30
+ usageError?: string | null;
24
31
  windows: Array<{
25
32
  key: 'session' | 'week' | 'sonnet_week' | 'month';
26
33
  label?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.53",
3
+ "version": "1.22.55",
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",
@@ -86,8 +86,11 @@
86
86
  "@homebridge/node-pty-prebuilt-multiarch"
87
87
  ],
88
88
  "dependencies": {
89
+ "@fontsource/inter": "^5.3.0",
90
+ "@fontsource/jetbrains-mono": "^5.3.0",
89
91
  "@homebridge/node-pty-prebuilt-multiarch": "0.13.1",
90
92
  "@inquirer/prompts": "8.5.2",
93
+ "@resvg/resvg-wasm": "^2.6.2",
91
94
  "@types/proper-lockfile": "4.1.4",
92
95
  "@xterm/headless": "6.0.0",
93
96
  "@zed-industries/agent-client-protocol": "0.4.5",
@@ -98,17 +101,20 @@
98
101
  "commander": "15.0.0",
99
102
  "croner": "10.0.1",
100
103
  "diff": "9.0.0",
104
+ "esbuild": "^0.28.2",
101
105
  "lru-cache": "11.5.2",
102
106
  "marked": "15.0.12",
103
107
  "marked-terminal": "7.3.0",
104
108
  "ora": "9.4.1",
105
109
  "proper-lockfile": "4.1.2",
110
+ "satori": "0.12.2",
106
111
  "shlex": "3.0.0",
107
112
  "simple-git": "3.36.0",
108
113
  "smol-toml": "1.7.0",
109
114
  "unbash": "4.0.5",
110
115
  "ws": "^8.21.0",
111
- "yaml": "2.9.0"
116
+ "yaml": "2.9.0",
117
+ "yoga-wasm-web": "^0.3.3"
112
118
  },
113
119
  "devDependencies": {
114
120
  "@types/diff": "8.0.0",
@@ -116,6 +122,7 @@
116
122
  "@types/node": "26.1.0",
117
123
  "@types/ssh2": "1.15.5",
118
124
  "@types/ws": "^8.18.1",
125
+ "miniflare": "4.20260730.0",
119
126
  "ssh2": "1.17.0",
120
127
  "tsx": "4.23.0",
121
128
  "typescript": "6.0.3",