@phnx-labs/agents-cli 1.14.1 → 1.14.3

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 (102) hide show
  1. package/README.md +31 -3
  2. package/dist/commands/browser.d.ts +2 -0
  3. package/dist/commands/browser.js +388 -0
  4. package/dist/commands/daemon.js +1 -1
  5. package/dist/commands/doctor.d.ts +16 -9
  6. package/dist/commands/doctor.js +248 -12
  7. package/dist/commands/exec.js +17 -17
  8. package/dist/commands/prune.js +9 -3
  9. package/dist/commands/refresh-rules.d.ts +15 -0
  10. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  11. package/dist/commands/routines.js +1 -1
  12. package/dist/commands/rules.js +100 -4
  13. package/dist/commands/secrets.js +206 -12
  14. package/dist/commands/sync.js +19 -0
  15. package/dist/commands/teams.js +162 -22
  16. package/dist/commands/trash.d.ts +10 -0
  17. package/dist/commands/trash.js +187 -0
  18. package/dist/commands/view.js +46 -13
  19. package/dist/index.js +62 -4
  20. package/dist/lib/agents.js +2 -2
  21. package/dist/lib/browser/cdp.d.ts +24 -0
  22. package/dist/lib/browser/cdp.js +94 -0
  23. package/dist/lib/browser/chrome.d.ts +16 -0
  24. package/dist/lib/browser/chrome.js +157 -0
  25. package/dist/lib/browser/drivers/local.d.ts +8 -0
  26. package/dist/lib/browser/drivers/local.js +22 -0
  27. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  28. package/dist/lib/browser/drivers/ssh.js +129 -0
  29. package/dist/lib/browser/index.d.ts +5 -0
  30. package/dist/lib/browser/index.js +5 -0
  31. package/dist/lib/browser/input.d.ts +6 -0
  32. package/dist/lib/browser/input.js +52 -0
  33. package/dist/lib/browser/ipc.d.ts +12 -0
  34. package/dist/lib/browser/ipc.js +223 -0
  35. package/dist/lib/browser/profiles.d.ts +11 -0
  36. package/dist/lib/browser/profiles.js +61 -0
  37. package/dist/lib/browser/refs.d.ts +21 -0
  38. package/dist/lib/browser/refs.js +88 -0
  39. package/dist/lib/browser/service.d.ts +45 -0
  40. package/dist/lib/browser/service.js +404 -0
  41. package/dist/lib/browser/types.d.ts +73 -0
  42. package/dist/lib/browser/types.js +7 -0
  43. package/dist/lib/cloud/codex.js +1 -1
  44. package/dist/lib/cloud/registry.js +2 -2
  45. package/dist/lib/cloud/rush.js +2 -2
  46. package/dist/lib/cloud/store.js +2 -2
  47. package/dist/lib/daemon.d.ts +1 -1
  48. package/dist/lib/daemon.js +47 -11
  49. package/dist/lib/diff-text.d.ts +25 -0
  50. package/dist/lib/diff-text.js +47 -0
  51. package/dist/lib/doctor-diff.d.ts +64 -0
  52. package/dist/lib/doctor-diff.js +497 -0
  53. package/dist/lib/git.js +3 -3
  54. package/dist/lib/hooks.d.ts +6 -0
  55. package/dist/lib/hooks.js +6 -1
  56. package/dist/lib/migrate.js +77 -0
  57. package/dist/lib/pty-client.js +3 -3
  58. package/dist/lib/pty-server.js +36 -7
  59. package/dist/lib/resources.js +1 -1
  60. package/dist/lib/rotate.d.ts +43 -26
  61. package/dist/lib/rotate.js +99 -44
  62. package/dist/lib/rules/compile.d.ts +104 -0
  63. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  64. package/dist/lib/rules/compose.d.ts +78 -0
  65. package/dist/lib/rules/compose.js +170 -0
  66. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  67. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  68. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  69. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  70. package/dist/lib/secrets/bundles.d.ts +61 -4
  71. package/dist/lib/secrets/bundles.js +222 -54
  72. package/dist/lib/secrets/index.d.ts +24 -5
  73. package/dist/lib/secrets/index.js +70 -41
  74. package/dist/lib/session/active.js +5 -5
  75. package/dist/lib/session/db.js +4 -4
  76. package/dist/lib/session/discover.js +2 -2
  77. package/dist/lib/session/render.js +21 -7
  78. package/dist/lib/shims.d.ts +28 -4
  79. package/dist/lib/shims.js +72 -14
  80. package/dist/lib/state.d.ts +22 -28
  81. package/dist/lib/state.js +83 -76
  82. package/dist/lib/sync-manifest.d.ts +2 -2
  83. package/dist/lib/sync-manifest.js +5 -5
  84. package/dist/lib/teams/agents.d.ts +4 -2
  85. package/dist/lib/teams/agents.js +11 -4
  86. package/dist/lib/teams/api.d.ts +1 -1
  87. package/dist/lib/teams/api.js +2 -2
  88. package/dist/lib/teams/index.d.ts +1 -0
  89. package/dist/lib/teams/index.js +1 -0
  90. package/dist/lib/teams/persistence.js +3 -3
  91. package/dist/lib/teams/registry.d.ts +8 -1
  92. package/dist/lib/teams/registry.js +8 -2
  93. package/dist/lib/teams/worktree.d.ts +30 -0
  94. package/dist/lib/teams/worktree.js +96 -0
  95. package/dist/lib/types.d.ts +13 -7
  96. package/dist/lib/types.js +3 -3
  97. package/dist/lib/versions.d.ts +30 -2
  98. package/dist/lib/versions.js +127 -105
  99. package/package.json +1 -1
  100. package/scripts/postinstall.js +29 -0
  101. package/dist/commands/refresh-memory.d.ts +0 -15
  102. package/dist/lib/memory-compile.d.ts +0 -66
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Rules file compilation -- resolving @-imports into a single flat file.
3
3
  *
