@phnx-labs/agents-cli 1.16.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 (60) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/commands/browser.js +248 -9
  3. package/dist/commands/cloud.js +8 -0
  4. package/dist/commands/exec.js +70 -1
  5. package/dist/commands/plugins.js +179 -5
  6. package/dist/commands/prune.js +6 -0
  7. package/dist/commands/secrets.js +117 -19
  8. package/dist/commands/view.js +21 -8
  9. package/dist/commands/workflows.d.ts +10 -0
  10. package/dist/commands/workflows.js +457 -0
  11. package/dist/index.js +31 -16
  12. package/dist/lib/browser/cdp.js +7 -4
  13. package/dist/lib/browser/chrome.d.ts +10 -0
  14. package/dist/lib/browser/chrome.js +37 -2
  15. package/dist/lib/browser/drivers/local.js +13 -2
  16. package/dist/lib/browser/input.d.ts +1 -0
  17. package/dist/lib/browser/input.js +3 -0
  18. package/dist/lib/browser/ipc.js +14 -0
  19. package/dist/lib/browser/profiles.d.ts +5 -0
  20. package/dist/lib/browser/profiles.js +45 -0
  21. package/dist/lib/browser/service.d.ts +10 -0
  22. package/dist/lib/browser/service.js +29 -1
  23. package/dist/lib/browser/types.d.ts +11 -1
  24. package/dist/lib/cloud/rush.d.ts +28 -1
  25. package/dist/lib/cloud/rush.js +68 -13
  26. package/dist/lib/commands.d.ts +0 -15
  27. package/dist/lib/commands.js +5 -5
  28. package/dist/lib/hooks.js +24 -11
  29. package/dist/lib/migrate.js +59 -1
  30. package/dist/lib/permissions.d.ts +0 -58
  31. package/dist/lib/permissions.js +10 -10
  32. package/dist/lib/plugins.d.ts +75 -34
  33. package/dist/lib/plugins.js +640 -133
  34. package/dist/lib/resource-patterns.d.ts +41 -0
  35. package/dist/lib/resource-patterns.js +82 -0
  36. package/dist/lib/resources/index.d.ts +17 -0
  37. package/dist/lib/resources/index.js +7 -0
  38. package/dist/lib/resources/types.d.ts +1 -1
  39. package/dist/lib/resources/workflows.d.ts +24 -0
  40. package/dist/lib/resources/workflows.js +110 -0
  41. package/dist/lib/resources.d.ts +6 -1
  42. package/dist/lib/resources.js +12 -2
  43. package/dist/lib/session/db.d.ts +18 -0
  44. package/dist/lib/session/db.js +106 -7
  45. package/dist/lib/session/discover.d.ts +6 -0
  46. package/dist/lib/session/discover.js +28 -17
  47. package/dist/lib/shims.d.ts +3 -51
  48. package/dist/lib/shims.js +18 -10
  49. package/dist/lib/sqlite.js +10 -4
  50. package/dist/lib/state.d.ts +15 -2
  51. package/dist/lib/state.js +29 -8
  52. package/dist/lib/types.d.ts +43 -14
  53. package/dist/lib/versions.d.ts +3 -0
  54. package/dist/lib/versions.js +139 -27
  55. package/dist/lib/workflows.d.ts +79 -0
  56. package/dist/lib/workflows.js +233 -0
  57. package/package.json +1 -5
  58. package/scripts/postinstall.js +59 -58
  59. package/dist/commands/fork.d.ts +0 -10
  60. package/dist/commands/fork.js +0 -146
@@ -13,23 +13,22 @@ import * as crypto from 'crypto';
13
13
  import * as readline from 'readline';
14
14
  import { execFile } from 'child_process';
15
15
  import { promisify } from 'util';
16
- import { getAgentsDir, getUserAgentsDir } from '../state.js';
16
+ import { getAgentsDir, getHistoryDir } from '../state.js';
17
17
  const execFileAsync = promisify(execFile);
18
18
  import { AGENTS, getCliVersion } from '../agents.js';
19
19
  import { walkForFiles } from '../fs-walk.js';
20
20
  import { getConfigSymlinkVersion } from '../shims.js';
21
21
  import { SESSION_AGENTS } from './types.js';
22
22
  import { extractSessionTopic } from './prompt.js';
23
- import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, upsertSessionsBatch, querySessions, countSessions, ftsSearch, } from './db.js';
23
+ import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
24
24
  const HOME = os.homedir();
