@phnx-labs/agents-cli 1.15.0 → 1.17.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 (111) hide show
  1. package/CHANGELOG.md +143 -39
  2. package/README.md +6 -6
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/browser-picker.d.ts +21 -0
  5. package/dist/commands/browser-picker.js +114 -0
  6. package/dist/commands/browser.js +793 -83
  7. package/dist/commands/cloud.js +8 -0
  8. package/dist/commands/commands.js +72 -22
  9. package/dist/commands/daemon.js +2 -2
  10. package/dist/commands/exec.js +70 -1
  11. package/dist/commands/hooks.js +71 -26
  12. package/dist/commands/mcp.js +81 -39
  13. package/dist/commands/plugins.js +224 -17
  14. package/dist/commands/prune.js +29 -1
  15. package/dist/commands/pull.js +3 -3
  16. package/dist/commands/repo.js +1 -1
  17. package/dist/commands/routines.js +2 -2
  18. package/dist/commands/secrets.js +154 -20
  19. package/dist/commands/sessions.js +62 -19
  20. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  21. package/dist/commands/{init.js → setup.js} +22 -21
  22. package/dist/commands/skills.js +60 -19
  23. package/dist/commands/subagents.js +41 -13
  24. package/dist/commands/utils.d.ts +16 -0
  25. package/dist/commands/utils.js +32 -0
  26. package/dist/commands/view.js +78 -20
  27. package/dist/commands/workflows.d.ts +10 -0
  28. package/dist/commands/workflows.js +457 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +48 -36
  31. package/dist/lib/agents.js +2 -2
  32. package/dist/lib/auto-pull-worker.js +2 -3
  33. package/dist/lib/auto-pull.js +2 -2
  34. package/dist/lib/browser/cdp.d.ts +7 -1
  35. package/dist/lib/browser/cdp.js +32 -1
  36. package/dist/lib/browser/chrome.d.ts +10 -0
  37. package/dist/lib/browser/chrome.js +41 -3
  38. package/dist/lib/browser/devices.d.ts +4 -0
  39. package/dist/lib/browser/devices.js +27 -0
  40. package/dist/lib/browser/drivers/local.js +22 -6
  41. package/dist/lib/browser/drivers/ssh.js +9 -2
  42. package/dist/lib/browser/input.d.ts +1 -0
  43. package/dist/lib/browser/input.js +3 -0
  44. package/dist/lib/browser/ipc.js +158 -23
  45. package/dist/lib/browser/profiles.d.ts +10 -2
  46. package/dist/lib/browser/profiles.js +122 -37
  47. package/dist/lib/browser/service.d.ts +91 -13
  48. package/dist/lib/browser/service.js +767 -132
  49. package/dist/lib/browser/types.d.ts +91 -3
  50. package/dist/lib/browser/types.js +16 -0
  51. package/dist/lib/cloud/rush.d.ts +28 -1
  52. package/dist/lib/cloud/rush.js +69 -14
  53. package/dist/lib/cloud/store.js +2 -2
  54. package/dist/lib/commands.d.ts +1 -15
  55. package/dist/lib/commands.js +11 -7
  56. package/dist/lib/daemon.js +2 -3
  57. package/dist/lib/doctor-diff.js +4 -4
  58. package/dist/lib/events.js +2 -2
  59. package/dist/lib/hooks.d.ts +11 -7
  60. package/dist/lib/hooks.js +138 -49
  61. package/dist/lib/migrate.d.ts +1 -1
  62. package/dist/lib/migrate.js +1237 -22
  63. package/dist/lib/models.js +2 -2
  64. package/dist/lib/permissions.d.ts +8 -66
  65. package/dist/lib/permissions.js +18 -18
  66. package/dist/lib/plugins.d.ts +94 -24
  67. package/dist/lib/plugins.js +702 -123
  68. package/dist/lib/pty-server.js +9 -10
  69. package/dist/lib/resource-patterns.d.ts +41 -0
  70. package/dist/lib/resource-patterns.js +82 -0
  71. package/dist/lib/resources/hooks.d.ts +5 -1
  72. package/dist/lib/resources/hooks.js +21 -4
  73. package/dist/lib/resources/index.d.ts +17 -0
  74. package/dist/lib/resources/index.js +7 -0
  75. package/dist/lib/resources/types.d.ts +1 -1
  76. package/dist/lib/resources/workflows.d.ts +24 -0
  77. package/dist/lib/resources/workflows.js +110 -0
  78. package/dist/lib/resources.d.ts +6 -1
  79. package/dist/lib/resources.js +12 -2
  80. package/dist/lib/rotate.js +3 -4
  81. package/dist/lib/session/active.d.ts +3 -0
  82. package/dist/lib/session/active.js +92 -6
  83. package/dist/lib/session/cloud.js +2 -2
  84. package/dist/lib/session/db.d.ts +18 -0
  85. package/dist/lib/session/db.js +109 -5
  86. package/dist/lib/session/discover.d.ts +6 -0
  87. package/dist/lib/session/discover.js +55 -29
  88. package/dist/lib/session/team-filter.js +2 -2
  89. package/dist/lib/shims.d.ts +4 -52
  90. package/dist/lib/shims.js +23 -15
  91. package/dist/lib/skills.js +6 -2
  92. package/dist/lib/sqlite.js +10 -4
  93. package/dist/lib/state.d.ts +101 -16
  94. package/dist/lib/state.js +179 -31
  95. package/dist/lib/subagents.d.ts +28 -0
  96. package/dist/lib/subagents.js +98 -1
  97. package/dist/lib/sync-manifest.d.ts +1 -1
  98. package/dist/lib/sync-manifest.js +3 -3
  99. package/dist/lib/teams/persistence.js +15 -5
  100. package/dist/lib/teams/registry.js +2 -2
  101. package/dist/lib/types.d.ts +75 -17
  102. package/dist/lib/types.js +3 -3
  103. package/dist/lib/usage.js +2 -2
  104. package/dist/lib/versions.d.ts +3 -0
  105. package/dist/lib/versions.js +158 -47
  106. package/dist/lib/workflows.d.ts +79 -0
  107. package/dist/lib/workflows.js +233 -0
  108. package/package.json +1 -5
  109. package/scripts/postinstall.js +60 -59
  110. package/dist/commands/fork.d.ts +0 -10
  111. package/dist/commands/fork.js +0 -146
