@phnx-labs/agents-cli 1.20.92 → 1.21.0

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 (146) hide show
  1. package/CHANGELOG.md +385 -0
  2. package/README.md +3 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/beta.js +3 -5
  5. package/dist/commands/doctor.js +23 -0
  6. package/dist/commands/events.js +91 -1
  7. package/dist/commands/exec.js +13 -0
  8. package/dist/commands/mailboxes.js +39 -1
  9. package/dist/commands/message.js +12 -1
  10. package/dist/commands/packages.js +14 -5
  11. package/dist/commands/projects.d.ts +38 -0
  12. package/dist/commands/projects.js +355 -48
  13. package/dist/commands/routines.js +27 -1
  14. package/dist/commands/rules.js +6 -0
  15. package/dist/commands/secrets.d.ts +17 -0
  16. package/dist/commands/secrets.js +198 -7
  17. package/dist/commands/send.d.ts +14 -12
  18. package/dist/commands/send.js +105 -35
  19. package/dist/commands/sessions.d.ts +17 -0
  20. package/dist/commands/sessions.js +16 -8
  21. package/dist/commands/setup-browser.js +5 -1
  22. package/dist/commands/setup-fleet.js +5 -0
  23. package/dist/commands/setup-preferences.d.ts +53 -0
  24. package/dist/commands/setup-preferences.js +142 -0
  25. package/dist/commands/setup.js +8 -9
  26. package/dist/commands/ssh.js +220 -10
  27. package/dist/commands/sync.d.ts +2 -2
  28. package/dist/commands/sync.js +13 -12
  29. package/dist/commands/view.js +4 -0
  30. package/dist/commands/watchdog.js +26 -3
  31. package/dist/index.js +38 -6
  32. package/dist/lib/activity.d.ts +15 -0
  33. package/dist/lib/activity.js +26 -2
  34. package/dist/lib/auto-dispatch.d.ts +6 -1
  35. package/dist/lib/auto-dispatch.js +7 -2
  36. package/dist/lib/beta.d.ts +1 -1
  37. package/dist/lib/beta.js +1 -1
  38. package/dist/lib/channels/send.d.ts +83 -0
  39. package/dist/lib/channels/send.js +112 -0
  40. package/dist/lib/daemon.d.ts +13 -0
  41. package/dist/lib/daemon.js +80 -60
  42. package/dist/lib/device-config.d.ts +82 -0
  43. package/dist/lib/device-config.js +296 -0
  44. package/dist/lib/event-provenance.d.ts +19 -0
  45. package/dist/lib/event-provenance.js +48 -0
  46. package/dist/lib/events-ingest.d.ts +46 -0
  47. package/dist/lib/events-ingest.js +182 -0
  48. package/dist/lib/events.d.ts +17 -5
  49. package/dist/lib/events.js +59 -56
  50. package/dist/lib/feed.d.ts +1 -1
  51. package/dist/lib/feed.js +2 -0
  52. package/dist/lib/git.d.ts +13 -2
  53. package/dist/lib/git.js +38 -6
  54. package/dist/lib/linear-project-counts.d.ts +129 -0
  55. package/dist/lib/linear-project-counts.js +206 -0
  56. package/dist/lib/linear-projects.d.ts +62 -0
  57. package/dist/lib/linear-projects.js +144 -0
  58. package/dist/lib/mailbox-gc.js +30 -7
  59. package/dist/lib/mailbox.d.ts +14 -1
  60. package/dist/lib/mailbox.js +35 -3
  61. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  62. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  63. package/dist/lib/menubar/install-menubar.d.ts +5 -21
  64. package/dist/lib/menubar/install-menubar.js +35 -44
  65. package/dist/lib/menubar/notify-desktop.d.ts +17 -2
  66. package/dist/lib/menubar/notify-desktop.js +8 -2
  67. package/dist/lib/migrate.js +1 -1
  68. package/dist/lib/models.js +21 -11
  69. package/dist/lib/overdue.d.ts +1 -1
  70. package/dist/lib/overdue.js +54 -19
  71. package/dist/lib/project-import.d.ts +96 -0
  72. package/dist/lib/project-import.js +171 -0
  73. package/dist/lib/project-probe.d.ts +75 -0
  74. package/dist/lib/project-probe.js +160 -0
  75. package/dist/lib/project-resources.d.ts +8 -0
  76. package/dist/lib/project-resources.js +31 -3
  77. package/dist/lib/project-status.d.ts +32 -1
  78. package/dist/lib/project-status.js +82 -1
  79. package/dist/lib/projects.d.ts +6 -0
  80. package/dist/lib/projects.js +12 -0
  81. package/dist/lib/remote-agents-json.d.ts +14 -1
  82. package/dist/lib/remote-agents-json.js +21 -3
  83. package/dist/lib/routine-notify.d.ts +11 -0
  84. package/dist/lib/routine-notify.js +22 -0
  85. package/dist/lib/routines.d.ts +53 -0
  86. package/dist/lib/routines.js +105 -4
  87. package/dist/lib/rules/run-sync.d.ts +18 -0
  88. package/dist/lib/rules/run-sync.js +92 -0
  89. package/dist/lib/run-notify.js +3 -0
  90. package/dist/lib/scheduler.js +12 -2
  91. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  92. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  93. package/dist/lib/secrets/audit.d.ts +1 -1
  94. package/dist/lib/secrets/audit.js +53 -10
  95. package/dist/lib/secrets/index.d.ts +4 -5
  96. package/dist/lib/secrets/index.js +4 -5
  97. package/dist/lib/secrets/list-filter.d.ts +20 -5
  98. package/dist/lib/secrets/list-filter.js +22 -6
  99. package/dist/lib/secrets/usage-db.d.ts +106 -0
  100. package/dist/lib/secrets/usage-db.js +236 -0
  101. package/dist/lib/session/active.d.ts +16 -2
  102. package/dist/lib/session/active.js +8 -4
  103. package/dist/lib/session/bundle.js +5 -1
  104. package/dist/lib/session/remote-active.d.ts +5 -1
  105. package/dist/lib/session/remote-active.js +4 -1
  106. package/dist/lib/session/remote-list.d.ts +1 -15
  107. package/dist/lib/session/remote-list.js +22 -111
  108. package/dist/lib/session/sync/config.d.ts +13 -5
  109. package/dist/lib/session/sync/config.js +21 -13
  110. package/dist/lib/sqlite.js +28 -1
  111. package/dist/lib/startup/command-registry.d.ts +0 -2
  112. package/dist/lib/startup/command-registry.js +1 -5
  113. package/dist/lib/state.d.ts +19 -2
  114. package/dist/lib/state.js +99 -10
  115. package/dist/lib/sync-umbrella.d.ts +11 -23
  116. package/dist/lib/sync-umbrella.js +14 -45
  117. package/dist/lib/teams/agents.d.ts +35 -3
  118. package/dist/lib/teams/agents.js +52 -8
  119. package/dist/lib/teams/scheduler.d.ts +29 -2
  120. package/dist/lib/teams/scheduler.js +60 -16
  121. package/dist/lib/types.d.ts +22 -5
  122. package/dist/lib/versions.d.ts +6 -0
  123. package/dist/lib/versions.js +6 -4
  124. package/package.json +1 -2
  125. package/dist/commands/drive.d.ts +0 -10
  126. package/dist/commands/drive.js +0 -183
  127. package/dist/commands/hq.d.ts +0 -2
  128. package/dist/commands/hq.js +0 -58
  129. package/dist/commands/sessions-sync.d.ts +0 -17
  130. package/dist/commands/sessions-sync.js +0 -135
  131. package/dist/commands/sync-provision.d.ts +0 -23
  132. package/dist/commands/sync-provision.js +0 -107
  133. package/dist/lib/drive-sync.d.ts +0 -45
  134. package/dist/lib/drive-sync.js +0 -238
  135. package/dist/lib/hq/floor.d.ts +0 -87
  136. package/dist/lib/hq/floor.js +0 -243
  137. package/dist/lib/session/sync/crdt.d.ts +0 -44
  138. package/dist/lib/session/sync/crdt.js +0 -119
  139. package/dist/lib/session/sync/manifest.d.ts +0 -62
  140. package/dist/lib/session/sync/manifest.js +0 -100
  141. package/dist/lib/session/sync/provision.d.ts +0 -49
  142. package/dist/lib/session/sync/provision.js +0 -91
  143. package/dist/lib/session/sync/r2.d.ts +0 -32
  144. package/dist/lib/session/sync/r2.js +0 -121
  145. package/dist/lib/session/sync/sync.d.ts +0 -106
  146. package/dist/lib/session/sync/sync.js +0 -374
