@phnx-labs/agents-cli 1.20.0 → 1.20.4

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 (111) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +4 -4
  3. package/dist/commands/cli.js +3 -3
  4. package/dist/commands/cloud.js +1 -1
  5. package/dist/commands/commands.js +24 -7
  6. package/dist/commands/exec.js +36 -16
  7. package/dist/commands/feedback.d.ts +7 -0
  8. package/dist/commands/feedback.js +89 -0
  9. package/dist/commands/helper.d.ts +12 -0
  10. package/dist/commands/helper.js +87 -0
  11. package/dist/commands/hooks.js +86 -7
  12. package/dist/commands/import.js +90 -37
  13. package/dist/commands/mcp.js +166 -10
  14. package/dist/commands/packages.js +196 -27
  15. package/dist/commands/permissions.js +21 -6
  16. package/dist/commands/profiles.d.ts +8 -0
  17. package/dist/commands/profiles.js +117 -4
  18. package/dist/commands/pull.js +4 -4
  19. package/dist/commands/routines.js +6 -6
  20. package/dist/commands/rules.js +8 -4
  21. package/dist/commands/secrets-migrate.d.ts +24 -0
  22. package/dist/commands/secrets-migrate.js +198 -0
  23. package/dist/commands/secrets-sync.d.ts +11 -0
  24. package/dist/commands/secrets-sync.js +155 -0
  25. package/dist/commands/secrets.js +74 -39
  26. package/dist/commands/skills.js +22 -5
  27. package/dist/commands/subagents.js +69 -49
  28. package/dist/commands/teams.js +48 -10
  29. package/dist/commands/utils.d.ts +33 -0
  30. package/dist/commands/utils.js +139 -0
  31. package/dist/commands/versions.js +4 -4
  32. package/dist/commands/view.d.ts +6 -0
  33. package/dist/commands/view.js +169 -8
  34. package/dist/commands/workflows.js +29 -6
  35. package/dist/index.js +4 -0
  36. package/dist/lib/acp/client.js +6 -1
  37. package/dist/lib/agents.d.ts +4 -0
  38. package/dist/lib/agents.js +41 -17
  39. package/dist/lib/auto-pull-worker.js +18 -1
  40. package/dist/lib/browser/chrome.js +4 -0
  41. package/dist/lib/browser/drivers/ssh.js +1 -1
  42. package/dist/lib/browser/profiles.d.ts +3 -3
  43. package/dist/lib/browser/profiles.js +3 -3
  44. package/dist/lib/browser/service.js +19 -0
  45. package/dist/lib/browser/types.d.ts +4 -4
  46. package/dist/lib/cli-resources.d.ts +36 -8
  47. package/dist/lib/cli-resources.js +268 -46
  48. package/dist/lib/cloud/factory.d.ts +1 -1
  49. package/dist/lib/cloud/factory.js +1 -1
  50. package/dist/lib/events.d.ts +16 -2
  51. package/dist/lib/events.js +33 -2
  52. package/dist/lib/exec.d.ts +39 -11
  53. package/dist/lib/exec.js +90 -31
  54. package/dist/lib/help.js +11 -5
  55. package/dist/lib/hooks/cache.d.ts +38 -0
  56. package/dist/lib/hooks/cache.js +242 -0
  57. package/dist/lib/hooks/profile.d.ts +33 -0
  58. package/dist/lib/hooks/profile.js +129 -0
  59. package/dist/lib/hooks.d.ts +0 -10
  60. package/dist/lib/hooks.js +68 -15
  61. package/dist/lib/import.d.ts +21 -0
  62. package/dist/lib/import.js +55 -2
  63. package/dist/lib/mcp.d.ts +15 -0
  64. package/dist/lib/mcp.js +40 -0
  65. package/dist/lib/permissions.d.ts +13 -0
  66. package/dist/lib/permissions.js +51 -1
  67. package/dist/lib/plugin-marketplace.d.ts +10 -0
  68. package/dist/lib/plugin-marketplace.js +47 -1
  69. package/dist/lib/plugins.js +15 -1
  70. package/dist/lib/profiles-presets.d.ts +26 -0
  71. package/dist/lib/profiles-presets.js +187 -8
  72. package/dist/lib/profiles.d.ts +34 -0
  73. package/dist/lib/profiles.js +112 -1
  74. package/dist/lib/pty-server.js +27 -3
  75. package/dist/lib/routines-format.d.ts +17 -5
  76. package/dist/lib/routines-format.js +37 -16
  77. package/dist/lib/routines.d.ts +1 -1
  78. package/dist/lib/routines.js +2 -2
  79. package/dist/lib/runner.js +64 -10
  80. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  81. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  83. package/dist/lib/secrets/bundles.d.ts +18 -22
  84. package/dist/lib/secrets/bundles.js +75 -99
  85. package/dist/lib/secrets/index.d.ts +51 -27
  86. package/dist/lib/secrets/index.js +147 -156
  87. package/dist/lib/secrets/install-helper.d.ts +45 -0
  88. package/dist/lib/secrets/install-helper.js +165 -0
  89. package/dist/lib/secrets/linux.js +4 -4
  90. package/dist/lib/secrets/sync.d.ts +56 -0
  91. package/dist/lib/secrets/sync.js +180 -0
  92. package/dist/lib/session/render.js +4 -4
  93. package/dist/lib/session/types.d.ts +1 -1
  94. package/dist/lib/shims.d.ts +4 -1
  95. package/dist/lib/shims.js +5 -35
  96. package/dist/lib/state.d.ts +14 -1
  97. package/dist/lib/state.js +49 -5
  98. package/dist/lib/teams/agents.d.ts +5 -4
  99. package/dist/lib/teams/agents.js +47 -21
  100. package/dist/lib/teams/api.d.ts +2 -1
  101. package/dist/lib/teams/api.js +4 -3
  102. package/dist/lib/types.d.ts +57 -1
  103. package/dist/lib/types.js +2 -0
  104. package/dist/lib/usage.d.ts +27 -2
  105. package/dist/lib/usage.js +100 -17
  106. package/dist/lib/versions.d.ts +35 -1
  107. package/dist/lib/versions.js +288 -64
  108. package/package.json +13 -12
  109. package/scripts/install-helper.js +97 -0
  110. package/scripts/postinstall.js +16 -0
  111. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Remote sync client for secrets bundles.