@@ -11,17 +11,26 @@ export interface BrowserProfile {
11
11
  viewport?: {
12
12
  width: number;
13
13
  height: number;
14
+ x?: number;
15
+ y?: number;
14
16
  };
15
17
  }
16
18
  export interface ChromeOptions {
17
19
  headless?: boolean;
18
20
  args?: string[];
21
+ viewport?: {
22
+ width: number;
23
+ height: number;
24
+ x?: number;
25
+ y?: number;
26
+ };
19
27
  }
20
28
  export interface Task {
21
29
  id: string;
30
+ name: string;
22
31
  profile: string;
23
- windowTargetId?: string;
24
- tabIds: string[];
32
+ tabs: Record<string, string>;
33
+ currentTabId?: string;
25
34
  createdAt: number;
26
35
  pid: number;
27
36
  }
@@ -36,17 +45,39 @@ export interface ProfileStatus {
36
45
  running: boolean;
37
46
  port?: number;
38
47
  pid?: number;
48
+ /** The port declared in the profile's first endpoint, when it differs from the running port. */
49
+ configuredPort?: number;
39
50
  tasks: TaskStatus[];
40
51
  }
41
52
  export interface TaskStatus {
42
53
  id: string;
54
+ name: string;
43
55
  tabCount: number;
56
+ currentTabId?: string;
44
57
  createdAt: number;
58
+ endedAt?: number;
59
+ domains?: string[];
60
+ tabs?: Array<{
61
+ id: string;
62
+ url: string;
63
+ title?: string;
64
+ current?: boolean;
65
+ }>;
45
66
  }