@@ -14,11 +14,38 @@
14
14
  import { createRequire } from 'module';
15
15
  const isBun = typeof globalThis.Bun !== 'undefined';
16
16
  const require = createRequire(import.meta.url);
17
+ // node:sqlite emits a process-level ExperimentalWarning the first time it loads.
18
+ // The packaged CLI launches Node with --no-warnings=ExperimentalWarning, but a
19
+ // direct `node dist/...` run (and vitest's subprocesses) does not, so the warning
20
+ // would leak onto stderr and break any command whose --json output is asserted to
21
+ // be clean. Suppress only that single warning for the duration of the load; every
22
+ // other warning passes through untouched.
23
+ function loadNodeSqlite() {
24
+ const original = process.emitWarning;
25
+ const filtered = ((warning, ...rest) => {
26
+ const name = warning instanceof Error
27
+ ? warning.name
28
+ : typeof rest[0] === 'string'
29
+ ? rest[0]
30
+ : rest[0]?.type;
31
+ const message = warning instanceof Error ? warning.message : warning;
32
+ if (name === 'ExperimentalWarning' && /SQLite/i.test(String(message ?? '')))
33
+ return;
34
+ original.call(process, warning, ...rest);
35
+ });
36
+ process.emitWarning = filtered;
37
+ try {
38
+ return require('node:sqlite');
39
+ }
40
+ finally {
41
+ process.emitWarning = original;
42
+ }
43
+ }
17
44
  // Keep Node on createRequire() so Vitest doesn't try to prebundle the built-in
18
45
  // sqlite module as a userland package during test collection.
19
46
  const sqliteMod = isBun
20
47
  ? await import('bun:sqlite')
21
- : require('node:sqlite');
48
+ : loadNodeSqlite();
22
49
  // bun:sqlite exports `Database`; node:sqlite exports `DatabaseSync`.
23
50
  const NativeDatabase = sqliteMod.Database
24
51
  ?? sqliteMod.DatabaseSync;
@@ -67,7 +67,6 @@ export declare const loadBeta: ModuleLoader;
67
67
  export declare const loadSync: ModuleLoader;
68
68
  export declare const loadLock: ModuleLoader;
69
69
  export declare const loadRefreshRules: ModuleLoader;
70
- export declare const loadDrive: ModuleLoader;
71
70
  export declare const loadFactory: ModuleLoader;
72
71
  export declare const loadUsage: ModuleLoader;
73
72
  export declare const loadCost: ModuleLoader;