3
+ *
4
+ * Replaces the previous "leave it to iCloud Keychain" model with explicit
5
+ * push/pull against api.prix.dev. Bundle contents (vars + secret values) are
6
+ * encrypted client-side with AES-256-GCM under a key derived from a
7
+ * user-supplied passphrase via PBKDF2-SHA256. Plaintext never leaves the
8
+ * machine — api.prix.dev only ever sees the ciphertext + KDF parameters.
9
+ */
10
+ import { type SecretsBundle } from './bundles.js';
11
+ export declare const MIN_PASSPHRASE_LEN = 12;
12
+ /** Envelope for an encrypted bundle. All byte fields are base64. */
13
+ export interface EncryptedEnvelope {
14
+ v: 1;
15
+ kdf: 'pbkdf2-sha256';
16
+ iter: number;
17
+ salt: string;
18
+ iv: string;
19
+ ct: string;
20
+ tag: string;
21
+ }
22
+ interface RemoteBundleSummary {
23
+ name: string;
24
+ updated_at: string;
25
+ }
26
+ /** Encrypt a JSON-serializable payload with a passphrase. */
27
+ export declare function encryptBlob(plaintext: string, passphrase: string): EncryptedEnvelope;
28
+ /** Decrypt an envelope. Throws on bad passphrase (auth tag mismatch). */
29
+ export declare function decryptBlob(envelope: EncryptedEnvelope, passphrase: string): string;
30
+ /** The plaintext we serialize before encrypting: bundle metadata + secret values. */
31
+ export interface BundleSnapshot {
32
+ bundle: SecretsBundle;
33
+ /** keychain shortId -> plaintext value. Only present for keychain: refs. */
34
+ secrets: Record<string, string>;
35
+ }
36
+ /** Options for pushBundle. */
37
+ export interface PushOptions {
38
+ passphrase: string;
39
+ }
40
+ /** Push a local bundle to api.prix.dev. Encrypts client-side; server only sees ciphertext. */
41
+ export declare function pushBundle(name: string, opts: PushOptions): Promise<{
42
+ updated_at: string;
43
+ }>;
44
+ /** Options for pullBundle. */
45
+ export interface PullOptions {
46
+ passphrase: string;
47
+ /** When true, overwrite an existing local bundle. */
48
+ force?: boolean;
49
+ }
50
+ /** Pull a bundle by name from api.prix.dev and materialize it locally. */
51
+ export declare function pullBundle(name: string, opts: PullOptions): Promise<SecretsBundle>;
52
+ /** Delete a bundle on the remote. */
53
+ export declare function deleteRemoteBundle(name: string): Promise<boolean>;
54
+ /** List bundles currently stored on api.prix.dev for this user. */
55
+ export declare function listRemoteBundles(): Promise<RemoteBundleSummary[]>;
56
+ export {};
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Remote sync client for secrets bundles.
3
+ *
4
+ * Replaces the previous "leave it to iCloud Keychain" model with explicit
5
+ * push/pull against api.prix.dev. Bundle contents (vars + secret values) are
6
+ * encrypted client-side with AES-256-GCM under a key derived from a
7
+ * user-supplied passphrase via PBKDF2-SHA256. Plaintext never leaves the
8
+ * machine — api.prix.dev only ever sees the ciphertext + KDF parameters.
9
+ */
10
+ import * as crypto from 'crypto';
11
+ import * as fs from 'fs';
12
+ import * as os from 'os';
13
+ import * as path from 'path';
14
+ import * as yaml from 'yaml';
15
+ import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from './index.js';
16
+ import { readBundle, writeBundle, keychainItemsForBundle, validateBundleName, } from './bundles.js';
17
+ const PROXY_BASE = 'https://api.prix.dev';
18
+ const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
19
+ const BUNDLE_ENDPOINT = '/api/v1/secrets/bundles';
20
+ // PBKDF2 cost. 600k SHA-256 iters matches OWASP 2023+ guidance and keeps a
21
+ // passphrase prompt under a second on the hardware the CLI targets.
22
+ const PBKDF2_ITER = 600_000;
23
+ export const MIN_PASSPHRASE_LEN = 12;
24
+ const KEY_LEN = 32;
25
+ const SALT_LEN = 16;
26
+ const IV_LEN = 12;
27
+ function readRushToken() {
28
+ if (!fs.existsSync(USER_YAML)) {
29
+ throw new Error('Not logged in to Rush. Run `rush login` first.');
30
+ }
31
+ const raw = fs.readFileSync(USER_YAML, 'utf-8');
32
+ const data = yaml.parse(raw);
33
+ const token = data?.session?.access_token;
34
+ if (!token) {
35
+ throw new Error('No session token in ~/.rush/user.yaml. Run `rush login` first.');
36
+ }
37
+ return token;
38
+ }
39
+ async function api(method, endpoint, body) {
40
+ const token = readRushToken();
41
+ const url = endpoint.startsWith('http') ? endpoint : `${PROXY_BASE}${endpoint}`;
42
+ return fetch(url, {
43
+ method,
44
+ headers: {
45
+ Authorization: `Bearer ${token}`,
46
+ 'Content-Type': 'application/json',
47
+ },
48
+ body: body === undefined ? undefined : JSON.stringify(body),
49
+ });
50
+ }
51
+ function deriveKey(passphrase, salt) {
52
+ return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITER, KEY_LEN, 'sha256');
53
+ }
54
+ /** Encrypt a JSON-serializable payload with a passphrase. */
55
+ export function encryptBlob(plaintext, passphrase) {
56
+ if (!passphrase || passphrase.length < MIN_PASSPHRASE_LEN) {
57
+ throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
58
+ }
59
+ const salt = crypto.randomBytes(SALT_LEN);
60
+ const iv = crypto.randomBytes(IV_LEN);
61
+ const key = deriveKey(passphrase, salt);
62
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
63
+ const ct = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
64
+ const tag = cipher.getAuthTag();
65
+ return {
66
+ v: 1,
67
+ kdf: 'pbkdf2-sha256',
68
+ iter: PBKDF2_ITER,
69
+ salt: salt.toString('base64'),
70
+ iv: iv.toString('base64'),
71
+ ct: ct.toString('base64'),
72
+ tag: tag.toString('base64'),
73
+ };
74
+ }
75
+ /** Decrypt an envelope. Throws on bad passphrase (auth tag mismatch). */
76
+ export function decryptBlob(envelope, passphrase) {
77
+ if (envelope.v !== 1 || envelope.kdf !== 'pbkdf2-sha256') {
78
+ throw new Error(`Unsupported envelope version (v${envelope.v}, kdf=${envelope.kdf}).`);
79
+ }
80
+ const salt = Buffer.from(envelope.salt, 'base64');
81
+ const iv = Buffer.from(envelope.iv, 'base64');
82
+ const ct = Buffer.from(envelope.ct, 'base64');
83
+ const tag = Buffer.from(envelope.tag, 'base64');
84
+ const key = deriveKey(passphrase, salt);
85
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
86
+ decipher.setAuthTag(tag);
87
+ try {
88
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf-8');
89
+ }
90
+ catch {
91
+ throw new Error('Decryption failed — wrong passphrase or corrupt blob.');
92
+ }
93
+ }
94
+ function snapshotBundle(name) {
95
+ const bundle = readBundle(name);
96
+ const secrets = {};
97
+ for (const { key, item } of keychainItemsForBundle(bundle)) {
98
+ if (!hasKeychainToken(item)) {
99
+ throw new Error(`Bundle '${name}' key '${key}': keychain item '${item}' missing — cannot push incomplete bundle.`);
100
+ }
101
+ const raw = bundle.vars[key];
102
+ if (typeof raw !== 'string' || !raw.startsWith('keychain:'))
103
+ continue;
104
+ const shortId = raw.slice('keychain:'.length);
105
+ secrets[shortId] = getKeychainToken(item);
106
+ }
107
+ return { bundle, secrets };
108
+ }
109
+ function restoreSnapshot(snap) {
110
+ const bundle = snap.bundle;
111
+ validateBundleName(bundle.name);
112
+ for (const [shortId, value] of Object.entries(snap.secrets)) {
113
+ const item = secretsKeychainItem(bundle.name, shortId);
114
+ setKeychainToken(item, value);
115
+ }
116
+ writeBundle(bundle);
117
+ }
118
+ /** Push a local bundle to api.prix.dev. Encrypts client-side; server only sees ciphertext. */
119
+ export async function pushBundle(name, opts) {
120
+ validateBundleName(name);
121
+ const snap = snapshotBundle(name);
122
+ const envelope = encryptBlob(JSON.stringify(snap), opts.passphrase);
123
+ const updated_at = new Date().toISOString();
124
+ const payload = { envelope, updated_at };
125
+ const res = await api('PUT', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`, payload);
126
+ if (!res.ok) {
127
+ const body = await res.text().catch(() => '');
128
+ throw new Error(`Push failed (${res.status} ${res.statusText}): ${body}`);
129
+ }
130
+ return { updated_at };
131
+ }
132
+ /** Pull a bundle by name from api.prix.dev and materialize it locally. */
133
+ export async function pullBundle(name, opts) {
134
+ validateBundleName(name);
135
+ const res = await api('GET', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`);
136
+ if (res.status === 404) {
137
+ throw new Error(`Remote bundle '${name}' not found on api.prix.dev.`);
138
+ }
139
+ if (!res.ok) {
140
+ const body = await res.text().catch(() => '');
141
+ throw new Error(`Pull failed (${res.status} ${res.statusText}): ${body}`);
142
+ }
143
+ const data = await res.json();
144
+ const plaintext = decryptBlob(data.envelope, opts.passphrase);
145
+ const snap = JSON.parse(plaintext);
146
+ if (!snap || !snap.bundle || snap.bundle.name !== name) {
147
+ throw new Error(`Decrypted payload for '${name}' is malformed (bundle name mismatch).`);
148
+ }
149
+ // existence check is the caller's responsibility; we trust opts.force.
150
+ if (!opts.force) {
151
+ const { bundleExists } = await import('./bundles.js');
152
+ if (bundleExists(name)) {
153
+ throw new Error(`Local bundle '${name}' already exists. Re-run with --force to overwrite.`);
154
+ }
155
+ }
156
+ restoreSnapshot(snap);
157
+ return snap.bundle;
158
+ }
159
+ /** Delete a bundle on the remote. */
160
+ export async function deleteRemoteBundle(name) {
161
+ validateBundleName(name);
162
+ const res = await api('DELETE', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`);
163
+ if (res.status === 404)
164
+ return false;
165
+ if (!res.ok) {
166
+ const body = await res.text().catch(() => '');
167
+ throw new Error(`Delete failed (${res.status} ${res.statusText}): ${body}`);
168
+ }
169
+ return true;
170
+ }
171
+ /** List bundles currently stored on api.prix.dev for this user. */
172
+ export async function listRemoteBundles() {
173
+ const res = await api('GET', BUNDLE_ENDPOINT);
174
+ if (!res.ok) {
175
+ const body = await res.text().catch(() => '');
176
+ throw new Error(`List failed (${res.status} ${res.statusText}): ${body}`);
177
+ }
178
+ const data = await res.json();
179
+ return data.bundles ?? [];
180
+ }
@@ -806,15 +806,15 @@ export function renderConversationMarkdown(events, opts = {}) {
806
806
  for (const event of events) {
807
807
  if (event.type === 'message') {
808
808
  if (event.role === 'user') {
809
- parts.push(`## User\n\n${event.content ?? ''}`);
809
+ parts.push(`## User\n\n${sanitize(event.content ?? '')}`);
810
810
  }
811
811
  else if (event.role === 'assistant') {
812
- parts.push(`## Assistant\n\n${event.content ?? ''}`);
812
+ parts.push(`## Assistant\n\n${sanitize(event.content ?? '')}`);
813
813
  }
814
814
  }