46
- export type IPCAction = 'start' | 'stop' | 'status' | 'navigate' | 'tabs' | 'close' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover';
67
+ export interface HistoricalTask {
68
+ id: string;
69
+ name: string;
70
+ profile: string;
71
+ createdAt: number;
72
+ endedAt: number;
73
+ domains: string[];
74
+ tabCount: number;
75
+ }
76
+ export type IPCAction = 'start' | 'launch-profile' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download';
47
77
  export interface IPCRequest {
48
78
  action: IPCAction;
49
79
  task?: string;
80
+ taskName?: string;
50
81
  profile?: string;
51
82
  url?: string;
52
83
  tabId?: string;
@@ -55,8 +86,26 @@ export interface IPCRequest {
55
86
  ref?: number;
56
87
  text?: string;
57
88
  key?: string;
89
+ scrollX?: number;
90
+ scrollY?: number;
91
+ scrollAtX?: number;
92
+ scrollAtY?: number;
58
93
  interactive?: boolean;
59
94
  limit?: number;
95
+ width?: number;
96
+ height?: number;
97
+ deviceName?: string;
98
+ mobile?: boolean;
99
+ deviceScaleFactor?: number;
100
+ level?: 'log' | 'info' | 'warn' | 'error';
101
+ clear?: boolean;
102
+ filter?: string;
103
+ urlPattern?: string;
104
+ maxChars?: number;
105
+ waitType?: 'time' | 'selector' | 'url' | 'function' | 'load';
106
+ waitValue?: string | number;
107
+ timeout?: number;
108
+ downloadPath?: string;
60
109
  }
61
110
  export interface IPCResponse {
62
111
  ok: boolean;
@@ -66,10 +115,49 @@ export interface IPCResponse {
66
115
  windowTargetId?: string;
67
116
  tabs?: TabInfo[];
68
117
  profiles?: ProfileStatus[];
118
+ history?: HistoricalTask[];
69
119
  result?: unknown;
70
120
  path?: string;
71
121
  refs?: string;
122
+ port?: number;
123
+ pid?: number;
124
+ logs?: ConsoleEntry[];
125
+ errors?: ErrorEntry[];
126
+ requests?: NetworkRequest[];
127
+ body?: string;
128
+ downloadPath?: string;
129
+ devices?: string[];
130
+ }
131
+ export interface ConsoleEntry {
132
+ level: 'log' | 'info' | 'warn' | 'error';
133
+ text: string;
134
+ timestamp: number;
135
+ url?: string;
136
+ line?: number;
137
+ }
138
+ export interface ErrorEntry {
139
+ message: string;
140
+ stack?: string;
141
+ timestamp: number;
142
+ url?: string;
143
+ line?: number;
144
+ }
145
+ export interface NetworkRequest {
146
+ id: string;
147
+ url: string;
148
+ method: string;
149
+ status?: number;
150
+ mimeType?: string;
151
+ timestamp: number;
152
+ }
153
+ export interface DeviceDescriptor {
154
+ width: number;
155
+ height: number;
156
+ deviceScaleFactor: number;
157
+ mobile: boolean;
72
158
  }
73
159
  export declare const TASK_ID_REGEX: RegExp;
74
160
  export declare function isValidTaskId(id: string): boolean;
75
161
  export declare function generateTaskId(): string;
162
+ export declare function generateShortId(): string;
163
+ export declare function generateFunName(): string;
@@ -5,3 +5,19 @@ export function isValidTaskId(id) {
5
5
  export function generateTaskId() {
6
6
  return crypto.randomUUID().slice(0, 8);
7
7
  }
8
+ export function generateShortId() {
9
+ return crypto.randomUUID().split('-')[0]; // 8 chars
10
+ }
11
+ const ADJECTIVES = [
12
+ 'swift', 'cosmic', 'jolly', 'quiet', 'bold', 'bright', 'calm', 'eager',
13
+ 'golden', 'happy', 'keen', 'lucky', 'noble', 'proud', 'quick', 'royal',
14
+ ];
15
+ const NOUNS = [
16
+ 'falcon', 'comet', 'tiger', 'nebula', 'phoenix', 'river', 'summit', 'wave',
17
+ 'aurora', 'breeze', 'crystal', 'dragon', 'ember', 'forest', 'glacier', 'harbor',
18
+ ];
19
+ export function generateFunName() {
20
+ const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
21
+ const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
22
+ return `${adj}-${noun}`;
23
+ }
@@ -5,6 +5,8 @@
5
5
  * Requires the Rush GitHub App installed on the target repo.
6
6
  */
7
7
  import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOptions, ProviderCapabilities } from './types.js';
8
+ export declare const RUSH_CONSENT_PATH: string;
9
+ export declare function hasRushUploadConsent(opts?: DispatchOptions): boolean;
8
10
  /** One version's entry in the account manifest sent on every dispatch. */
9
11
  export interface AccountManifestEntry {
10
12
  version: string;
@@ -35,7 +37,7 @@ export interface AccountTokenEntry {
35
37
  * Returns null when no Claude versions are signed in (the dispatch falls back
36
38
  * to the platform-wide key, current behavior).
37
39
  */
38
- export declare function buildAccountManifest(): Promise<AccountManifest | null>;
40
+ export declare function buildAccountManifest(strategy?: string): Promise<AccountManifest | null>;
39
41
  /**
40
42
  * Re-load OAuth blobs for the given versions so they can be uploaded to the
41
43
  * server on a retry. Only the versions named in the manifest are loaded — we
@@ -52,6 +54,7 @@ export declare function buildDispatchBody(input: {
52
54
  agent?: string;
53
55
  prompt: string;
54
56
  mode?: string;
57
+ strategy?: string;
55
58
  resolvedRepos: Array<{
56
59
  installation_id: number;
57
60
  repo_owner: string;
@@ -60,6 +63,30 @@ export declare function buildDispatchBody(input: {
60
63
  accountManifest?: AccountManifest | null;
61
64
  accountTokens?: AccountTokenEntry[] | null;
62
65
  }): Record<string, unknown>;
66
+ /** A single account registered in Rush Cloud's multi-account rotation pool. */
67
+ export interface RemoteAccount {
68
+ id: string;
69
+ provider: string;
70
+ email: string | null;
71
+ subscription_type: string | null;
72
+ five_hour_pct: number | null;
73
+ seven_day_pct: number | null;
74
+ usage_fetched_at: string | null;
75
+ created_at: string;
76
+ }
77
+ /** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
78
+ export declare function listRemoteAccounts(): Promise<RemoteAccount[]>;
79
+ /**
80
+ * Register a CLAUDE_CODE_OAUTH_TOKEN with Rush Cloud's rotation pool.
81
+ * The server validates the token against the Anthropic usage API and stores it
82
+ * encrypted in Vault. Returns the account metadata (no token).
83
+ */
84
+ export declare function addRemoteAccount(provider: string, pastedToken: string): Promise<RemoteAccount & {
85
+ five_hour_pct: number | null;
86
+ seven_day_pct: number | null;
87
+ }>;
88
+ /** Remove a Claude account from Rush Cloud's rotation pool by its ID. */
89
+ export declare function removeRemoteAccount(id: string): Promise<void>;
63
90
  export declare class RushCloudProvider implements CloudProvider {
64
91
  id: "rush";
65
92
  name: string;
@@ -9,20 +9,21 @@ import * as path from 'path';
9
9
  import * as os from 'os';
10
10
  import * as crypto from 'crypto';
11
11
  import * as yaml from 'yaml';
12
- import { getUserAgentsDir } from '../state.js';
12
+ import { getCloudDir } from '../state.js';
13
13
  import { resolveDispatchRepos } from './types.js';
14
14
  import { parseSSE } from './stream.js';
15
15
  import { listInstalledVersions, getVersionHomePath } from '../versions.js';
16
16
  import { getAccountInfo } from '../agents.js';
17
17
  import { loadClaudeOauth } from '../usage.js';
18
+ import { selectBalancedVersion } from '../rotate.js';
18
19
  const PROXY_BASE = 'https://api.prix.dev';
19
20
  const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
20
21
  // Persistent consent record for uploading Claude OAuth blobs to Rush Cloud.
21
22
  // Created on first explicit consent (env var or flag); subsequent dispatches
22
23
  // see it and proceed without re-prompting.
23
- const RUSH_CONSENT_PATH = path.join(getUserAgentsDir(), 'cloud', 'rush-consent.json');
24
+ export const RUSH_CONSENT_PATH = path.join(getCloudDir(), 'rush-consent.json');
24
25
  const RUSH_CONSENT_ENV = 'AGENTS_RUSH_UPLOAD_TOKENS';
25
- function hasRushUploadConsent(opts) {
26
+ export function hasRushUploadConsent(opts) {
26
27
  if (process.env[RUSH_CONSENT_ENV] === '1')
27
28
  return true;
28
29
  const po = opts?.providerOptions;
@@ -184,20 +185,37 @@ async function readClaudeCredentialsBlob(home) {
184
185
  * Returns null when no Claude versions are signed in (the dispatch falls back
185
186
  * to the platform-wide key, current behavior).
186
187
  */
187
- export async function buildAccountManifest() {
188
- const versions = listInstalledVersions('claude');
189
- if (versions.length === 0)
190
- return null;
188
+ export async function buildAccountManifest(strategy) {
189
+ let candidateVersions;
190
+ if (strategy === 'balanced') {
191
+ // Use the same health-checked, deduped-by-email set that `agents run --balanced` uses.
192
+ // `result.healthy` contains one candidate per unique email, ordered by remaining capacity.
193
+ const result = await selectBalancedVersion('claude');
194
+ if (!result || result.healthy.length === 0)
195
+ return null;
196
+ candidateVersions = result.healthy
197
+ .filter((c) => !!c.email)
198
+ .map((c) => ({ version: c.version, email: c.email }));
199
+ }
200
+ else {
201
+ // Default: all installed versions that have a signed-in account.
202
+ const versions = listInstalledVersions('claude');
203
+ if (versions.length === 0)
204
+ return null;
205
+ const rows = await Promise.all(versions.map(async (version) => {
206
+ const home = getVersionHomePath('claude', version);
207
+ const info = await getAccountInfo('claude', home);
208
+ return info.email ? { version, email: info.email } : null;
209
+ }));
210
+ candidateVersions = rows.filter((r) => r !== null);
211
+ }
191
212
  const entries = [];
192
- for (const version of versions) {
213
+ for (const { version, email } of candidateVersions) {
193
214
  const home = getVersionHomePath('claude', version);
194
- const info = await getAccountInfo('claude', home);
195
- if (!info.email)
196
- continue;
197
215
  const blob = await readClaudeCredentialsBlob(home);
198
216
  if (!blob)
199
217
  continue;
200
- entries.push({ version, email: info.email, cred_fp: sha256(blob) });
218
+ entries.push({ version, email, cred_fp: sha256(blob) });
201
219
  }
202
220
  if (entries.length === 0)
203
221
  return null;
@@ -237,6 +255,7 @@ export function buildDispatchBody(input) {
237
255
  prompt: input.prompt,
238
256
  repos: input.resolvedRepos,
239
257
  mode: input.mode,
258
+ ...(input.strategy ? { strategy: input.strategy } : {}),
240
259
  };
241
260
  if (input.resolvedRepos.length === 1) {
242
261
  body.installation_id = primary.installation_id;
@@ -251,6 +270,37 @@ export function buildDispatchBody(input) {
251
270
  }
252
271
  return body;
253
272
  }
273
+ /** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
274
+ export async function listRemoteAccounts() {
275
+ const token = readToken();
276
+ const res = await api('GET', '/api/v1/cloud-accounts', token);
277
+ if (!res.ok) {
278
+ throw new Error(`Failed to list accounts (${res.status}): ${sanitizeErrorBody(await res.text())}`);
279
+ }
280
+ const data = await res.json();
281
+ return data.accounts ?? [];
282
+ }
283
+ /**
284
+ * Register a CLAUDE_CODE_OAUTH_TOKEN with Rush Cloud's rotation pool.
285
+ * The server validates the token against the Anthropic usage API and stores it
286
+ * encrypted in Vault. Returns the account metadata (no token).
287
+ */
288
+ export async function addRemoteAccount(provider, pastedToken) {
289
+ const token = readToken();
290
+ const res = await api('POST', '/api/v1/cloud-accounts', token, { provider, token: pastedToken });
291
+ if (!res.ok) {
292
+ throw new Error(`Failed to add account (${res.status}): ${sanitizeErrorBody(await res.text())}`);
293
+ }
294
+ return await res.json();
295
+ }
296
+ /** Remove a Claude account from Rush Cloud's rotation pool by its ID. */
297
+ export async function removeRemoteAccount(id) {
298
+ const token = readToken();
299
+ const res = await api('DELETE', `/api/v1/cloud-accounts/${encodeURIComponent(id)}`, token);
300
+ if (!res.ok) {
301
+ throw new Error(`Failed to remove account (${res.status}): ${sanitizeErrorBody(await res.text())}`);
302
+ }
303
+ }
254
304
  export class RushCloudProvider {
255
305
  id = 'rush';
256
306
  name = 'Rush Cloud';
@@ -289,13 +339,18 @@ export class RushCloudProvider {
289
339
  repo_owner: r.owner,
290
340
  repo_name: r.name,
291
341
  })));
292
- const accountManifest = await buildAccountManifest();
342
+ const strategy = options.providerOptions?.strategy;
343
+ // When balanced, the server owns the pool and rotates internally — no
344
+ // client-side manifest needed. We just forward the strategy so the server
345
+ // knows to load from Vault instead of waiting for a manifest.
346
+ const accountManifest = strategy === 'balanced' ? null : await buildAccountManifest();
293
347
  const body = buildDispatchBody({
294
348
  agent: options.agent,
295
349
  prompt: options.prompt,
296
350
  mode: options.providerOptions?.mode,
297
351
  resolvedRepos,
298
352
  accountManifest,
353
+ strategy,
299
354
  });
300
355
  let res = await api('POST', '/api/v1/cloud-runs', token, body);
301
356
  // Server detects drift (new account or rotated token) by comparing the
@@ -317,7 +372,7 @@ export class RushCloudProvider {
317
372
  ``,
318
373
  `To consent, re-run with one of:`,
319
374
  ` AGENTS_RUSH_UPLOAD_TOKENS=1 agents cloud run ...`,
320
- ` agents cloud run --upload-account-tokens ... # if your CLI exposes this flag`,
375
+ ` agents cloud run --upload-account-tokens ...`,
321
376
  ``,
322
377
  `Consent will be recorded at ${RUSH_CONSENT_PATH} so you won't be asked again.`,
323
378
  `Remove that file to revoke.`,
@@ -8,8 +8,8 @@
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import Database from '../sqlite.js';
11
- import { getUserAgentsDir } from '../state.js';
12
- const CLOUD_DIR = path.join(getUserAgentsDir(), 'cloud');
11
+ import { getCloudDir } from '../state.js';
12
+ const CLOUD_DIR = getCloudDir();
13
13
  const DB_PATH = path.join(CLOUD_DIR, 'tasks.db');
14
14
  const SCHEMA = `
15
15
  CREATE TABLE IF NOT EXISTS tasks (
@@ -35,10 +35,6 @@ export interface InstalledCommand {
35
35
  path: string;
36
36
  description?: string;
37
37
  }
38
- /** Parse command metadata (name, description) from YAML frontmatter or TOML headers. */
39
- export declare function parseCommandMetadata(filePath: string): CommandMetadata | null;
40
- /** Validate command metadata, returning errors and warnings. */
41
- export declare function validateCommandMetadata(metadata: CommandMetadata | null, commandName: string): ValidationResult;
42
38
  /** Discover all command markdown files in a repository's commands/ directory. */
43
39
  export declare function discoverCommands(repoPath: string): DiscoveredCommand[];
44
40
  /** Find the source path for a command in a repository. */
@@ -79,6 +75,7 @@ export declare function installCommandToVersion(agent: AgentId, version: string,
79
75
  };
80
76
  /**
81
77
  * Remove a single command from a specific version home.
78
+ * Soft-deletes to ~/.agents/.trash/commands/.
82
79
  */
83
80
  export declare function removeCommandFromVersion(agent: AgentId, version: string, commandName: string): {
84
81
  success: boolean;
@@ -97,17 +94,6 @@ export declare function iterCommandsCapableVersions(filter?: {
97
94
  }>;
98
95
  /** Remove a command from an agent's config directory. */
99
96
  export declare function uninstallCommand(agentId: AgentId, commandName: string): boolean;
100
- /** List command names installed for an agent in the active version home. */
101
- export declare function listInstalledCommands(agentId: AgentId): string[];
102
- /**
103
- * Check if a command exists for an agent.
104
- */
105
- export declare function commandExists(agentId: AgentId, commandName: string): boolean;
106
- /**
107
- * Check if installed command content matches source content.
108
- * Handles format conversion (markdown to TOML for Gemini).
109
- */
110
- export declare function commandContentMatches(agentId: AgentId, commandName: string, sourcePath: string): boolean;
111
97
  /**
112
98
  * List installed commands with scope information.
113
99
  * Pass options.home to read from a version-managed agent's home directory.
@@ -11,11 +11,11 @@ import * as path from 'path';
11
11
  import * as yaml from 'yaml';
12
12
  import { AGENTS, COMMANDS_CAPABLE_AGENTS, ensureCommandsDir } from './agents.js';
13
13
  import { markdownToToml } from './convert.js';
14
- import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAgentsDir, getSkillsDir } from './state.js';
14
+ import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAgentsDir, getSkillsDir, getTrashCommandsDir } from './state.js';
15
15
  import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
16
16
  import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldInstallCommandAsSkill, } from './command-skills.js';
17
17
  /** Parse command metadata (name, description) from YAML frontmatter or TOML headers. */
18
- export function parseCommandMetadata(filePath) {
18
+ function parseCommandMetadata(filePath) {
19
19
  if (!fs.existsSync(filePath)) {
20
20
  return null;
21
21
  }
@@ -51,7 +51,7 @@ export function parseCommandMetadata(filePath) {
51
51
  }
52
52
  }
53
53
  /** Validate command metadata, returning errors and warnings. */
54
- export function validateCommandMetadata(metadata, commandName) {
54
+ function validateCommandMetadata(metadata, commandName) {
55
55
  const errors = [];
56
56
  const warnings = [];
57
57
  if (!metadata) {
@@ -279,6 +279,7 @@ export function installCommandToVersion(agent, version, commandName, method = 'c
279
279
  }
280
280
  /**
281
281
  * Remove a single command from a specific version home.
282
+ * Soft-deletes to ~/.agents/.trash/commands/.
282
283
  */
283
284
  export function removeCommandFromVersion(agent, version, commandName) {
284
285
  const versionHome = getVersionHomePath(agent, version);
@@ -292,7 +293,10 @@ export function removeCommandFromVersion(agent, version, commandName) {
292
293
  return { success: true };
293
294
  }
294
295
  try {
295
- fs.unlinkSync(targetPath);
296
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
297
+ const trashDir = path.join(getTrashCommandsDir(), agent, version, commandName);
298
+ fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
299
+ fs.renameSync(targetPath, path.join(trashDir, `${commandName}${ext}.${stamp}`));
296
300
  }
297
301
  catch (err) {
298
302
  return { success: false, error: err.message };
@@ -332,7 +336,7 @@ export function uninstallCommand(agentId, commandName) {
332
336
  return false;
333
337
  }
334
338
  /** List command names installed for an agent in the active version home. */
335
- export function listInstalledCommands(agentId) {
339
+ function listInstalledCommands(agentId) {
336
340
  const agent = AGENTS[agentId];
337
341
  const home = getEffectiveHome(agentId);
338
342
  const commandsDir = path.join(home, `.${agentId}`, agent.commandsSubdir);
@@ -348,7 +352,7 @@ export function listInstalledCommands(agentId) {
348
352
  /**
349
353
  * Check if a command exists for an agent.
350
354
  */
351
- export function commandExists(agentId, commandName) {
355
+ function commandExists(agentId, commandName) {
352
356
  const agent = AGENTS[agentId];
353
357
  const home = getEffectiveHome(agentId);
354
358
  const commandsDir = path.join(home, `.${agentId}`, agent.commandsSubdir);
@@ -366,7 +370,7 @@ function normalizeContent(content) {
366
370
  * Check if installed command content matches source content.
367
371
  * Handles format conversion (markdown to TOML for Gemini).
368
372
  */
369
- export function commandContentMatches(agentId, commandName, sourcePath) {
373
+ function commandContentMatches(agentId, commandName, sourcePath) {
370
374
  const agent = AGENTS[agentId];
371
375
  const home = getEffectiveHome(agentId);
372
376
  const commandsDir = path.join(home, `.${agentId}`, agent.commandsSubdir);
@@ -10,13 +10,12 @@ import { spawn, execSync, execFileSync } from 'child_process';
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as os from 'os';
13
- import { getAgentsDir } from './state.js';
13
+ import { getDaemonDir as getDaemonDirRoot } from './state.js';
14
14
  import { listJobs as listAllJobs } from './routines.js';
15
15
  import { JobScheduler } from './scheduler.js';
16
16
  import { executeJobDetached, monitorRunningJobs } from './runner.js';
17
17
  import { BrowserService } from './browser/service.js';
18
18
  import { BrowserIPCServer } from './browser/ipc.js';
19
- const DAEMON_DIR = 'helpers/daemon';
20
19
  const PID_FILE = 'daemon.pid';
21
20
  const LOCK_FILE = 'daemon.lock';
22
21
  const LOG_FILE = 'logs.jsonl';
@@ -25,7 +24,7 @@ const LOG_ROTATE_COUNT = 3;
25
24
  const PLIST_NAME = 'com.phnx-labs.agents-daemon';
26
25
  const SYSTEMD_UNIT = 'agents-daemon.service';
27
26
  function getDaemonDir() {
28
- const dir = path.join(getAgentsDir(), DAEMON_DIR);
27
+ const dir = getDaemonDirRoot();
29
28
  fs.mkdirSync(dir, { recursive: true });
30
29
  return dir;
31
30
  }
@@ -22,7 +22,7 @@
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
  import { AGENTS } from './agents.js';
25
- import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir, getPromptcutsPath, } from './state.js';
25
+ import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir, getEffectivePromptcutsPath, } from './state.js';
26
26
  import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, } from './versions.js';
27
27
  import { markdownToToml } from './convert.js';
28
28
  import { resolveImports, supportsRulesImports } from './rules/compile.js';
@@ -426,10 +426,10 @@ function diffPresenceOnly(kind, available, synced) {
426
426
  return rows.sort((a, b) => a.name.localeCompare(b.name));
427
427
  }
428
428
  function diffPromptcuts() {
429
- const exists = fs.existsSync(getPromptcutsPath());
430
- if (!exists)
429
+ const sourcePath = getEffectivePromptcutsPath();
430
+ if (!fs.existsSync(sourcePath))
431
431
  return [];
432
- return [{ kind: 'promptcuts', name: 'promptcuts.yaml', status: 'ok', sourcePath: getPromptcutsPath() }];
432
+ return [{ kind: 'promptcuts', name: 'promptcuts.yaml', status: 'ok', sourcePath }];
433
433
  }
434
434
  export function diffVersionResources(agent, version, options = {}) {
435
435
  const cwd = options.cwd ?? process.cwd();
@@ -15,8 +15,8 @@ import * as fs from 'fs';
15
15
  import * as path from 'path';
16
16
  import * as os from 'os';
17
17
  // ─── Constants ────────────────────────────────────────────────────────────────
18
- const USER_AGENTS_DIR = path.join(os.homedir(), '.agents');
19
- const LOGS_DIR = path.join(USER_AGENTS_DIR, 'logs');
18
+ // Logs live under the cache bucket — they're regenerable telemetry.
19
+ const LOGS_DIR = path.join(os.homedir(), '.agents', '.cache', 'logs');
20
20
  /** Default retention period in days. */
21
21
  const DEFAULT_RETENTION_DAYS = 30;
22
22
  /** Default max length for truncated strings. */
@@ -66,6 +66,7 @@ export declare function installHookToVersion(agent: AgentId, version: string, ho
66
66
  };
67
67
  /**
68
68
  * Remove a single hook (script + data file) from a specific version home.
69
+ * Soft-deletes to ~/.agents/.trash/hooks/.
69
70
  */
70
71
  export declare function removeHookFromVersion(agent: AgentId, version: string, hookName: string): {
71
72
  success: boolean;
@@ -113,10 +114,11 @@ export declare function installHooksCentrally(source: string): Promise<{
113
114
  */
114
115
  export declare function listCentralHooks(): HookEntry[];
115
116
  /**
116
- * Parse hooks.yaml manifests. Reads BOTH system (~/.agents-system/hooks.yaml)
117
- * and user (~/.agents/hooks.yaml), merging with user-wins-on-key-collision
118
- * precedence. A user entry with `enabled: false` disables the system-shipped
119
- * hook of the same name without forking the system file.
117
+ * Parse hook manifests. Reads system hooks from ~/.agents-system/hooks.yaml
118
+ * (npm-shipped defaults) and user hooks from the `hooks:` section of
119
+ * ~/.agents/agents.yaml. Merges with user-wins-on-key-collision precedence.
120
+ * A user entry with `enabled: false` disables the system-shipped hook of
121
+ * the same name without forking the system file.
120
122
  *
121
123
  * Hooks marked `enabled: false` are dropped from the returned map.
122
124
  */
@@ -124,10 +126,12 @@ export declare function parseHookManifest(): Record<string, ManifestHook>;
124
126
  /**
125
127
  * Register hooks as lifecycle events in an agent's config.
126
128
  * Reads hooks.yaml manifest, merges into the agent's config file(s).
127
- * Only manages hooks whose command paths are under ~/.agents/hooks/.
128
- * Does not remove user-added hooks.
129
+ * Only manages hooks whose command paths are under ~/.agents/hooks/ or
130
+ * ~/.agents-system/hooks/. Does not remove user-added hooks.
129
131
  *
130
- * @param agentsDirOverride - Override the agents dir (used in tests to inject a temp path).
132
+ * @param agentsDirOverride - When provided, treats this single dir as the
133
+ * only managed hook root. Used by tests to inject a temp path. In normal
134
+ * operation, both user and system roots are consulted with user precedence.
131
135
  */
132
136
  export declare function registerHooksToSettings(agentId: AgentId, versionHome: string, hookManifest?: Record<string, ManifestHook>, agentsDirOverride?: string): {
133
137
  registered: string[];