4
- * Agents that do not natively resolve `@path/to/file` imports (Codex, Gemini)
4
+ * Agents that do not natively resolve `@path/to/file` imports (Codex, Cursor)
5
5
  * need a pre-compiled rules file with all imports inlined. This module
6
- * handles that expansion.
6
+ * handles that expansion for both user-scope (writes into version home) and
7
+ * project-scope (writes into the workspace).
7
8
  */
8
9
  import * as fs from 'fs';
9
10
  import * as path from 'path';
10
11
  import * as os from 'os';
11
12
  import * as crypto from 'crypto';
12
- import { AGENTS } from './agents.js';
13
- import { getResolvedRulesDir, getVersionsDir } from './state.js';
13
+ import { AGENTS } from '../agents.js';
14
+ import { getResolvedRulesDir, getVersionsDir } from '../state.js';
15
+ import { composeRules, composeRulesFromState } from './compose.js';
14
16
  // Match `@path` preceded by start-of-string or whitespace. This avoids
15
17
  // matching emails ("foo@bar.com") and the middle of words. The leading
16
18
  // whitespace (if any) is captured so we can preserve it in the output.
@@ -18,6 +20,8 @@ const IMPORT_RE = /(^|\s)@(\S+)/g;
18
20
  const MAX_DEPTH = 5;
19
21
  const COMPILED_HEADER = '<!-- Auto-compiled by agents-cli from ~/.agents/rules/AGENTS.md + imports.\n' +
20
22
  ' Edit the source files under ~/.agents/rules/ — edits to this file will be overwritten on next sync. -->\n\n';