25
25
  // Versions can live under either repo: the user repo (current canonical
26
- // location, ~/.agents/versions/) or the system repo (legacy / npm-shipped,
26
+ // location, ~/.agents/.history/versions/) or the system repo (legacy / npm-shipped,
27
27
  // ~/.agents-system/versions/). Both must be scanned — sessions written by
28
28
  // any installed version end up in that version's projects/ dir, and the user
29
29
  // can be running one repo's version while another repo holds older versions
30
30
  // whose JSONLs the user still wants to search.
31
- const VERSIONS_ROOTS = [getUserAgentsDir(), getAgentsDir()];
32
- const AGENTS_DIR = getAgentsDir();
31
+ const VERSIONS_ROOTS = [getHistoryDir(), getAgentsDir()];
33
32
  const RUSH_SESSIONS_DIR = path.join(HOME, '.rush', 'sessions');
34
33
  const HERMES_SESSIONS_DIR = path.join(HOME, '.hermes', 'sessions');
35
34
  /** How long OpenClaw channel/cron snapshots stay valid before we re-shell-out. */
@@ -39,24 +38,36 @@ const cachedAgentVersions = new Map();
39
38
  /**
40
39
  * Discover sessions. Scans only files whose (mtime, size) have changed since
41
40
  * the last run; everything else is served from the SQLite cache.
41
+ *
42
+ * Only one process runs the incremental scan at a time. When many agents boot
43
+ * simultaneously (e.g. after a restart), the first to claim the scan slot does
44
+ * the work; the rest skip parsing entirely and serve from the DB. The claim is
45
+ * stored in the `meta` table — crash-safe via dead-PID detection and a 2-min
46
+ * TTL, no external lock files needed.
42
47
  */
43
48
  export async function discoverSessions(options) {
44
49
  // Touch the DB so the schema is ready and connection is cached for this run.
45
50
  getDB();
46
51
  const agents = options?.agent ? [options.agent] : SESSION_AGENTS;
47
52
  const onProgress = options?.onProgress;
48
- // Incrementally re-scan changed files across all selected agents in parallel.
49
- await Promise.all(agents.map(agent => {
50
- switch (agent) {
51
- case 'claude': return scanClaudeIncremental(onProgress);
52
- case 'codex': return scanCodexIncremental(onProgress);
53
- case 'gemini': return scanGeminiIncremental(onProgress);
54
- case 'opencode': return scanOpenCodeIncremental();
55
- case 'openclaw': return scanOpenClawIncremental();
56
- case 'rush': return scanRushIncremental(onProgress);
57
- case 'hermes': return scanHermesIncremental(onProgress);
53
+ if (tryClaimScan(process.pid)) {
54
+ try {
55
+ await Promise.all(agents.map(agent => {
56
+ switch (agent) {
57
+ case 'claude': return scanClaudeIncremental(onProgress);
58
+ case 'codex': return scanCodexIncremental(onProgress);
59
+ case 'gemini': return scanGeminiIncremental(onProgress);
60
+ case 'opencode': return scanOpenCodeIncremental();
61
+ case 'openclaw': return scanOpenClawIncremental();
62
+ case 'rush': return scanRushIncremental(onProgress);
63
+ case 'hermes': return scanHermesIncremental(onProgress);
64
+ }
65
+ }));
58
66
  }
59
- }));
67
+ finally {
68
+ releaseScan(process.pid);
69
+ }
70
+ }
60
71
  const sessions = querySessions(buildQueryOptions(options, agents, { includeLimit: true }));
61
72
  return sessions;
62
73
  }
@@ -202,7 +213,7 @@ export function getAgentSessionDirs(agent, subdir) {
202
213
  }
203
214
  catch { /* dir unreadable */ }
204
215
  }
