@phnx-labs/agents-cli 1.14.2 → 1.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -7
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +7 -0
- package/dist/commands/browser.d.ts +3 -0
- package/dist/commands/browser.js +392 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +198 -11
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +184 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +47 -14
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +123 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources/commands.d.ts +46 -0
- package/dist/lib/resources/commands.js +208 -0
- package/dist/lib/resources/hooks.d.ts +12 -0
- package/dist/lib/resources/hooks.js +136 -0
- package/dist/lib/resources/index.d.ts +36 -0
- package/dist/lib/resources/index.js +69 -0
- package/dist/lib/resources/mcp.d.ts +34 -0
- package/dist/lib/resources/mcp.js +483 -0
- package/dist/lib/resources/permissions.d.ts +13 -0
- package/dist/lib/resources/permissions.js +184 -0
- package/dist/lib/resources/rules.d.ts +43 -0
- package/dist/lib/resources/rules.js +146 -0
- package/dist/lib/resources/skills.d.ts +37 -0
- package/dist/lib/resources/skills.js +238 -0
- package/dist/lib/resources/subagents.d.ts +46 -0
- package/dist/lib/resources/subagents.js +198 -0
- package/dist/lib/resources/types.d.ts +82 -0
- package/dist/lib/resources/types.js +8 -0
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +8 -1
- package/dist/lib/rotate.js +17 -4
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -78
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +12 -1
- package/dist/lib/teams/registry.js +12 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +12 -6
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +32 -3
- package/dist/lib/versions.js +147 -119
- package/package.json +3 -2
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- 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,
|
|
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 '
|
|
13
|
-
import { getResolvedRulesDir, getVersionsDir } from '
|
|
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
|
|
91
|
-
return !!AGENTS[agentId].capabilities.
|
|
94
|
+
export function supportsRulesImports(agentId) {
|
|
95
|
+
return !!AGENTS[agentId].capabilities.rulesImports;
|
|
92
96
|
}
|
|
93
|
-
function
|
|
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
|
|
111
|
-
if (
|
|
114
|
+
export function isRulesStale(agentId, version) {
|
|
115
|
+
if (supportsRulesImports(agentId))
|
|
112
116
|
return false;
|
|
113
|
-
const compiledPath =
|
|
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
|
|
147
|
-
if (
|
|
150
|
+
export function compileRulesForAgent(agentId, version) {
|
|
151
|
+
if (supportsRulesImports(agentId)) {
|
|
148
152
|
return { compiled: false, compiledPath: '', sources: 0 };
|
|
149
153
|
}
|
|
150
|
-
const
|
|
151
|
-
const sourceAgents = path.join(
|
|
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,
|
|
160
|
+
const { content, sources } = resolveImports(rootContent, rulesDir);
|
|
157
161
|
const newContent = COMPILED_HEADER + content;
|
|
158
|
-
const compiledPath =
|
|
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
|
|
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
|
|
183
|
-
if (
|
|
186
|
+
export function ensureRulesFresh(agentId, version) {
|
|
187
|
+
if (supportsRulesImports(agentId))
|
|
184
188
|
return false;
|
|
185
|
-
if (!
|
|
189
|
+
if (!isRulesStale(agentId, version))
|
|
186
190
|
return false;
|
|
187
|
-
const result =
|
|
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 '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
12
|
-
import { getResolvedRulesDir, getUserRulesDir, getProjectAgentsDir } from '
|
|
13
|
-
import { getEffectiveHome } from '
|
|
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
|
|
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
|
|
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 !==
|
|
56
|
-
return
|
|
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 =
|
|
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
|
|
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
|
|
286
|
+
export function listCentralRules() {
|
|
287
287
|
const seen = new Set();
|
|
288
288
|
for (const dir of [getUserRulesDir(), getResolvedRulesDir()]) {
|
|
289
289
|
if (!fs.existsSync(dir))
|
|
Binary file
|
|
Binary file
|