23
+ const COMPILED_HEADER_PROJECT = '<!-- Auto-compiled by agents-cli from .agents/rules/AGENTS.md + imports.\n' +
24
+ ' Edit the source files under .agents/rules/ — edits to this file will be overwritten on next sync. -->\n\n';
21
25
  function expandTilde(p) {
22
26
  if (p === '~')
23
27
  return os.homedir();
@@ -87,10 +91,10 @@ export function resolveImports(content, baseDir) {
87
91
  return { content: result, sources };
88
92
  }
89
93
  /** True if the agent's native runtime resolves `@path` imports in its rules file. */
90
- export function supportsMemoryImports(agentId) {
91
- return !!AGENTS[agentId].capabilities.memoryImports;
94
+ export function supportsRulesImports(agentId) {
95
+ return !!AGENTS[agentId].capabilities.rulesImports;
92
96
  }
93
- function getCompiledMemoryPath(agentId, version) {
97
+ function getCompiledRulesPath(agentId, version) {
94
98
  const agentConfig = AGENTS[agentId];
95
99
  const versionHome = path.join(getVersionsDir(), agentId, version, 'home');
96
100
  return path.join(versionHome, `.${agentId}`, agentConfig.instructionsFile);
@@ -107,10 +111,10 @@ function getManifestPath(compiledPath) {
107
111
  * For agents that support @-imports natively, always returns false — there's
108
112
  * nothing to compile.
109
113
  */
110
- export function isMemoryStale(agentId, version) {
111
- if (supportsMemoryImports(agentId))
114
+ export function isRulesStale(agentId, version) {
115
+ if (supportsRulesImports(agentId))
112
116
  return false;
113
- const compiledPath = getCompiledMemoryPath(agentId, version);
117
+ const compiledPath = getCompiledRulesPath(agentId, version);
114
118
  const manifestPath = getManifestPath(compiledPath);
115
119
  if (!fs.existsSync(compiledPath) || !fs.existsSync(manifestPath))
116
120
  return true;
@@ -143,19 +147,19 @@ export function isMemoryStale(agentId, version) {
143
147
  * Agents that natively resolve @-imports are skipped (no-op) — their sync
144
148
  * uses the standard copyFileSync path in `syncResourcesToVersion`.
145
149
  */
146
- export function compileMemoryForAgent(agentId, version) {
147
- if (supportsMemoryImports(agentId)) {
150
+ export function compileRulesForAgent(agentId, version) {
151
+ if (supportsRulesImports(agentId)) {
148
152
  return { compiled: false, compiledPath: '', sources: 0 };
149
153
  }
150
- const memoryDir = getResolvedRulesDir();
151
- const sourceAgents = path.join(memoryDir, 'AGENTS.md');
154
+ const rulesDir = getResolvedRulesDir();
155
+ const sourceAgents = path.join(rulesDir, 'AGENTS.md');
152
156
  if (!fs.existsSync(sourceAgents)) {
153
157
  return { compiled: false, compiledPath: '', sources: 0 };
154
158
  }
155
159
  const rootContent = fs.readFileSync(sourceAgents, 'utf8');
156
- const { content, sources } = resolveImports(rootContent, memoryDir);
160
+ const { content, sources } = resolveImports(rootContent, rulesDir);
157
161
  const newContent = COMPILED_HEADER + content;
158
- const compiledPath = getCompiledMemoryPath(agentId, version);
162
+ const compiledPath = getCompiledRulesPath(agentId, version);
159
163
  fs.mkdirSync(path.dirname(compiledPath), { recursive: true });
160
164
  const existing = fs.existsSync(compiledPath) ? fs.readFileSync(compiledPath, 'utf8') : null;
161
165
  if (existing === newContent) {
@@ -175,15 +179,150 @@ export function compileMemoryForAgent(agentId, version) {
175
179
  return { compiled: true, compiledPath, sources: allSources.length };
176
180
  }
177
181
  /**
178
- * Recompile memory if stale. Safe to call on every agent invocation — the
182
+ * Recompile rules if stale. Safe to call on every agent invocation — the
179
183
  * staleness check is fast (sha256 of 8-10 small files, ~10-20ms). Returns
180
184
  * true if a recompile happened, false otherwise.
181
185
  */
182
- export function ensureMemoryFresh(agentId, version) {
183
- if (supportsMemoryImports(agentId))
186
+ export function ensureRulesFresh(agentId, version) {
187
+ if (supportsRulesImports(agentId))
184
188
  return false;
185
- if (!isMemoryStale(agentId, version))
189
+ if (!isRulesStale(agentId, version))
186
190
  return false;
187
- const result = compileMemoryForAgent(agentId, version);
191
+ const result = compileRulesForAgent(agentId, version);
188
192
  return result.compiled;
189
193
  }
194
+ /**
195
+ * Compile project-scope rules into a workspace's root memory files so each
196
+ * agent's native loader picks them up.
197
+ *
198
+ * Composes rules from all available layers (project > user > extras > system)
199
+ * with project highest priority — so a project's `subrules/` and `rules.yaml`
200
+ * shadow user/system fragments and presets. Writes `cwd/AGENTS.md` with
201
+ * COMPILED_HEADER_PROJECT and creates symlinks (CLAUDE.md, GEMINI.md,
202
+ * .cursorrules, etc.) → AGENTS.md so every agent finds its expected file at
203
+ * cwd. The agent's own loader merges this project-level file with its
204
+ * user-level rules (in version home) at runtime.
205
+ *
206
+ * Don't-clobber guard: if `cwd/AGENTS.md` exists without our header, the user
207
+ * authored it — leave it alone and report via `skippedClobber`. Same for any
208
+ * pre-existing per-agent file or symlink that doesn't already point at
209
+ * AGENTS.md.
210
+ *
211
+ * No-op when `cwd/.agents/rules/` does not exist. Idempotent on repeated
212
+ * calls — content equality short-circuits the write.
213
+ */
214
+ export function compileRulesForProject(cwd, opts = {}) {
215
+ const projectRulesDir = path.join(cwd, '.agents', 'rules');
216
+ const empty = {
217
+ compiled: false, agentsPath: '', symlinks: [], sources: 0, skippedClobber: [],
218
+ };
219
+ if (!fs.existsSync(projectRulesDir))
220
+ return empty;
221
+ let composed;
222
+ try {
223
+ // Tests inject `layers` to isolate from real ~/.agents-system / ~/.agents
224
+ // state. Production callers omit it and compose from discovered state.
225
+ const result = opts.layers
226
+ ? composeRules({ preset: opts.preset, layers: opts.layers })
227
+ : composeRulesFromState({ cwd, preset: opts.preset });
228
+ composed = { content: result.content, subrules: result.subrules };
229
+ }
230
+ catch {
231
+ // Composer threw (no preset, malformed yaml). Don't write a half-baked
232
+ // file — bail out cleanly, same as if the rules dir didn't exist.
233
+ return empty;
234
+ }
235
+ const newContent = COMPILED_HEADER_PROJECT + composed.content;
236
+ const agentsPath = path.join(cwd, 'AGENTS.md');
237
+ const skippedClobber = [];
238
+ let compiled = false;
239
+ let weOwnAgentsMd = false;
240
+ let agentsLstat = null;
241
+ try {
242
+ agentsLstat = fs.lstatSync(agentsPath);
243
+ }
244
+ catch { /* missing */ }
245
+ if (!agentsLstat) {
246
+ fs.writeFileSync(agentsPath, newContent);
247
+ compiled = true;
248
+ weOwnAgentsMd = true;
249
+ }
250
+ else if (agentsLstat.isFile()) {
251
+ let existing = '';
252
+ try {
253
+ existing = fs.readFileSync(agentsPath, 'utf8');
254
+ }
255
+ catch { /* unreadable */ }
256
+ if (existing.startsWith(COMPILED_HEADER_PROJECT)) {
257
+ if (existing !== newContent) {
258
+ fs.writeFileSync(agentsPath, newContent);
259
+ compiled = true;
260
+ }
261
+ weOwnAgentsMd = true;
262
+ }
263
+ else {
264
+ skippedClobber.push('AGENTS.md');
265
+ }
266
+ }
267
+ else {
268
+ // Symlink or other non-regular file — treat as user-owned, do not clobber
269
+ skippedClobber.push('AGENTS.md');
270
+ }
271
+ // Per-agent symlinks. Only attempt when we own AGENTS.md — never create a
272
+ // dangling symlink to a file we couldn't write.
273
+ const symlinks = [];
274
+ if (weOwnAgentsMd) {
275
+ const seen = new Set(['AGENTS.md']);
276
+ for (const agent of Object.values(AGENTS)) {
277
+ const fname = agent.instructionsFile;
278
+ if (seen.has(fname))
279
+ continue;
280
+ // Skip agents whose instructions live at a nested path (e.g. OpenClaw's
281
+ // workspace/AGENTS.md) — those are managed by their own setup paths.
282
+ if (fname.includes('/') || fname.includes('\\'))
283
+ continue;
284
+ seen.add(fname);
285
+ const linkPath = path.join(cwd, fname);
286
+ let lstat = null;
287
+ try {
288
+ lstat = fs.lstatSync(linkPath);
289
+ }
290
+ catch { /* missing */ }
291
+ if (lstat) {
292
+ if (lstat.isSymbolicLink()) {
293
+ let target = '';
294
+ try {
295
+ target = fs.readlinkSync(linkPath);
296
+ }
297
+ catch { /* unreadable */ }
298
+ if (target === 'AGENTS.md') {
299
+ symlinks.push(fname);
300
+ continue;
301
+ }
302
+ skippedClobber.push(fname);
303
+ continue;
304
+ }
305
+ // Regular file — user authored
306
+ skippedClobber.push(fname);
307
+ continue;
308
+ }
309
+ try {
310
+ fs.symlinkSync('AGENTS.md', linkPath);
311
+ symlinks.push(fname);
312
+ }
313
+ catch {
314
+ // Filesystems that disallow symlinks (some Windows configs) — fall
315
+ // back to a copy. The agent reads the same content either way.
316
+ try {
317
+ fs.copyFileSync(agentsPath, linkPath);
318
+ symlinks.push(fname);
319
+ }
320
+ catch {
321
+ // Give up on this one quietly; the agent that needs this filename
322
+ // will fall back to its own discovery rules.
323
+ }
324
+ }
325
+ }
326
+ }
327
+ return { compiled, agentsPath, symlinks, sources: composed.subrules.length, skippedClobber };
328
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Rules composition — assemble a fully-inlined rules document from layered
3
+ * `subrules/` fragments and `rules.yaml` preset definitions.
4
+ *
5
+ * The model:
6
+ * - Every DotAgents repo holds `<repo>/rules/subrules/*.md` (rule fragments)
7
+ * and `<repo>/rules/rules.yaml` (preset definitions).
8
+ * - Layers are read in precedence order (highest first):
9
+ * project > user > extra > system.
10
+ * - The active preset's `subrules:` list is resolved against the layer set
11
+ * using per-name shadowing — a project subrule shadows a user/system one
12
+ * of the same name.
13
+ * - Subrules in the user / extra / project layers that the preset did NOT
14
+ * name are auto-appended in precedence order. (System auto-append is
15
+ * opt-in only: system never auto-appends to avoid noise.)
16
+ * - Output is a single concatenated string with no `@-import` syntax.
17
+ *
18
+ * No filesystem writes happen here — callers (`syncResourcesToVersion`,
19
+ * project-rules compile) decide where to land the composed output.
20
+ */
21
+ export type LayerScope = 'project' | 'user' | 'extra' | 'system';
22
+ export interface RulesLayer {
23
+ scope: LayerScope;
24
+ rulesDir: string;
25
+ /** Set when scope is 'extra'; undefined otherwise. */
26
+ alias?: string;
27
+ }
28
+ export interface PresetDef {
29
+ /** Subrule names (without `.md`), in concatenation order. */
30
+ subrules: string[];
31
+ }
32
+ export interface RulesYaml {
33
+ presets?: Record<string, PresetDef>;
34
+ }
35
+ export interface ComposeOptions {
36
+ /** Defaults to `"default"`. */
37
+ preset?: string;
38
+ /** Layers in precedence order, highest first. */
39
+ layers: RulesLayer[];
40
+ }
41
+ export interface ComposedSubrule {
42
+ name: string;
43
+ sourcePath: string;
44
+ layerScope: LayerScope;
45
+ layerAlias?: string;
46
+ }
47
+ export interface ComposeResult {
48
+ /** Fully concatenated, no @-imports. */
49
+ content: string;
50
+ /** The preset name that was applied. */
51
+ preset: string;
52
+ /** The layer that defined the preset. */
53
+ presetLayer: LayerScope;
54
+ /** Subrules included, in concatenation order. */
55
+ subrules: ComposedSubrule[];
56
+ }
57
+ /**
58
+ * Compose a rules document from the given layers.
59
+ *
60
+ * Throws when the requested preset isn't defined in any layer's rules.yaml —
61
+ * means the caller passed a typo or no layer ships the named preset.
62
+ */
63
+ export declare function composeRules(opts: ComposeOptions): ComposeResult;
64
+ /**
65
+ * Discover layers for use at sync time (no cwd) or runtime (with cwd).
66
+ *
67
+ * Project layer is included only when cwd is given AND `<cwd>/.agents/rules/`
68
+ * exists. Without cwd, only user / extras / system are surfaced — matching
69
+ * the home-file write at sync time.
70
+ */
71
+ export declare function discoverRulesLayers(opts?: {
72
+ cwd?: string;
73
+ }): RulesLayer[];
74
+ /** Convenience wrapper — discovers layers from state, then composes. */
75
+ export declare function composeRulesFromState(opts?: {
76
+ preset?: string;
77
+ cwd?: string;
78
+ }): ComposeResult;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Rules composition — assemble a fully-inlined rules document from layered
3
+ * `subrules/` fragments and `rules.yaml` preset definitions.
4
+ *
5
+ * The model:
6
+ * - Every DotAgents repo holds `<repo>/rules/subrules/*.md` (rule fragments)
7
+ * and `<repo>/rules/rules.yaml` (preset definitions).
8
+ * - Layers are read in precedence order (highest first):
9
+ * project > user > extra > system.
10
+ * - The active preset's `subrules:` list is resolved against the layer set
11
+ * using per-name shadowing — a project subrule shadows a user/system one
12
+ * of the same name.
13
+ * - Subrules in the user / extra / project layers that the preset did NOT
14
+ * name are auto-appended in precedence order. (System auto-append is
15
+ * opt-in only: system never auto-appends to avoid noise.)
16
+ * - Output is a single concatenated string with no `@-import` syntax.
17
+ *
18
+ * No filesystem writes happen here — callers (`syncResourcesToVersion`,
19
+ * project-rules compile) decide where to land the composed output.
20
+ */
21
+ import * as fs from 'fs';
22
+ import * as path from 'path';
23
+ import * as yaml from 'yaml';
24
+ import { getResolvedRulesDir, getUserRulesDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
25
+ const SUBRULES_DIR_NAME = 'subrules';
26
+ const RULES_YAML_NAME = 'rules.yaml';
27
+ const DEFAULT_PRESET = 'default';
28
+ const SUBRULES_README = 'README.md';
29
+ function readRulesYaml(rulesDir) {
30
+ const p = path.join(rulesDir, RULES_YAML_NAME);
31
+ if (!fs.existsSync(p))
32
+ return null;
33
+ try {
34
+ const parsed = yaml.parse(fs.readFileSync(p, 'utf-8'));
35
+ return parsed || {};
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function resolvePreset(layers, preset) {
42
+ for (const layer of layers) {
43
+ const yml = readRulesYaml(layer.rulesDir);
44
+ if (!yml?.presets)
45
+ continue;
46
+ const def = yml.presets[preset];
47
+ if (def)
48
+ return { def, layer };
49
+ }
50
+ return null;
51
+ }
52
+ function findSubrule(layers, name) {
53
+ for (const layer of layers) {
54
+ const p = path.join(layer.rulesDir, SUBRULES_DIR_NAME, `${name}.md`);
55
+ if (fs.existsSync(p))
56
+ return { sourcePath: p, layer };
57
+ }
58
+ return null;
59
+ }
60
+ function listLayerSubruleNames(layer) {
61
+ const dir = path.join(layer.rulesDir, SUBRULES_DIR_NAME);
62
+ if (!fs.existsSync(dir))
63
+ return [];
64
+ try {
65
+ return fs
66
+ .readdirSync(dir)
67
+ .filter((f) => f.endsWith('.md') && f !== SUBRULES_README)
68
+ .map((f) => f.slice(0, -3))
69
+ .sort();
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ /**
76
+ * Compose a rules document from the given layers.
77
+ *
78
+ * Throws when the requested preset isn't defined in any layer's rules.yaml —
79
+ * means the caller passed a typo or no layer ships the named preset.
80
+ */
81
+ export function composeRules(opts) {
82
+ const presetName = opts.preset || DEFAULT_PRESET;
83
+ const presetMatch = resolvePreset(opts.layers, presetName);
84
+ if (!presetMatch) {
85
+ throw new Error(`Preset "${presetName}" not found in any rules.yaml across the active layers.`);
86
+ }
87
+ const composed = [];
88
+ const seen = new Set();
89
+ // 1. Preset's named subrules, resolved by per-name shadowing.
90
+ for (const name of presetMatch.def.subrules || []) {
91
+ if (seen.has(name))
92
+ continue;
93
+ const found = findSubrule(opts.layers, name);
94
+ if (!found)
95
+ continue; // missing subrule: skip silently — same as @-import miss
96
+ composed.push({
97
+ name,
98
+ sourcePath: found.sourcePath,
99
+ layerScope: found.layer.scope,
100
+ layerAlias: found.layer.alias,
101
+ });
102
+ seen.add(name);
103
+ }
104
+ // 2. Auto-append: any subrule in a non-system layer not yet included.
105
+ // Honors precedence — project layer's auto-appends come first.
106
+ for (const layer of opts.layers) {
107
+ if (layer.scope === 'system')
108
+ continue;
109
+ for (const name of listLayerSubruleNames(layer)) {
110
+ if (seen.has(name))
111
+ continue;
112
+ composed.push({
113
+ name,
114
+ sourcePath: path.join(layer.rulesDir, SUBRULES_DIR_NAME, `${name}.md`),
115
+ layerScope: layer.scope,
116
+ layerAlias: layer.alias,
117
+ });
118
+ seen.add(name);
119
+ }
120
+ }
121
+ // 3. Concatenate. Trim trailing whitespace on each fragment so spacing is
122
+ // predictable — fragments often end in a newline already.
123
+ const parts = composed.map((c) => fs.readFileSync(c.sourcePath, 'utf-8').replace(/\s+$/, ''));
124
+ const content = parts.length === 0 ? '' : parts.join('\n\n') + '\n';
125
+ return {
126
+ content,
127
+ preset: presetName,
128
+ presetLayer: presetMatch.layer.scope,
129
+ subrules: composed,
130
+ };
131
+ }
132
+ /**
133
+ * Discover layers for use at sync time (no cwd) or runtime (with cwd).
134
+ *
135
+ * Project layer is included only when cwd is given AND `<cwd>/.agents/rules/`
136
+ * exists. Without cwd, only user / extras / system are surfaced — matching
137
+ * the home-file write at sync time.
138
+ */
139
+ export function discoverRulesLayers(opts = {}) {
140
+ const layers = [];
141
+ if (opts.cwd) {
142
+ const projectAgentsDir = getProjectAgentsDir(opts.cwd);
143
+ if (projectAgentsDir) {
144
+ const rulesDir = path.join(projectAgentsDir, 'rules');
145
+ if (fs.existsSync(rulesDir)) {
146
+ layers.push({ scope: 'project', rulesDir });
147
+ }
148
+ }
149
+ }
150
+ const userRulesDir = getUserRulesDir();
151
+ if (fs.existsSync(userRulesDir)) {
152
+ layers.push({ scope: 'user', rulesDir: userRulesDir });
153
+ }
154
+ for (const extra of getEnabledExtraRepos()) {
155
+ const rulesDir = path.join(extra.dir, 'rules');
156
+ if (fs.existsSync(rulesDir)) {
157
+ layers.push({ scope: 'extra', rulesDir, alias: extra.alias });
158
+ }
159
+ }
160
+ const systemRulesDir = getResolvedRulesDir();
161
+ if (fs.existsSync(systemRulesDir)) {
162
+ layers.push({ scope: 'system', rulesDir: systemRulesDir });
163
+ }
164
+ return layers;
165
+ }
166
+ /** Convenience wrapper — discovers layers from state, then composes. */
167
+ export function composeRulesFromState(opts = {}) {
168
+ const layers = discoverRulesLayers({ cwd: opts.cwd });
169
+ return composeRules({ preset: opts.preset, layers });
170
+ }
@@ -6,7 +6,7 @@
6
6
  * GEMINI.md, etc.). This module handles reading, managing includes, and
7
7
  * refreshing rules files across version homes.
8
8
  */
9
- import type { AgentId } from './types.js';
9
+ import type { AgentId } from '../types.js';
10
10
  export type InstructionsScope = 'user' | 'project';
11
11
  export interface InstalledInstructions {
12
12
  agentId: AgentId;
@@ -23,7 +23,7 @@ export interface DiscoveredInstructions {
23
23
  * Central rules filename constant.
24
24
  * All agents map to this file in ~/.agents/rules/, renamed per-agent when synced.
25
25
  */
26
- export declare const CENTRAL_MEMORY_FILENAME = "AGENTS.md";
26
+ export declare const CENTRAL_RULES_FILENAME = "AGENTS.md";
27
27
  /**
28
28
  * Get the canonical central rules filename for an agent's instructionsFile.
29
29
  * Central storage uses AGENTS.md, which gets renamed per-agent when syncing:
@@ -32,12 +32,12 @@ export declare const CENTRAL_MEMORY_FILENAME = "AGENTS.md";
32
32
  * - Cursor: AGENTS.md → .cursorrules
33
33
  * - Codex/OpenCode: AGENTS.md → AGENTS.md (no rename)
34
34
  */
35
- export declare function getCentralMemoryFileName(agentId: AgentId): string;
35
+ export declare function getCentralRulesFileName(agentId: AgentId): string;
36
36
  export declare function getInstructionsPath(agentId: AgentId, scope: InstructionsScope, cwd?: string): string;
37
37
  export declare function instructionsExists(agentId: AgentId, scope?: InstructionsScope, cwd?: string): boolean;
38
38
  export declare function discoverInstructionsFromRepo(repoPath: string): DiscoveredInstructions[];
39
39
  export declare function resolveInstructionsSource(repoPath: string, agentId: AgentId): string | null;
40
- export declare function discoverMemoryFilesFromRepo(repoPath: string): string[];
40
+ export declare function discoverRuleFilesFromRepo(repoPath: string): string[];
41
41
  export declare function installInstructions(sourcePath: string, agentId: AgentId, method?: 'symlink' | 'copy'): {
42
42
  path: string;
43
43
  method: 'symlink' | 'copy';
@@ -60,4 +60,4 @@ export declare function installInstructionsCentrally(repoPath: string, filesToIn
60
60
  /**
61
61
  * List top-level rules files from user and system dirs (user wins on collision).
62
62
  */
63
- export declare function listCentralMemory(): string[];
63
+ export declare function listCentralRules(): string[];
@@ -8,14 +8,14 @@
8
8
  */
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
- import { AGENTS, ALL_AGENT_IDS } from './agents.js';
12
- import { getResolvedRulesDir, getUserRulesDir, getProjectAgentsDir } from './state.js';
13
- import { getEffectiveHome } from './versions.js';
11
+ import { AGENTS, ALL_AGENT_IDS } from '../agents.js';
12
+ import { getResolvedRulesDir, getUserRulesDir, getProjectAgentsDir } from '../state.js';
13
+ import { getEffectiveHome } from '../versions.js';
14
14
  /**
15
15
  * Central rules filename constant.
16
16
  * All agents map to this file in ~/.agents/rules/, renamed per-agent when synced.
17
17
  */
18
- export const CENTRAL_MEMORY_FILENAME = 'AGENTS.md';
18
+ export const CENTRAL_RULES_FILENAME = 'AGENTS.md';
19
19
  const RULES_DOC_FILENAME = 'README.md';
20
20
  function isSyncableRuleMarkdown(filename) {
21
21
  return filename.endsWith('.md') && filename !== RULES_DOC_FILENAME;
@@ -46,14 +46,14 @@ function listRuleMarkdownFiles(rulesDir) {
46
46
  * - Cursor: AGENTS.md → .cursorrules
47
47
  * - Codex/OpenCode: AGENTS.md → AGENTS.md (no rename)
48
48
  */
49
- export function getCentralMemoryFileName(agentId) {
49
+ export function getCentralRulesFileName(agentId) {
50
50
  const agent = AGENTS[agentId];
51
51
  const instrFile = agent.instructionsFile;
52
52
  // If it contains a path separator, extract just the filename
53
53
  const filename = instrFile.includes('/') ? path.basename(instrFile) : instrFile;
54
54
  // If the agent's instructionsFile isn't AGENTS.md, it was renamed FROM AGENTS.md
55
- if (filename !== CENTRAL_MEMORY_FILENAME) {
56
- return CENTRAL_MEMORY_FILENAME;
55
+ if (filename !== CENTRAL_RULES_FILENAME) {
56
+ return CENTRAL_RULES_FILENAME;
57
57
  }
58
58
  return filename;
59
59
  }
@@ -75,7 +75,7 @@ export function getInstructionsPath(agentId, scope, cwd = process.cwd()) {
75
75
  const projectAgentsDir = getProjectAgentsDir(cwd);
76
76
  if (projectAgentsDir) {
77
77
  const projectRulesDir = path.join(projectAgentsDir, 'rules');
78
- const centralName = getCentralMemoryFileName(agentId);
78
+ const centralName = getCentralRulesFileName(agentId);
79
79
  const candidates = [
80
80
  path.join(projectRulesDir, centralName),
81
81
  path.join(projectRulesDir, agent.instructionsFile),
@@ -142,7 +142,7 @@ export function resolveInstructionsSource(repoPath, agentId) {
142
142
  }
143
143
  return null;
144
144
  }
145
- export function discoverMemoryFilesFromRepo(repoPath) {
145
+ export function discoverRuleFilesFromRepo(repoPath) {
146
146
  const rulesDir = path.join(repoPath, 'rules');
147
147
  if (!fs.existsSync(rulesDir)) {
148
148
  return [];
@@ -283,7 +283,7 @@ export function installInstructionsCentrally(repoPath, filesToInstall) {
283
283
  /**
284
284
  * List top-level rules files from user and system dirs (user wins on collision).
285
285
  */
286
- export function listCentralMemory() {
286
+ export function listCentralRules() {
287
287
  const seen = new Set();
288
288
  for (const dir of [getUserRulesDir(), getResolvedRulesDir()]) {
289
289
  if (!fs.existsSync(dir))