205
- const backupsBase = path.join(AGENTS_DIR, 'backups', agent);
216
+ const backupsBase = path.join(getHistoryDir(), 'backups', agent);
206
217
  if (fs.existsSync(backupsBase)) {
207
218
  try {
208
219
  for (const ts of fs.readdirSync(backupsBase)) {
@@ -13,15 +13,6 @@ export interface ConflictInfo {
13
13
  version: string;
14
14
  conflicts: string[];
15
15
  }
16
- /**
17
- * Detect conflicting files between source and destination directories.
18
- * Returns list of filenames that exist in both locations (excluding symlinks in dest).
19
- */
20
- export declare function detectConflicts(src: string, dest: string, prefix?: string): string[];
21
- /**
22
- * Prompt user for conflict resolution strategy.
23
- */
24
- export declare function promptConflictStrategy(conflictInfos: ConflictInfo[]): Promise<ConflictStrategy | null>;
25
16
  /**
26
17
  * Generate the shim script content for an agent.
27
18
  *
@@ -57,8 +48,10 @@ export declare function promptConflictStrategy(conflictInfos: ConflictInfo[]): P
57
48
  * and capability flag `memoryImports` → `rulesImports`.
58
49
  * v8 — versions moved from ~/.agents-system/versions to ~/.agents/versions
59
50
  * (two-repo split: system = shipped defaults, user = operational state).
51
+ * v9 — claude shim exports CLAUDE_CODE_OAUTH_TOKEN from per-version
52
+ * .oauth_token file on Linux (keychain-less sandbox fallback).
60
53
  */
61
- export declare const SHIM_SCHEMA_VERSION = 9;
54
+ export declare const SHIM_SCHEMA_VERSION = 10;
62
55
  /**
63
56
  * Generate the full bash shim script for the given agent. The returned string
64
57
  * is written to ~/.agents/shims/{cliCommand} and made executable.
@@ -128,14 +121,6 @@ export declare function removeVersionedAlias(agent: AgentId, version: string): b
128
121
  * Check if a versioned alias exists.
129
122
  */
130
123
  export declare function versionedAliasExists(agent: AgentId, version: string): boolean;
131
- /**
132
- * Detect conflicts that would occur when switching config symlink for an agent/version.
133
- * This allows collecting conflicts upfront before prompting for a strategy.
134
- *
135
- * Returns null if no migration is needed (already symlink or doesn't exist),
136
- * or ConflictInfo with the list of conflicting files.
137
- */
138
- export declare function detectMigrationConflicts(agent: AgentId, version: string): ConflictInfo | null;
139
124
  /**
140
125
  * Switch the agent's config symlink to point to a specific version.
141
126
  * e.g., ~/.claude -> ~/.agents/versions/claude/2.0.65/home/.claude/
@@ -185,14 +170,6 @@ export declare function switchHomeFileSymlinks(agent: AgentId, version: string):
185
170
  * - Symlink points elsewhere: replace it.
186
171
  */
187
172
  export declare function ensureClaudeInsideSymlink(version: string): void;
188
- /**
189
- * Apply `ensureClaudeInsideSymlink` to every installed Claude version.
190
- * Safe to call repeatedly; per-version calls are idempotent.
191
- */
192
- export declare function ensureAllClaudeInsideSymlinks(): {
193
- migrated: string[];
194
- errors: string[];
195
- };
196
173
  /**
197
174
  * Get the current config symlink target version, if any.
198
175
  */
@@ -201,17 +178,6 @@ export declare function getConfigSymlinkVersion(agent: AgentId): string | null;
201
178
  * Check if shim exists for an agent.
202
179
  */
203
180
  export declare function shimExists(agent: AgentId): boolean;
204
- /**
205
- * Read the schema version embedded in an existing on-disk shim. Returns
206
- * `null` if the shim doesn't exist or has no version marker (pre-v2 shim).
207
- */
208
- export declare function readShimSchemaVersion(agent: AgentId): number | null;
209
- /**
210
- * True if the on-disk shim's schema version matches `SHIM_SCHEMA_VERSION`.
211
- * False means either the shim is missing, is pre-v2 (no marker), or is an
212
- * older version that needs regeneration.
213
- */
214
- export declare function isShimCurrent(agent: AgentId): boolean;
215
181
  /**
216
182
  * Regenerate the shim if it's missing or outdated. Returns a status describing
217
183
  * what happened — callers can surface a one-line notice to the user ("Updated
@@ -276,10 +242,6 @@ export declare function addShimsToPath(overrides?: {
276
242
  error?: string;
277
243
  };
278
244
  export declare function listAgentsWithInstalledVersions(): AgentId[];
279
- /**
280
- * Create shims for all installed agents.
281
- */
282
- export declare function ensureAllShims(): void;
283
245
  /**
284
246
  * Resource diff between two versions. Each field lists resources present in
285
247
  * the current version but missing from the target.
@@ -295,17 +257,7 @@ export interface ResourceDiff {
295
257
  }[];
296
258
  mcp: string[];
297
259
  }
298
- /**
299
- * Compare resources between two versions.
300
- * Returns resources that exist in currentVersion but not in targetVersion.
301
- */
302
- export declare function compareVersionResources(agent: AgentId, currentVersion: string, targetVersion: string): ResourceDiff;
303
260
  /**
304
261
  * Check if a ResourceDiff has any differences.
305
262
  */
306
263
  export declare function hasResourceDiff(diff: ResourceDiff): boolean;
307
- /**
308
- * Copy resources from one version to another.
309
- * Only copies resources listed in the diff (i.e., ones missing in target).
310
- */
311
- export declare function copyResourcesToVersion(agent: AgentId, fromVersion: string, toVersion: string, diff: ResourceDiff): void;
package/dist/lib/shims.js CHANGED
@@ -44,7 +44,7 @@ function shouldIgnore(name) {
44
44
  * Detect conflicting files between source and destination directories.
45
45
  * Returns list of filenames that exist in both locations (excluding symlinks in dest).
46
46
  */
47
- export function detectConflicts(src, dest, prefix = '') {
47
+ function detectConflicts(src, dest, prefix = '') {
48
48
  const conflicts = [];
49
49
  if (!fs.existsSync(src) || !fs.existsSync(dest)) {
50
50
  return conflicts;
@@ -93,7 +93,7 @@ export function detectConflicts(src, dest, prefix = '') {
93
93
  /**
94
94
  * Prompt user for conflict resolution strategy.
95
95
  */
96
- export async function promptConflictStrategy(conflictInfos) {
96
+ async function promptConflictStrategy(conflictInfos) {
97
97
  const totalConflicts = conflictInfos.reduce((sum, info) => sum + info.conflicts.length, 0);
98
98
  if (totalConflicts === 0) {
99
99
  return null; // No conflicts, no prompt needed
@@ -171,8 +171,10 @@ export async function promptConflictStrategy(conflictInfos) {
171
171
  * and capability flag `memoryImports` → `rulesImports`.
172
172
  * v8 — versions moved from ~/.agents-system/versions to ~/.agents/versions
173
173
  * (two-repo split: system = shipped defaults, user = operational state).
174
+ * v9 — claude shim exports CLAUDE_CODE_OAUTH_TOKEN from per-version
175
+ * .oauth_token file on Linux (keychain-less sandbox fallback).
174
176
  */
175
- export const SHIM_SCHEMA_VERSION = 9;
177
+ export const SHIM_SCHEMA_VERSION = 10;
176
178
  /** Internal marker string used to embed the schema version in shim scripts. */
177
179
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
178
180
  /**
@@ -189,6 +191,12 @@ export function generateShimScript(agent) {
189
191
  # selected version's config directory so switching versions also switches the
190
192
  # live Claude account.
191
193
  export CLAUDE_CONFIG_DIR="$VERSION_DIR/home/${configDirName}"
194
+ # On Linux sandboxes (no keychain), fall back to a per-version token file.
195
+ # The env var always wins if already set; no-op on macOS.
196
+ if [ "\$(uname -s)" = "Linux" ] && [ -z "\${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -f "\$CLAUDE_CONFIG_DIR/.oauth_token" ]; then
197
+ CLAUDE_CODE_OAUTH_TOKEN=\$(cat "\$CLAUDE_CONFIG_DIR/.oauth_token")
198
+ export CLAUDE_CODE_OAUTH_TOKEN
199
+ fi
192
200
  `
193
201
  : agent === 'codex'
194
202
  ? `
@@ -543,7 +551,7 @@ function getVersionConfigPath(agent, version) {
543
551
  * Returns null if no migration is needed (already symlink or doesn't exist),
544
552
  * or ConflictInfo with the list of conflicting files.
545
553
  */
546
- export function detectMigrationConflicts(agent, version) {
554
+ function detectMigrationConflicts(agent, version) {
547
555
  const configPath = getAgentConfigPath(agent);
548
556
  const versionConfigPath = getVersionConfigPath(agent, version);
549
557
  try {
@@ -837,7 +845,7 @@ export function ensureClaudeInsideSymlink(version) {
837
845
  * Apply `ensureClaudeInsideSymlink` to every installed Claude version.
838
846
  * Safe to call repeatedly; per-version calls are idempotent.
839
847
  */
840
- export function ensureAllClaudeInsideSymlinks() {
848
+ function ensureAllClaudeInsideSymlinks() {
841
849
  const versionsDir = getVersionsDir();
842
850
  const claudeVersionsDir = path.join(versionsDir, 'claude');
843
851
  const migrated = [];
@@ -975,7 +983,7 @@ export function shimExists(agent) {
975
983
  * Read the schema version embedded in an existing on-disk shim. Returns
976
984
  * `null` if the shim doesn't exist or has no version marker (pre-v2 shim).
977
985
  */
978
- export function readShimSchemaVersion(agent) {
986
+ function readShimSchemaVersion(agent) {
979
987
  if (!shimExists(agent))
980
988
  return null;
981
989
  try {
@@ -996,7 +1004,7 @@ export function readShimSchemaVersion(agent) {
996
1004
  * False means either the shim is missing, is pre-v2 (no marker), or is an
997
1005
  * older version that needs regeneration.
998
1006
  */
999
- export function isShimCurrent(agent) {
1007
+ function isShimCurrent(agent) {
1000
1008
  const version = readShimSchemaVersion(agent);
1001
1009
  return version === SHIM_SCHEMA_VERSION;
1002
1010
  }
@@ -1309,7 +1317,7 @@ export function listAgentsWithInstalledVersions() {
1309
1317
  /**
1310
1318
  * Create shims for all installed agents.
1311
1319
  */
1312
- export function ensureAllShims() {
1320
+ function ensureAllShims() {
1313
1321
  const versionsDir = getVersionsDir();
1314
1322
  if (!fs.existsSync(versionsDir)) {
1315
1323
  return;
@@ -1331,7 +1339,7 @@ export function ensureAllShims() {
1331
1339
  * Compare resources between two versions.
1332
1340
  * Returns resources that exist in currentVersion but not in targetVersion.
1333
1341
  */
1334
- export function compareVersionResources(agent, currentVersion, targetVersion) {
1342
+ function compareVersionResources(agent, currentVersion, targetVersion) {
1335
1343
  const agentConfig = AGENTS[agent];
1336
1344
  const currentPath = getVersionConfigPath(agent, currentVersion);
1337
1345
  const targetPath = getVersionConfigPath(agent, targetVersion);
@@ -1420,7 +1428,7 @@ export function hasResourceDiff(diff) {
1420
1428
  * Copy resources from one version to another.
1421
1429
  * Only copies resources listed in the diff (i.e., ones missing in target).
1422
1430
  */
1423
- export function copyResourcesToVersion(agent, fromVersion, toVersion, diff) {
1431
+ function copyResourcesToVersion(agent, fromVersion, toVersion, diff) {
1424
1432
  const agentConfig = AGENTS[agent];
1425
1433
  const fromPath = getVersionConfigPath(agent, fromVersion);
1426
1434
  const toPath = getVersionConfigPath(agent, toVersion);
@@ -66,12 +66,18 @@ class Database {
66
66
  pragma(stmt) {
67
67
  this.inner.exec(`PRAGMA ${stmt}`);
68
68
  }
69
- // Wrap fn in BEGIN/COMMIT, ROLLBACK on throw. Manual on both runtimes
70
- // because node:sqlite has no `db.transaction(fn)` and the manual form is
71
- // identical in shape to what better-sqlite3 / bun:sqlite produce.
69
+ // Wrap fn in BEGIN IMMEDIATE/COMMIT, ROLLBACK on throw. Manual on both
70
+ // runtimes because node:sqlite has no `db.transaction(fn)`.
71
+ //
72
+ // BEGIN IMMEDIATE (not BEGIN DEFERRED) is required for write transactions in
73
+ // WAL mode. BEGIN DEFERRED upgrades the lock lazily on the first write; if
74
+ // another writer already committed since the transaction started, SQLite
75
+ // returns SQLITE_BUSY_SNAPSHOT (a sub-code of SQLITE_BUSY that the busy
76
+ // handler does NOT retry). BEGIN IMMEDIATE claims the write lock upfront so
77
+ // the busy handler fires correctly and respects busy_timeout.
72
78
  transaction(fn) {
73
79
  return (...args) => {
74
- this.inner.exec('BEGIN');
80
+ this.inner.exec('BEGIN IMMEDIATE');
75
81
  try {
76
82
  const result = fn(...args);
77
83
  this.inner.exec('COMMIT');
@@ -89,6 +89,8 @@ export declare function getUserRulesDir(): string;
89
89
  export declare function getUserMcpDir(): string;
90
90
  export declare function getUserPermissionsDir(): string;
91
91
  export declare function getUserSubagentsDir(): string;
92
+ export declare function getSystemWorkflowsDir(): string;
93
+ export declare function getUserWorkflowsDir(): string;
92
94
  export declare function getUserSecretsDir(): string;
93
95
  export declare function getUserPromptcutsPath(): string;
94
96
  /** Bucket root for durable runtime data (~/.agents/.history/). */
@@ -163,6 +165,7 @@ export declare function getTrashHooksDir(): string;
163
165
  export declare function getTrashPluginsDir(): string;
164
166
  /** Path to soft-deleted subagents (~/.agents/trash/subagents/). */
165
167
  export declare function getTrashSubagentsDir(): string;
168
+ export declare function getTrashWorkflowsDir(): string;
166
169
  /**
167
170
  * Path to a single user-level extra DotAgent repo clone (~/.agents-<alias>/).
168
171
  *
@@ -194,8 +197,18 @@ export declare function writeMeta(meta: Meta): void;
194
197
  export declare function updateMeta(updates: Partial<Meta>): Meta;
195
198
  /** Derive a filesystem-safe local clone path for a package source URL. */
196
199
  export declare function getPackageLocalPath(source: string): string;
197
- import type { AgentId, ResourceType, VersionResources } from './types.js';
198
- export declare function recordVersionResources(agent: AgentId, version: string, resourceType: ResourceType, resources: string[]): void;
200
+ import type { AgentId, ResourceType, VersionResources, ResourcePattern } from './types.js';
201
+ /**
202
+ * @deprecated No-op. Use ensureVersionResourcePatterns instead.
203
+ * Kept for backward compat with command files that still call it.
204
+ */
205
+ export declare function recordVersionResources(_agent: AgentId, _version: string, _resourceType: ResourceType, _resources: string[]): void;
206
+ /**
207
+ * Write default resource selection patterns for an agent@version.
208
+ * Only writes each field when it is not already set, preserving user customization.
209
+ * Pass all resource types you want to initialize in one call to batch the write.
210
+ */
211
+ export declare function ensureVersionResourcePatterns(agent: AgentId, version: string, updates: Partial<Record<Exclude<keyof VersionResources, 'rulesPreset'>, ResourcePattern[]>>): void;
199
212
  export declare function getVersionResources(agent: AgentId, version: string): VersionResources | null;
200
213
  export declare function clearVersionResources(agent: AgentId, version: string): void;
201
214
  /** Active rules preset for an agent@version. Defaults to "default" when unset. */
package/dist/lib/state.js CHANGED
@@ -23,7 +23,7 @@ import * as path from 'path';
23
23
  import * as os from 'os';
24
24
  import * as yaml from 'yaml';
25
25
  import { SEEDED_REGISTRIES } from './types.js';
26
- const HOME = os.homedir();
26
+ const HOME = process.env.HOME ?? os.homedir();
27
27
  // ─── Root directories ─────────────────────────────────────────────────────────
28
28
  /** System repo — npm-shipped, read-only from user commands. */
29
29
  const SYSTEM_AGENTS_DIR = path.join(HOME, '.agents-system');
@@ -41,6 +41,7 @@ const SYSTEM_RULES_DIR = path.join(SYSTEM_AGENTS_DIR, 'rules');
41
41
  const SYSTEM_MCP_DIR = path.join(SYSTEM_AGENTS_DIR, 'mcp');
42
42
  const SYSTEM_PERMISSIONS_DIR = path.join(SYSTEM_AGENTS_DIR, 'permissions');
43
43
  const SYSTEM_SUBAGENTS_DIR = path.join(SYSTEM_AGENTS_DIR, 'subagents');
44
+ const SYSTEM_WORKFLOWS_DIR = path.join(SYSTEM_AGENTS_DIR, 'workflows');
44
45
  const SYSTEM_PROMPTCUTS_FILE = path.join(SYSTEM_AGENTS_DIR, 'hooks', 'promptcuts.yaml');
45
46
  const SYSTEM_MCP_CONFIG_FILE = path.join(SYSTEM_AGENTS_DIR, 'mcp.json');
46
47
  const SYSTEM_INSTRUCTIONS_FILE = path.join(SYSTEM_AGENTS_DIR, 'instructions.md');
@@ -88,6 +89,7 @@ const USER_RULES_DIR = path.join(USER_AGENTS_DIR, 'rules');
88
89
  const USER_MCP_DIR = path.join(USER_AGENTS_DIR, 'mcp');
89
90
  const USER_PERMISSIONS_DIR = path.join(USER_AGENTS_DIR, 'permissions');
90
91
  const USER_SUBAGENTS_DIR = path.join(USER_AGENTS_DIR, 'subagents');
92
+ const USER_WORKFLOWS_DIR = path.join(USER_AGENTS_DIR, 'workflows');
91
93
  const USER_SECRETS_DIR = path.join(USER_AGENTS_DIR, 'secrets');
92
94
  const USER_PROMPTCUTS_FILE = path.join(USER_AGENTS_DIR, 'hooks', 'promptcuts.yaml');
93
95
  const META_HEADER = `# agents-cli metadata
@@ -238,6 +240,8 @@ export function getUserRulesDir() { return USER_RULES_DIR; }
238
240
  export function getUserMcpDir() { return USER_MCP_DIR; }
239
241
  export function getUserPermissionsDir() { return USER_PERMISSIONS_DIR; }
240
242
  export function getUserSubagentsDir() { return USER_SUBAGENTS_DIR; }
243
+ export function getSystemWorkflowsDir() { return SYSTEM_WORKFLOWS_DIR; }
244
+ export function getUserWorkflowsDir() { return USER_WORKFLOWS_DIR; }
241
245
  export function getUserSecretsDir() { return USER_SECRETS_DIR; }
242
246
  export function getUserPromptcutsPath() { return USER_PROMPTCUTS_FILE; }
243
247
  // ─── User operational path getters ────────────────────────────────────────────
@@ -316,6 +320,7 @@ export function getTrashHooksDir() { return path.join(TRASH_DIR, 'hooks'); }
316
320
  export function getTrashPluginsDir() { return path.join(TRASH_DIR, 'plugins'); }
317
321
  /** Path to soft-deleted subagents (~/.agents/trash/subagents/). */
318
322
  export function getTrashSubagentsDir() { return path.join(TRASH_DIR, 'subagents'); }
323
+ export function getTrashWorkflowsDir() { return path.join(TRASH_DIR, 'workflows'); }
319
324
  /**
320
325
  * Path to a single user-level extra DotAgent repo clone (~/.agents-<alias>/).
321
326
  *
@@ -552,9 +557,19 @@ export function getPackageLocalPath(source) {
552
557
  .replace(/\//g, '-');
553
558
  return path.join(PACKAGES_DIR, sanitized);
554
559
  }
555
- export function recordVersionResources(agent, version, resourceType, resources) {
556
- if (resources.length === 0)
557
- return;
560
+ /**
561
+ * @deprecated No-op. Use ensureVersionResourcePatterns instead.
562
+ * Kept for backward compat with command files that still call it.
563
+ */
564
+ export function recordVersionResources(_agent, _version, _resourceType, _resources) {
565
+ // intentional no-op — tracking moved to pattern-based ensureVersionResourcePatterns
566
+ }
567
+ /**
568
+ * Write default resource selection patterns for an agent@version.
569
+ * Only writes each field when it is not already set, preserving user customization.
570
+ * Pass all resource types you want to initialize in one call to batch the write.
571
+ */
572
+ export function ensureVersionResourcePatterns(agent, version, updates) {
558
573
  const meta = readMeta();
559
574
  if (!meta.versions)
560
575
  meta.versions = {};
@@ -562,10 +577,16 @@ export function recordVersionResources(agent, version, resourceType, resources)
562
577
  meta.versions[agent] = {};
563
578
  if (!meta.versions[agent][version])
564
579
  meta.versions[agent][version] = {};
565
- const existing = meta.versions[agent][version][resourceType] || [];
566
- const merged = [...new Set([...existing, ...resources])];
567
- meta.versions[agent][version][resourceType] = merged;
568
- writeMeta(meta);
580
+ const vr = meta.versions[agent][version];
581
+ let changed = false;
582
+ for (const [type, patterns] of Object.entries(updates)) {
583
+ if (!vr[type] || vr[type].length === 0) {
584
+ vr[type] = patterns;
585
+ changed = true;
586
+ }
587
+ }
588
+ if (changed)
589
+ writeMeta(meta);
569
590
  }
570
591
  export function getVersionResources(agent, version) {
571
592
  const meta = readMeta();
@@ -288,30 +288,49 @@ export interface ResolvedPackage {
288
288
  skillEntry?: SkillEntry;
289
289
  }
290
290
  /** Categories of resources that can be synced into an agent version home. */
291
- export type ResourceType = 'commands' | 'skills' | 'hooks' | 'memory' | 'mcp' | 'permissions' | 'subagents' | 'plugins';
292
- /** Map of resource names synced to a specific agent version, keyed by type. */
291
+ export type ResourceType = 'commands' | 'skills' | 'hooks' | 'memory' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'workflows';
292
+ /**
293
+ * A resource selection pattern stored in agents.yaml versions:
294
+ * "system:*" — all resources from ~/.agents-system/
295
+ * "user:*" — all resources from ~/.agents/
296
+ * "rush:*" — all resources from ~/.agents-rush/ (extra repo alias)
297
+ * "project:*" — all resources from .agents/ in the project root
298
+ * "user:foo" — specifically "foo" from ~/.agents/
299
+ * "!user:temp" — exclude "temp" from the user repo
300
+ */
301
+ export type ResourcePattern = string;
302
+ /** Sync specification for a specific agent@version, keyed by resource type. */
293
303
  export interface VersionResources {
294
- commands?: string[];
295
- skills?: string[];
296
- hooks?: string[];
297
- memory?: string[];
298
- mcp?: string[];
299
- permissions?: string[];
300
- subagents?: string[];
301
- plugins?: string[];
302
304
  /**
303
- * Active rule preset for this agent@version. The composer reads layered
304
- * `rules.yaml` files and emits this preset's subrules as the agent's
305
- * single instruction file. Absent/null means the literal "default" preset.
305
+ * Active rule preset. Absent/null means "default".
306
306
  */
307
307
  rulesPreset?: string;
308
+ skills?: ResourcePattern[];
309
+ commands?: ResourcePattern[];
310
+ hooks?: ResourcePattern[];
311
+ subagents?: ResourcePattern[];
312
+ plugins?: ResourcePattern[];
313
+ workflows?: ResourcePattern[];
314
+ permissions?: ResourcePattern[];
315
+ mcp?: ResourcePattern[];
316
+ }
317
+ /** A userConfig field declared in a plugin manifest. */
318
+ export interface PluginUserConfigField {
319
+ key: string;
320
+ description: string;
321
+ required?: boolean;
322
+ default?: string;
308
323
  }
309
- /** Manifest file (plugin.yaml) at the root of a plugin bundle. */
324
+ /** Manifest file (plugin.json) at the root of a plugin bundle. */
310
325
  export interface PluginManifest {
311
326
  name: string;
312
327
  description: string;
313
328
  version: string;
314
329
  agents?: AgentId[];
330
+ /** Interactive config fields prompted at install time. Values stored in .user-config.json. */
331
+ userConfig?: PluginUserConfigField[];
332
+ /** Other plugin names this plugin depends on. Missing deps produce a warning. */
333
+ dependencies?: string[];
315
334
  }
316
335
  /** A plugin found on disk with its parsed manifest and resource inventory. */
317
336
  export interface DiscoveredPlugin {
@@ -321,6 +340,16 @@ export interface DiscoveredPlugin {
321
340
  skills: string[];
322
341
  hooks: string[];
323
342
  scripts: string[];
343
+ /** Slash-command .md files in the plugin's commands/ directory (names without extension). */
344
+ commands: string[];
345
+ /** Subagent .md files in the plugin's agents/ directory (names without extension). */
346
+ agentDefs: string[];
347
+ /** Executable files in the plugin's bin/ directory. */
348
+ bin: string[];
349
+ /** Whether the plugin root contains a .mcp.json file. */
350
+ hasMcp: boolean;
351
+ /** Whether the plugin root contains a settings.json with non-permission keys to merge. */
352
+ hasSettings: boolean;
324
353
  }
325
354
  /** Frontmatter fields parsed from a subagent's agent.md file. */
326
355
  export interface SubagentFrontmatter {
@@ -15,6 +15,7 @@ export interface ResourceSelection {
15
15
  permissions?: string[] | 'all';
16
16
  subagents?: string[] | 'all';
17
17
  plugins?: string[] | 'all';
18
+ workflows?: string[] | 'all';
18
19
  }
19
20
  /**
20
21
  * Available resources in ~/.agents/ for syncing.
@@ -33,6 +34,7 @@ export interface AvailableResources {
33
34
  permissions: string[];
34
35
  subagents: string[];
35
36
  plugins: string[];
37
+ workflows: string[];
36
38
  promptcuts: boolean;
37
39
  }
38
40
  /**
@@ -215,6 +217,7 @@ export interface SyncResult {
215
217
  mcp: string[];
216
218
  subagents: string[];
217
219
  plugins: string[];
220
+ workflows: string[];
218
221
  }
219
222
  /** Diff between central ~/.agents/ resources and what is synced to a version home. */
220
223
  export interface ResourceDiff {