@@ -96,7 +95,6 @@ export declare const loadTeams: ModuleLoader;
96
95
  export declare const loadCloud: ModuleLoader;
97
96
  export declare const loadMessage: ModuleLoader;
98
97
  export declare const loadSend: ModuleLoader;
99
- export declare const loadHq: ModuleLoader;
100
98
  export declare const loadFeed: ModuleLoader;
101
99
  export declare const loadActivity: ModuleLoader;
102
100
  export declare const loadMailboxes: ModuleLoader;
@@ -45,7 +45,6 @@ export const loadBeta = async () => (await import('../../commands/beta.js')).reg
45
45
  export const loadSync = async () => (await import('../../commands/sync.js')).registerSyncCommand;
46
46
  export const loadLock = async () => (await import('../../commands/lock.js')).registerLockCommand;
47
47
  export const loadRefreshRules = async () => (await import('../../commands/refresh-rules.js')).registerRefreshRulesCommand;
48
- export const loadDrive = async () => (await import('../../commands/drive.js')).registerDriveCommands;
49
48
  export const loadFactory = async () => (await import('../../commands/factory.js')).registerFactoryCommands;
50
49
  export const loadUsage = async () => (await import('../../commands/usage.js')).registerUsageCommand;
51
50
  export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
@@ -74,7 +73,6 @@ export const loadTeams = async () => (await import('../../commands/teams.js')).r
74
73
  export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
75
74
  export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
76
75
  export const loadSend = async () => (await import('../../commands/send.js')).registerSendCommand;
77
- export const loadHq = async () => (await import('../../commands/hq.js')).registerHqCommand;
78
76
  export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
79
77
  export const loadActivity = async () => (await import('../../commands/activity.js')).registerActivityCommand;
80
78
  export const loadMailboxes = async () => (await import('../../commands/mailboxes.js')).registerMailboxesCommand;
@@ -90,7 +88,7 @@ export const loadFunnel = async () => (await import('../../commands/funnel.js'))
90
88
  * inherit the root's custom help formatter rather than getting the per-command
91
89
  * recursive pass. Keeping that ordering preserves their `--help` output exactly.
92
90
  */
93
- export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', 'hq', 'serve']);
91
+ export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', 'serve']);
94
92
  /**
95
93
  * User-typed top-level command name -> ordered list of module loaders to run.
96
94
  *
@@ -161,7 +159,6 @@ export const COMMAND_LOADERS = {
161
159
  sync: [loadSync],
162
160
  lock: [loadLock],
163
161
  'refresh-rules': [loadRefreshRules],
164
- drive: [loadDrive],
165
162
  factory: [loadFactory],
166
163
  usage: [loadUsage],
167
164
  cost: [loadCost],
@@ -199,7 +196,6 @@ export const COMMAND_LOADERS = {
199
196
  message: [loadMessage],
200
197
  send: [loadSend],
201
198
  notify: [loadSend],
202
- hq: [loadHq],
203
199
  feed: [loadFeed],
204
200
  activity: [loadActivity],
205
201
  mailboxes: [loadMailboxes],
@@ -23,6 +23,13 @@
23
23
  * and leaves a back-compat symlink at the old path.
24
24
  */
25
25
  import type { Meta } from './types.js';
26
+ /**
27
+ * Header prepended to every agents.yaml the CLI writes (central and per-device
28
+ * docs). Carries the yaml-language-server schema hint so editors validate the
29
+ * file against `schema/agents-yaml.schema.json`. Exported so
30
+ * `lib/device-config.ts` writes a sibling device's doc with the same header.
31
+ */
32
+ export declare const META_HEADER = "# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agents-cli/main/apps/cli/schema/agents-yaml.schema.json\n\n";
26
33
  /** Root of the system data directory (~/.agents/.system/). */
27
34
  export declare function getAgentsDir(): string;
28
35
  /** Root of the system data directory (~/.agents/.system/). */
@@ -98,6 +105,18 @@ export declare function getUserSubagentsDir(): string;
98
105
  export declare function getSystemWorkflowsDir(): string;
99
106
  export declare function getUserWorkflowsDir(): string;
100
107
  export declare function getUserSecretsDir(): string;
108
+ /**
109
+ * Path to the secrets usage read-model database (~/.agents/secrets/secrets.db).
110
+ * Read at CALL time so a test can redirect it to a temp file via
111
+ * AGENTS_SECRETS_DB without racing the module-load capture of USER_SECRETS_DIR —
112
+ * mirrors the AGENTS_EVENTS_PATH / AGENTS_DEVICES_DIR escape hatches. Holds only
113
+ * value-free usage telemetry (which bundle was created/imported/exported/viewed/
114
+ * accessed/unlocked, when, by whom), never a secret value. It is a derived index
115
+ * fed FROM the emitSecretAudit chokepoint alongside the append-only
116
+ * ~/.agents/events.jsonl audit log — the same way sessions.db indexes session
117
+ * metadata off the real session flow — not a second write path.
118
+ */
119
+ export declare function getSecretsDbPath(): string;
101
120
  export declare function getUserPromptcutsPath(): string;
102
121
  /** Canonical home anchor (HOME env override or os.homedir()). */
103
122
  export declare function getHomeDir(): string;
@@ -182,8 +201,6 @@ export declare function getSystemPluginsDir(): string;
182
201
  export declare function getExtraPluginsDir(alias: string): string;
183
202
  /** Path to a project-scoped plugins directory (<project>/.agents/plugins/), or null when none. */
184
203
  export declare function getProjectPluginsDir(cwd?: string): string | null;