815
815
  else if (event.type === 'thinking') {
816
816
  if (event.content)
817
- parts.push(`### Thinking\n\n${event.content}`);
817
+ parts.push(`### Thinking\n\n${sanitize(event.content)}`);
818
818
  }
819
819
  else if (event.type === 'tool_use') {
820
820
  const tool = event.tool || 'unknown';
@@ -837,7 +837,7 @@ export function renderConversationMarkdown(events, opts = {}) {
837
837
  }
838
838
  }
839
839
  else if (event.type === 'error') {
840
- parts.push(`### Error\n\n${event.content || event.tool || 'Unknown error'}`);
840
+ parts.push(`### Error\n\n${event.content ? sanitize(event.content) : (event.tool || 'Unknown error')}`);
841
841
  }
842
842
  }
843
843
  return parts.join('\n\n');
@@ -37,7 +37,7 @@ export interface SessionEvent {
37
37
  export interface TeamOrigin {
38
38
  /** Teammate name if set, otherwise first 8 chars of the agent UUID. */
39
39
  handle?: string;
40
- /** Agent mode: 'plan', 'edit', or 'full'. */
40
+ /** Agent mode: 'plan', 'edit', 'auto', or 'skip' ('full' accepted as legacy alias for 'skip'). */
41
41
  mode?: string;
42
42
  }
43
43
  /** Lightweight metadata for a discovered session, used in listings and pickers. */
@@ -59,8 +59,11 @@ export interface ConflictInfo {
59
59
  * instead of hardcoding `.${agent}`. Backwards-compatible for every
60
60
  * existing agent (their configDir is `~/.{agent}`); enables nested
61
61
  * layouts like Antigravity's `~/.gemini/antigravity-cli/`.
62
+ * v15 — remove foreground resource sync / rules refresh from launch shims.
63
+ * Version homes are reconciled by agents-cli management commands; the
64
+ * shim hot path only resolves a version and execs the agent binary.
62
65
  */
63
- export declare const SHIM_SCHEMA_VERSION = 14;
66
+ export declare const SHIM_SCHEMA_VERSION = 15;
64
67
  /**
65
68
  * Generate the full bash shim script for the given agent. The returned string
66
69
  * is written to ~/.agents/shims/{cliCommand} and made executable.
package/dist/lib/shims.js CHANGED
@@ -183,8 +183,11 @@ async function promptConflictStrategy(conflictInfos) {
183
183
  * instead of hardcoding `.${agent}`. Backwards-compatible for every
184
184
  * existing agent (their configDir is `~/.{agent}`); enables nested
185
185
  * layouts like Antigravity's `~/.gemini/antigravity-cli/`.
186
+ * v15 — remove foreground resource sync / rules refresh from launch shims.
187
+ * Version homes are reconciled by agents-cli management commands; the
188
+ * shim hot path only resolves a version and execs the agent binary.
186
189
  */
187
- export const SHIM_SCHEMA_VERSION = 14;
190
+ export const SHIM_SCHEMA_VERSION = 15;
188
191
  /** Internal marker string used to embed the schema version in shim scripts. */
189
192
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
190
193
  function shellQuote(value) {
@@ -242,18 +245,6 @@ export COPILOT_HOME="$VERSION_DIR/home/${configDirName}"
242
245
  export GROK_HOME="$VERSION_DIR/home/.grok"
243
246
  `
244
247
  : '';
245
- // Agents that don't natively resolve @-imports in their rules file need
246
- // agents-cli to recompile when the user edits a rule/preset file. The
247
- // check is fast (sha256 of ~8 small files) and skips the recompile when
248
- // sources haven't changed.
249
- const refreshRulesCall = !agentConfig.capabilities.rulesImports
250
- ? `
251
- # Recompile rules if any rule/preset source has changed since last sync.
252
- # Fast-path check (~10-20ms) when nothing changed; full recompile only on
253
- # actual diff. Non-blocking failure — if the refresh errors, we still launch.
254
- "$AGENTS_BIN" refresh-rules --agent "$AGENT" --agent-version "$VERSION" --quiet 2>/dev/null || true
255
- `
256
- : '';
257
248
  const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
258
249
  return `#!/bin/bash
259
250
  # Auto-generated by agents-cli - do not edit
@@ -307,22 +298,6 @@ resolve_default_version() {
307
298
  fi
308
299
  }
309
300
 
310
- # Find project-scoped .agents directory (stop at agents.yaml or .git)
311
- find_project_agents_dir() {
312
- local dir="$PWD"
313
- while [ "$dir" != "/" ]; do
314
- if [ -d "$dir/.agents" ]; then
315
- echo "$dir/.agents"
316
- return 0
317
- fi
318
- if [ -f "$dir/agents.yaml" ] || [ -d "$dir/.git" ] || [ -f "$dir/.git" ]; then
319
- break
320
- fi
321
- dir=$(dirname "$dir")
322
- done
323
- return 1
324
- }
325
-
326
301
  # Find the latest installed version by numeric component comparison.
327
302
  # Handles both semver (2.1.138) and date-based (2026.5.7) version strings.
328
303
  find_latest_installed() {
@@ -474,12 +449,7 @@ if [ ! -x "$BINARY" ]; then
474
449
  fi
475
450
  fi
476
451
 
477
- # Sync project-scoped resources into version home if a project .agents/ is present
478
- PROJECT_AGENTS_DIR=$(find_project_agents_dir)
479
- if [ -n "$PROJECT_AGENTS_DIR" ]; then
480
- "$AGENTS_BIN" sync --agent "$AGENT" --agent-version "$VERSION" --project-dir "$PROJECT_AGENTS_DIR" --quiet >/dev/null 2>&1
481
- fi
482
- ${refreshRulesCall}${managedEnv}
452
+ ${managedEnv}
483
453
 
484
454
  exec "$BINARY"${launchArgs} "$@"
485
455
  `;
@@ -107,6 +107,10 @@ export declare function getRunsDir(): string;
107
107
  export declare function getVersionsDir(): string;
108
108
  /** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
109
109
  export declare function getShimsDir(): string;
110
+ /** Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/). */
111
+ export declare function getHookShimsDir(): string;
112
+ /** Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). */
113
+ export declare function getHookCacheDir(): string;
110
114
  /** Path to per-agent installed CLI binaries (~/.agents/.cache/bin/). */
111
115
  export declare function getBinDir(): string;
112
116
  /** Path to config backups (~/.agents/.history/backups/). */
@@ -191,7 +195,16 @@ export declare function getEnabledExtraRepos(): Array<{
191
195
  export declare function ensureAgentsDir(): void;
192
196
  /** Return an empty Meta object used when no agents.yaml exists yet. */
193
197
  export declare function createDefaultMeta(): Meta;
194
- /** Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed. */
198
+ /**
199
+ * Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed.
200
+ *
201
+ * Cache invariants:
202
+ * - Cache key is the mtime of the user agents.yaml.
203
+ * - `writeMetaUnlocked` clears the cache; in-process callers always see fresh state.
204
+ * - If the file is mutated by ANOTHER process while we hold a stale cache, the
205
+ * mtime check below catches it on the next read (assuming the mtime advanced).
206
+ * - The cache stores the merged system+user meta; both files' mtimes contribute.
207
+ */
195
208
  export declare function readMeta(): Meta;
196
209
  /** Serialize and write agents.yaml to the user repo, invalidating the in-memory cache. */
197
210
  export declare function writeMeta(meta: Meta): void;
package/dist/lib/state.js CHANGED
@@ -64,6 +64,8 @@ const BACKUPS_DIR = path.join(HISTORY_DIR, 'backups');
64
64
  const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
65
65
  // Cache bucket (regenerable).
66
66
  const SHIMS_DIR = path.join(CACHE_DIR, 'shims');
67
+ const HOOK_SHIMS_DIR = path.join(SHIMS_DIR, 'hooks');
68
+ const HOOK_CACHE_DIR = path.join(CACHE_DIR, 'state', 'hooks');
67
69
  const BIN_DIR = path.join(CACHE_DIR, 'bin');
68
70
  const PACKAGES_DIR = path.join(CACHE_DIR, 'packages');
69
71
  // Plugins are user-authored resources, alongside skills/, commands/, hooks/.
@@ -265,6 +267,10 @@ export function getRunsDir() { return RUNS_DIR; }
265
267
  export function getVersionsDir() { return VERSIONS_DIR; }
266
268
  /** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
267
269
  export function getShimsDir() { return SHIMS_DIR; }
270
+ /** Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/). */
271
+ export function getHookShimsDir() { return HOOK_SHIMS_DIR; }
272
+ /** Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). */
273
+ export function getHookCacheDir() { return HOOK_CACHE_DIR; }
268
274
  /** Path to per-agent installed CLI binaries (~/.agents/.cache/bin/). */
269
275
  export function getBinDir() { return BIN_DIR; }
270
276
  /** Path to config backups (~/.agents/.history/backups/). */
@@ -413,6 +419,24 @@ export function createDefaultMeta() {
413
419
  }
414
420
  let metaCache = null;
415
421
  let metaLockDepth = 0;
422
+ /** Return mtimeMs for a file path, or 0 if the file is absent or unreadable. */
423
+ function safeMtimeMs(filePath) {
424
+ try {
425
+ return fs.statSync(filePath).mtimeMs;
426
+ }
427
+ catch {
428
+ return 0;
429
+ }
430
+ }
431
+ /** Compute the combined cache stamp for the user + system agents.yaml files. */
432
+ function currentMetaStamp() {
433
+ return safeMtimeMs(META_FILE) + safeMtimeMs(SYSTEM_META_FILE) * 1e-3;
434
+ }
435
+ /** Memoize a parsed Meta against the current file mtimes. */
436
+ function rememberMeta(meta) {
437
+ metaCache = { mtime: currentMetaStamp(), meta };
438
+ return meta;
439
+ }
416
440
  function withMetaLock(fn) {
417
441
  ensureAgentsDir();
418
442
  if (metaLockDepth > 0) {
@@ -483,9 +507,29 @@ function migrateSystemMetaToUser() {
483
507
  // Best-effort; proceed with fresh state if it fails.
484
508
  }
485
509
  }
486
- /** Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed. */
510
+ /**
511
+ * Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed.
512
+ *
513
+ * Cache invariants:
514
+ * - Cache key is the mtime of the user agents.yaml.
515
+ * - `writeMetaUnlocked` clears the cache; in-process callers always see fresh state.
516
+ * - If the file is mutated by ANOTHER process while we hold a stale cache, the
517
+ * mtime check below catches it on the next read (assuming the mtime advanced).
518
+ * - The cache stores the merged system+user meta; both files' mtimes contribute.
519
+ */
487
520
  export function readMeta() {
488
521
  ensureAgentsDir();
522
+ // Fast path: serve from cache when both source files are byte-identical to
523
+ // what we last parsed. Reduces N readMeta calls per CLI invocation to ~2 stat
524
+ // syscalls plus an in-memory object spread.
525
+ if (metaCache) {
526
+ const userMtime = safeMtimeMs(META_FILE);
527
+ const systemMtime = safeMtimeMs(SYSTEM_META_FILE);
528
+ const stamp = userMtime + systemMtime * 1e-3;
529
+ if (stamp === metaCache.mtime) {
530
+ return metaCache.meta;
531
+ }
532
+ }
489
533
  // NOTE: agents.yaml migration from ~/.agents-system/ to ~/.agents/ is handled
490
534
  // exclusively by runMigration() in migrate.ts, called from postinstall and
491
535
  // from a one-shot bootstrap step in src/index.ts. Calling it here would
@@ -515,7 +559,7 @@ export function readMeta() {
515
559
  fs.unlinkSync(oldMetaFile);
516
560
  }
517
561
  catch { /* non-critical */ }
518
- return meta;
562
+ return rememberMeta(meta);
519
563
  }
520
564
  catch {
521
565
  /* meta.yaml migration failed */
@@ -557,15 +601,15 @@ export function readMeta() {
557
601
  }
558
602
  if (applyRegistrySeeds(meta)) {
559
603
  writeMeta(meta);
560
- return meta;
604
+ return rememberMeta(meta);
561
605
  }
562
- return meta;
606
+ return rememberMeta(meta);
563
607
  }
564
608
  const meta = createDefaultMeta();
565
609
  if (applyRegistrySeeds(meta)) {
566
610
  writeMeta(meta);
567
611
  }
568
- return meta;
612
+ return rememberMeta(meta);
569
613
  }
570
614
  /** Serialize and write agents.yaml to the user repo, invalidating the in-memory cache. */
571
615
  export function writeMeta(meta) {
@@ -36,8 +36,8 @@ export declare function captureProcessStartTime(pid: number): string | null;
36
36
  * model_reasoning_effort override). Mode (plan/edit/full) is a separate knob.
37
37
  */
38
38
  export type EffortLevel = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
39
- declare const VALID_MODES: readonly ["plan", "edit", "full", "auto"];
40
- type Mode = typeof VALID_MODES[number];
39
+ export declare const VALID_MODES: readonly ["plan", "edit", "auto", "skip", "full"];
40
+ type Mode = 'plan' | 'edit' | 'auto' | 'skip';
41
41
  /** Resolve a mode string to a validated Mode, falling back to the given default. */
42
42
  export declare function resolveMode(requestedMode: string | null | undefined, defaultMode?: Mode): Mode;
43
43
  /** Ensure Gemini's settings.json has experimental.plan enabled for headless plan mode. */
@@ -82,6 +82,7 @@ export declare class AgentProcess {
82
82
  after: string[];
83
83
  effort: EffortLevel | null;
84
84
  model: string | null;
85
+ profileName: string | null;
85
86
  envOverrides: Record<string, string> | null;
86
87
  taskType: TaskType | null;
87
88
  cloudRepo: string | null;
@@ -91,7 +92,7 @@ export declare class AgentProcess {
91
92
  private eventsCache;
92
93
  private lastReadPos;
93
94
  private baseDir;
94
- constructor(agentId: string, taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode, pid?: number | null, status?: AgentStatus, startedAt?: Date, completedAt?: Date | null, baseDir?: string | null, parentSessionId?: string | null, workspaceDir?: string | null, cloudSessionId?: string | null, cloudProvider?: string | null, prUrl?: string | null, version?: string | null, remoteSessionId?: string | null, name?: string | null, after?: string[], effort?: EffortLevel | null, model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null);
95
+ constructor(agentId: string, taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode, pid?: number | null, status?: AgentStatus, startedAt?: Date, completedAt?: Date | null, baseDir?: string | null, parentSessionId?: string | null, workspaceDir?: string | null, cloudSessionId?: string | null, cloudProvider?: string | null, prUrl?: string | null, version?: string | null, remoteSessionId?: string | null, name?: string | null, after?: string[], effort?: EffortLevel | null, model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null, profileName?: string | null);
95
96
  get isEditMode(): boolean;
96
97
  getAgentDir(): Promise<string>;
97
98
  /**
@@ -179,7 +180,7 @@ export declare class AgentManager {
179
180
  */
180
181
  rescanFromDisk(): Promise<number>;
181
182
  private loadExistingAgents;
182
- spawn(taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode | null, effort?: EffortLevel, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null): Promise<AgentProcess>;
183
+ spawn(taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode | null, effort?: EffortLevel, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null, profileName?: string | null): Promise<AgentProcess>;
183
184
  /**
184
185
  * Actually spawn the OS process for a teammate. Extracted from spawn() so
185
186
  * staged teammates can be launched later by startReady().