185
- /** Path to synced remote session data (~/.agents/.cache/drive/). */
186
- export declare function getDriveDir(): string;
187
204
  /** Path to soft-deleted resources (~/.agents/.history/trash/). */
188
205
  export declare function getTrashDir(): string;
189
206
  /** Path to local session indexer storage (~/.agents/.history/sessions/). */
package/dist/lib/state.js CHANGED
@@ -26,7 +26,9 @@ import * as fs from 'fs';
26
26
  import * as path from 'path';
27
27
  import * as os from 'os';
28
28
  import * as yaml from 'yaml';
29
+ import { execFileSync } from 'child_process';
29
30
  import { ensureLockTarget, atomicWriteFileSync, withFileLock } from './fs-atomic.js';
31
+ import { DEFAULT_SYSTEM_REPO, systemRepoSlug } from './types.js';
30
32
  import { machineId } from './machine-id.js';
31
33
  const HOME = process.env.HOME ?? os.homedir();
32
34
  /**
@@ -116,7 +118,6 @@ const PACKAGES_DIR = path.join(CACHE_DIR, 'packages');
116
118
  // They live at the user-root so they're git-tracked as source of truth.
117
119
  const PLUGINS_DIR = path.join(USER_AGENTS_DIR, 'plugins');
118
120
  const CLOUD_DIR = path.join(CACHE_DIR, 'cloud');
119
- const DRIVE_DIR = path.join(CACHE_DIR, 'drive');
120
121
  const TERMINALS_DIR = path.join(CACHE_DIR, 'terminals');
121
122
  const LOGS_DIR = path.join(CACHE_DIR, 'logs');
122
123
  /** Disposable performance samples (~/.agents/.cache/perf/) — safe to wipe. */
@@ -144,9 +145,16 @@ const USER_SUBAGENTS_DIR = path.join(USER_AGENTS_DIR, 'subagents');
144
145
  const USER_WORKFLOWS_DIR = path.join(USER_AGENTS_DIR, 'workflows');
145
146
  const USER_SECRETS_DIR = path.join(USER_AGENTS_DIR, 'secrets');
146
147
  const USER_PROMPTCUTS_FILE = path.join(USER_AGENTS_DIR, 'hooks', 'promptcuts.yaml');
147
- const META_HEADER = `# agents-cli metadata
148
+ /**
149
+ * Header prepended to every agents.yaml the CLI writes (central and per-device
150
+ * docs). Carries the yaml-language-server schema hint so editors validate the
151
+ * file against `schema/agents-yaml.schema.json`. Exported so
152
+ * `lib/device-config.ts` writes a sibling device's doc with the same header.
153
+ */
154
+ export const META_HEADER = `# agents-cli metadata
148
155
  # Auto-generated - do not edit manually
149
156
  # https://github.com/phnx-labs/agents-cli
157
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agents-cli/main/apps/cli/schema/agents-yaml.schema.json
150
158
 
151
159
  `;
152
160
  // ─── Root getters ─────────────────────────────────────────────────────────────
@@ -190,13 +198,69 @@ export function getOptionalUserAgentsDir() {
190
198
  }
191
199
  return USER_AGENTS_DIR;
192
200
  }
201
+ /**
202
+ * Origin `owner/repo` slug (lowercased, `.git` stripped) of a git checkout, or
203
+ * null when `dir` isn't a git repo / has no origin. Extracts the slug from any
204
+ * remote URL form: `git@host:owner/repo.git`, `https://host/owner/repo.git`,
205
+ * `ssh://git@host/owner/repo`. Sync (mirrors readGitConfigUser in git.ts) so it
206
+ * can be used from the synchronous getProjectAgentsDir walk.
207
+ */
208
+ function gitOriginSlug(dir) {
209
+ let url;
210
+ try {
211
+ url = execFileSync('git', ['-C', dir, 'config', '--get', 'remote.origin.url'], {
212
+ stdio: ['ignore', 'pipe', 'ignore'],
213
+ }).toString().trim();
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ if (!url)
219
+ return null;
220
+ const m = url.replace(/\.git$/i, '').match(/[:/]([^/:]+\/[^/]+)$/);
221
+ return m ? m[1].toLowerCase() : null;
222
+ }
223
+ /** Slugs of the user + system DotAgents repos — a checkout of any of these is not a project layer. */
224
+ function canonicalDotAgentsRepoSlugs() {
225
+ const slugs = new Set([systemRepoSlug(DEFAULT_SYSTEM_REPO).toLowerCase()]);
226
+ for (const dir of [USER_AGENTS_DIR, SYSTEM_AGENTS_DIR]) {
227
+ const slug = gitOriginSlug(dir);
228
+ if (slug)
229
+ slugs.add(slug);
230
+ }
231
+ return slugs;
232
+ }
233
+ /**
234
+ * True when `agentsPath` is itself a git checkout of the user's or system's
235
+ * DotAgents repo — i.e. a *clone* of the very repo whose rules already load as
236
+ * the user/system layer (e.g. `git clone …/.agents.git ~/src/github.com/<you>/.agents`).
237
+ * Such a clone must NOT also be treated as a *project* layer: because project
238
+ * outranks user, a stale clone would silently shadow the live user rules by
239
+ * filename and plant a compiled AGENTS.md in an ancestor dir (RUSH-2037).
240
+ *
241
+ * A legitimate project layer is a plain subdirectory of a project and is never
242
+ * itself a git-repo root, so the cheap `.git` gate skips the (git-spawning)
243
+ * origin comparison for the common case — only a `.agents/` that is its own
244
+ * checkout pays it, and even then it's kept unless its origin matches the
245
+ * user/system DotAgents repo (an unrelated repo checked out at `.agents/`,
246
+ * or a project's own versioned `.agents/`, stays a valid project layer).
247
+ */
248
+ function isUserOrSystemRepoCheckout(agentsPath) {
249
+ if (!fs.existsSync(path.join(agentsPath, '.git')))
250
+ return false;
251
+ const origin = gitOriginSlug(agentsPath);
252
+ if (!origin)
253
+ return false;
254
+ return canonicalDotAgentsRepoSlugs().has(origin);
255
+ }
193
256
  /** Walk up from startPath to find a project-scoped .agents/ directory (skipping both roots). */
194
257
  export function getProjectAgentsDir(startPath = process.cwd()) {
195
258
  let dir = path.resolve(startPath);
196
259
  while (true) {
197
260
  const agentsPath = path.join(dir, '.agents');
198
261
  if (fs.existsSync(agentsPath) && fs.statSync(agentsPath).isDirectory()) {
199
- if (!isSamePath(agentsPath, SYSTEM_AGENTS_DIR) && !isSamePath(agentsPath, USER_AGENTS_DIR)) {
262
+ if (!isSamePath(agentsPath, SYSTEM_AGENTS_DIR) && !isSamePath(agentsPath, USER_AGENTS_DIR)
263
+ && !isUserOrSystemRepoCheckout(agentsPath)) {
200
264
  return agentsPath;
201
265
  }
202
266
  }
@@ -299,6 +363,20 @@ export function getUserSubagentsDir() { return USER_SUBAGENTS_DIR; }
299
363
  export function getSystemWorkflowsDir() { return SYSTEM_WORKFLOWS_DIR; }
300
364
  export function getUserWorkflowsDir() { return USER_WORKFLOWS_DIR; }
301
365
  export function getUserSecretsDir() { return USER_SECRETS_DIR; }
366
+ /**
367
+ * Path to the secrets usage read-model database (~/.agents/secrets/secrets.db).
368
+ * Read at CALL time so a test can redirect it to a temp file via
369
+ * AGENTS_SECRETS_DB without racing the module-load capture of USER_SECRETS_DIR —
370
+ * mirrors the AGENTS_EVENTS_PATH / AGENTS_DEVICES_DIR escape hatches. Holds only
371
+ * value-free usage telemetry (which bundle was created/imported/exported/viewed/
372
+ * accessed/unlocked, when, by whom), never a secret value. It is a derived index
373
+ * fed FROM the emitSecretAudit chokepoint alongside the append-only
374
+ * ~/.agents/events.jsonl audit log — the same way sessions.db indexes session
375
+ * metadata off the real session flow — not a second write path.
376
+ */
377
+ export function getSecretsDbPath() {
378
+ return process.env.AGENTS_SECRETS_DB ?? path.join(USER_SECRETS_DIR, 'secrets.db');
379
+ }
302
380
  export function getUserPromptcutsPath() { return USER_PROMPTCUTS_FILE; }
303
381
  // ─── User operational path getters ────────────────────────────────────────────
304
382
  //
@@ -404,8 +482,6 @@ export function getProjectPluginsDir(cwd = process.cwd()) {
404
482
  return null;
405
483
  return path.join(projectAgentsDir, 'plugins');
406
484
  }
407
- /** Path to synced remote session data (~/.agents/.cache/drive/). */
408
- export function getDriveDir() { return DRIVE_DIR; }
409
485
  /** Path to soft-deleted resources (~/.agents/.history/trash/). */
410
486
  export function getTrashDir() { return TRASH_DIR; }
411
487
  /** Path to local session indexer storage (~/.agents/.history/sessions/). */
@@ -659,8 +735,9 @@ function writeIfChanged(filePath, content) {
659
735
  /**
660
736
  * Partition the in-memory Meta across three files by sync-domain:
661
737
  * - central `~/.agents/agents.yaml` — portable, everything else
738
+ * (including the user-scope `config:` block)
662
739
  * - device `~/.agents/devices/<machine>/agents.yaml` — `agents:` pins +
663
- * `defaultBrowserProfile:` (both per-device)
740
+ * `defaultBrowserProfile:` + device-scope `config:` (all per-device)
664
741
  * - history `~/.agents/.history/version-resources.json` — `versions:` (machine-local)
665
742
  * All callers funnel through writeMeta → here, so nothing else changes. Empty
666
743
  * `agents:` / `versions:` are not written (no empty committed files).
@@ -722,7 +799,7 @@ function serializeCentral(central) {
722
799
  return isEmpty ? META_HEADER : doc.toString({ collectionStyle: 'block' });
723
800
  }
724
801
  function writeMetaUnlocked(meta) {
725
- const { agents, isolatedAgents, versions, defaultBrowserProfile, ...central } = meta;
802
+ const { agents, isolatedAgents, versions, defaultBrowserProfile, deviceConfig, ...central } = meta;
726
803
  // Write the machine-local files FIRST, then strip central — so a crash mid-write
727
804
  // never removes pins/versions from central before they're persisted elsewhere.
728
805
  const devicePath = getDeviceMetaPath();
@@ -733,9 +810,13 @@ function writeMetaUnlocked(meta) {
733
810
  // it does not have.
734
811
  const hasIsolatedAgents = !!isolatedAgents && Object.keys(isolatedAgents).length > 0;
735
812
  const hasDefaultBrowser = !!defaultBrowserProfile;
736
- if (hasAgents || hasIsolatedAgents || hasDefaultBrowser) {
737
- // Device-local doc carries `agents:` pins and `defaultBrowserProfile:` both
738
- // are per-machine and must never land in central agents.yaml (which syncs).
813
+ // Device-scope config (`maxAgents`, `schedulerEnabled`, ) is per-machine, so it
814
+ // rides the device doc under `config:` never the central doc that syncs.
815
+ const hasDeviceConfig = !!deviceConfig && Object.keys(deviceConfig).length > 0;
816
+ if (hasAgents || hasIsolatedAgents || hasDefaultBrowser || hasDeviceConfig) {
817
+ // Device-local doc carries `agents:` pins, `defaultBrowserProfile:`, and the
818
+ // device-scope `config:` — all per-machine and must never land in central
819
+ // agents.yaml (which syncs).
739
820
  const deviceDoc = {};
740
821
  if (hasAgents)
741
822
  deviceDoc.agents = agents;
@@ -743,6 +824,8 @@ function writeMetaUnlocked(meta) {
743
824
  deviceDoc.isolatedAgents = isolatedAgents;
744
825
  if (hasDefaultBrowser)
745
826
  deviceDoc.defaultBrowserProfile = defaultBrowserProfile;
827
+ if (hasDeviceConfig)
828
+ deviceDoc.config = deviceConfig;
746
829
  fs.mkdirSync(path.dirname(devicePath), { recursive: true });
747
830
  writeIfChanged(devicePath, META_HEADER + yaml.stringify(deviceDoc));
748
831
  }
@@ -768,6 +851,10 @@ function writeMetaUnlocked(meta) {
768
851
  * still has pins)
769
852
  * - `defaultBrowserProfile:` from the device file (device is the sole source;
770
853
  * the field is stripped from central on write, so nothing to merge against)
854
+ * - `config:` from the device file into `deviceConfig` (device is the sole
855
+ * source for device-scope config, same routing as `defaultBrowserProfile`;
856
+ * the in-memory field is named `deviceConfig` so it can never collide with
857
+ * the user-scope `config:` central carries)
771
858
  * - `versions:` from the history JSON (wholesale replace; falls back to
772
859
  * whatever central carried when the history file doesn't exist yet)
773
860
  */
@@ -782,6 +869,8 @@ function overlayMachineLocal(meta) {
782
869
  meta.isolatedAgents = { ...meta.isolatedAgents, ...dm.isolatedAgents };
783
870
  if (dm?.defaultBrowserProfile)
784
871
  meta.defaultBrowserProfile = dm.defaultBrowserProfile;
872
+ if (dm?.config)
873
+ meta.deviceConfig = dm.config;
785
874
  }
786
875
  catch { /* ignore malformed device file */ }
787
876
  }
@@ -2,25 +2,23 @@
2
2
  * Umbrella `agents sync` orchestration — "make this machine current".
3
3
  *
4
4
  * Bare `agents sync` fetches the config repos then reconciles them into every
5
- * installed agent's version home. Secrets and sessions are opt-in stages
6
- * (`--secrets` / `--sessions`) — see `planUmbrellaStages` for why they're off by
7
- * default. Each stage is an existing exported library function; this module only
8
- * sequences them and decides — from the flags — which stages run. The planner is
9
- * pure so the flag matrix is unit-tested without any I/O.
5
+ * installed agent's version home. Secrets are an opt-in stage (`--secrets`) —
6
+ * see `planUmbrellaStages` for why it's off by default. Each stage is an
7
+ * existing exported library function; this module only sequences them and
8
+ * decides — from the flags — which stages run. The planner is pure so the
9
+ * flag matrix is unit-tested without any I/O.
10
10
  *
11
11
  * Stage backends:
12
12
  * repos -> git pull of ~/.agents + enabled ~/.agents-* extras (pullRepo)
13
13
  * secrets -> listRemoteBundles + pullBundle (needs a passphrase; skipped
14
14
  * cleanly when none is available — tokenized non-interactive auth
15
15
  * arrives with `agents login`, #366/#367)
16
- * sessions -> syncSessions(), gated by the session-sync beta opt-in + isSyncConfigured(), like the daemon
17
16
  * reconcile-> refresh({ skipPrompts }) — re-materialize resources into homes
18
17
  */
19
- /** The five umbrella flags off `agents sync`. */
18
+ /** The umbrella flags off `agents sync`. */
20
19
  export interface UmbrellaFlags {
21
20
  repos?: boolean;
22
21
  secrets?: boolean;
23
- sessions?: boolean;
24
22
  cloud?: boolean;
25
23
  local?: boolean;
26
24
  }
@@ -28,7 +26,6 @@ export interface UmbrellaFlags {
28
26
  export interface UmbrellaPlan {
29
27
  fetchRepos: boolean;
30
28
  fetchSecrets: boolean;
31
- fetchSessions: boolean;
32
29
  reconcile: boolean;
33
30
  }
34
31
  /**
@@ -36,14 +33,12 @@ export interface UmbrellaPlan {
36
33
  * bare (no flags) fetch repos, then reconcile
37
34
  * --local reconcile only, no fetch
38
35
  * --cloud fetch repos (or the selected subset), skip reconcile
39
- * --repos/--secrets/... fetch only the selected types, then reconcile
36
+ * --repos/--secrets fetch only the selected types, then reconcile
40
37
  * `--local` wins over everything; `--cloud` suppresses reconcile.
41
38
  *
42
- * Secrets and sessions are NOT part of the bare default — they are opt-in via
43
- * `--secrets` / `--sessions`. Pulling every secret bundle onto the machine on a
44
- * bare `agents sync` is more blast radius than the verb should carry by
45
- * default, and session transcripts are queryable on demand (`agents sessions
46
- * --host <machine>`) so they don't need eager mirroring.
39
+ * Secrets are NOT part of the bare default — they are opt-in via `--secrets`.
40
+ * Pulling every secret bundle onto the machine on a bare `agents sync` is more
41
+ * blast radius than the verb should carry by default.
47
42
  */
48
43
  export declare function planUmbrellaStages(f: UmbrellaFlags): UmbrellaPlan;
49
44
  export interface UmbrellaResult {
@@ -58,13 +53,6 @@ export interface UmbrellaResult {
58
53
  reason?: string;
59
54
  errors: string[];
60
55
  };
61
- sessions?: {
62
- ran: boolean;
63
- pushed: number;
64
- pulled: number;
65
- merged: number;
66
- error?: string;
67
- };
68
56
  devices?: {
69
57
  synced: number;
70
58
  pending: number;
@@ -82,7 +70,7 @@ export interface RunUmbrellaArgs {
82
70
  passphrase?: string;
83
71
  }
84
72
  /**
85
- * Execute the planned stages in order: repos -> secrets -> sessions -> reconcile.
73
+ * Execute the planned stages in order: repos -> secrets -> reconcile.
86
74
  * A failure in one fetch stage is recorded and does not abort the others or the
87
75
  * reconcile — `agents sync` should make as much current as it can in one pass.
88
76
  */
@@ -2,18 +2,17 @@
2
2
  * Umbrella `agents sync` orchestration — "make this machine current".
3
3
  *
4
4
  * Bare `agents sync` fetches the config repos then reconciles them into every
5
- * installed agent's version home. Secrets and sessions are opt-in stages
6
- * (`--secrets` / `--sessions`) — see `planUmbrellaStages` for why they're off by
7
- * default. Each stage is an existing exported library function; this module only
8
- * sequences them and decides — from the flags — which stages run. The planner is
9
- * pure so the flag matrix is unit-tested without any I/O.
5
+ * installed agent's version home. Secrets are an opt-in stage (`--secrets`) —
6
+ * see `planUmbrellaStages` for why it's off by default. Each stage is an
7
+ * existing exported library function; this module only sequences them and
8
+ * decides — from the flags — which stages run. The planner is pure so the
9
+ * flag matrix is unit-tested without any I/O.
10
10
  *
11
11
  * Stage backends:
12
12
  * repos -> git pull of ~/.agents + enabled ~/.agents-* extras (pullRepo)
13
13
  * secrets -> listRemoteBundles + pullBundle (needs a passphrase; skipped
14
14
  * cleanly when none is available — tokenized non-interactive auth
15
15
  * arrives with `agents login`, #366/#367)
16
- * sessions -> syncSessions(), gated by the session-sync beta opt-in + isSyncConfigured(), like the daemon
17
16
  * reconcile-> refresh({ skipPrompts }) — re-materialize resources into homes
18
17
  */
19
18
  import { pullRepo } from './git.js';
@@ -24,34 +23,31 @@ import { listRemoteBundles, pullBundle } from './secrets/sync.js';
24
23
  * bare (no flags) fetch repos, then reconcile
25
24
  * --local reconcile only, no fetch
26
25
  * --cloud fetch repos (or the selected subset), skip reconcile
27
- * --repos/--secrets/... fetch only the selected types, then reconcile
26
+ * --repos/--secrets fetch only the selected types, then reconcile
28
27
  * `--local` wins over everything; `--cloud` suppresses reconcile.
29
28
  *
30
- * Secrets and sessions are NOT part of the bare default — they are opt-in via
31
- * `--secrets` / `--sessions`. Pulling every secret bundle onto the machine on a
32
- * bare `agents sync` is more blast radius than the verb should carry by
33
- * default, and session transcripts are queryable on demand (`agents sessions
34
- * --host <machine>`) so they don't need eager mirroring.
29
+ * Secrets are NOT part of the bare default — they are opt-in via `--secrets`.
30
+ * Pulling every secret bundle onto the machine on a bare `agents sync` is more
31
+ * blast radius than the verb should carry by default.
35
32
  */
36
33
  export function planUmbrellaStages(f) {
37
34
  if (f.local) {
38
- return { fetchRepos: false, fetchSecrets: false, fetchSessions: false, reconcile: true };
35
+ return { fetchRepos: false, fetchSecrets: false, reconcile: true };
39
36
  }
40
- const anySelector = !!(f.repos || f.secrets || f.sessions);
37
+ const anySelector = !!(f.repos || f.secrets);
41
38
  if (anySelector) {
42
39
  return {
43
40
  fetchRepos: !!f.repos,
44
41
  fetchSecrets: !!f.secrets,
45
- fetchSessions: !!f.sessions,
46
42
  reconcile: !f.cloud,
47
43
  };
48
44
  }
49
45
  // No per-type selector: bare = repos + reconcile; --cloud = repos, no
50
- // reconcile. Secrets/sessions stay off unless explicitly selected above.
51
- return { fetchRepos: true, fetchSecrets: false, fetchSessions: false, reconcile: !f.cloud };
46
+ // reconcile. Secrets stay off unless explicitly selected above.
47
+ return { fetchRepos: true, fetchSecrets: false, reconcile: !f.cloud };
52
48
  }
53
49
  /**
54
- * Execute the planned stages in order: repos -> secrets -> sessions -> reconcile.
50
+ * Execute the planned stages in order: repos -> secrets -> reconcile.
55
51
  * A failure in one fetch stage is recorded and does not abort the others or the
56
52
  * reconcile — `agents sync` should make as much current as it can in one pass.
57
53
  */
@@ -110,33 +106,6 @@ export async function runUmbrellaSync(args) {
110
106
  result.secrets = { pulled, skipped: false, errors };
111
107
  }
112
108
  }
113
- if (plan.fetchSessions) {
114
- // Gate exactly like the daemon: without the `session-sync` beta opt-in (or
115
- // with a missing r2.backups bundle) this is a clean no-op, not an error that
116
- // fails the whole sync.
117
- const { isBetaEnabled } = await import('./beta.js');
118
- const { isSyncConfigured } = await import('./session/sync/config.js');
119
- if (isBetaEnabled('session-sync') && isSyncConfigured()) {
120
- const { syncSessions } = await import('./session/sync/sync.js');
121
- // Record a failure here instead of letting it throw: like the fetchSecrets
122
- // block above, a failure in one fetch stage must not abort the others or
123
- // the reconcile below (see this function's doc comment). Without this,
124
- // syncSessions() throwing — e.g. via the C1 containment reject on a
125
- // malicious peer manifest — would skip `plan.reconcile` entirely.
126
- try {
127
- const r = await syncSessions();
128
- result.sessions = { ran: true, pushed: r.pushed, pulled: r.pulled, merged: r.merged };
129
- log(`sessions: pushed ${r.pushed}, pulled ${r.pulled}, merged ${r.merged}`);
130
- }
131
- catch (err) {
132
- result.sessions = { ran: false, pushed: 0, pulled: 0, merged: 0, error: err.message };
133
- log(`sessions: failed — ${err.message}`);
134
- }
135
- }
136
- else {
137
- result.sessions = { ran: false, pushed: 0, pulled: 0, merged: 0 };
138
- }
139
- }
140
109
  if (plan.reconcile) {
141
110
  const { refresh } = await import('./refresh.js');
142
111
  await refresh({ skipPrompts: yes });
@@ -218,13 +218,30 @@ export declare class AgentProcess {
218
218
  * Uses a per-wave batched snapshot (remotePollSnapshot) when the supervisor's
219
219
  * one-ssh-per-host pre-pass populated it; otherwise falls back to its own
220
220
  * round-trips so a bare `teams status`/`teams logs` is still correct.
221
+ *
222
+ * Only polls a teammate that is plausibly still RUNNING (RUSH-2118). Once a
223
+ * remote teammate reaches a terminal status, the poll that resolved it already
224
+ * mirrored the final log bytes and read the `.exit` sentinel in this SAME
225
+ * function (delta pulled before the exit check below) — the underlying process
226
+ * is gone and can never write more, so there is nothing left to fetch. Without
227
+ * this guard every finished remote teammate still cost one ssh round-trip on
228
+ * EVERY `--active`/`listAll` poll forever, which is what made `agents sessions
229
+ * --active --local` take ~4.3s on a box with 30 completed teammates.
221
230
  */
222
231
  private syncRemoteMirror;
223
232
  /** Reset the local stdout cursor for a newly truncated resume log. */
224
233
  resetLogReadPosition(): number;
225
234
  /** Restore the cursor when a resume transaction puts the prior log back. */
226
235
  restoreLogReadPosition(position: number): void;
227
- readNewEvents(): Promise<void>;
236
+ /**
237
+ * @param opts.skipRemote A `--local` caller (RUSH-2118): never dial a
238
+ * remote-host teammate, not even a still-RUNNING one — report its
239
+ * last-persisted meta.json state as-is. A local-only query is by definition
240
+ * this-machine-only, so it must not issue an ssh round-trip at all.
241
+ */
242
+ readNewEvents(opts?: {
243
+ skipRemote?: boolean;
244
+ }): Promise<void>;
228
245
  /**
229
246
  * Truncate the local mirror to its trailing REMOTE_MIRROR_MAX_BYTES and reset
230
247
  * lastReadPos to the new (smaller) size so the parser doesn't re-read the kept
@@ -244,7 +261,14 @@ export declare class AgentProcess {
244
261
  saveMeta(): Promise<void>;
245
262
  static loadFromDisk(agentId: string, baseDir?: string | null): Promise<AgentProcess | null>;
246
263
  isProcessAlive(): boolean;
247
- updateStatusFromProcess(): Promise<void>;
264
+ /**
265
+ * @param opts.skipRemote A `--local` caller (RUSH-2118): a distributed
266
+ * teammate is never dialed — its in-memory state (already loaded from
267
+ * meta.json) stands as-is, no ssh, no re-save.
268
+ */
269
+ updateStatusFromProcess(opts?: {
270
+ skipRemote?: boolean;
271
+ }): Promise<void>;
248
272
  /**
249
273
  * Recover the teammate's exit status after its process is gone.
250
274
  *
@@ -298,8 +322,16 @@ export declare class AgentManager {
298
322
  private defaultMode;
299
323
  private initPromise;
300
324
  private cloudDispatcher;
325
+ /**
326
+ * A `--local` caller (RUSH-2118): every poll this manager issues skips the
327
+ * ssh round-trip for a distributed (remote-host) teammate, reporting its
328
+ * last-persisted meta.json state instead. Set once at construction so the
329
+ * INITIAL load in doInitialize()/loadExistingAgents() — which polls every
330
+ * teammate before listRunning()/listAll() ever run — honors it too.
331
+ */
332
+ private localOnly;
301
333
  private constructorAgentsDir;
302
- constructor(maxAgents?: number, agentsDir?: string | null, defaultMode?: Mode | null, filterByCwd?: string | null, cleanupAgeDays?: number);
334
+ constructor(maxAgents?: number, agentsDir?: string | null, defaultMode?: Mode | null, filterByCwd?: string | null, cleanupAgeDays?: number, localOnly?: boolean);
303
335
  private initialize;
304
336
  private doInitialize;
305
337
  getDefaultMode(): Mode;