@phnx-labs/agents-cli 1.14.2 → 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.
- package/README.md +17 -7
- package/dist/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +388 -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 +162 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +46 -13
- 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 +77 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- 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 -76
- 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 +8 -1
- package/dist/lib/teams/registry.js +8 -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 +30 -2
- package/dist/lib/versions.js +127 -105
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- package/dist/lib/memory-compile.d.ts +0 -66
package/dist/lib/migrate.js
CHANGED
|
@@ -76,10 +76,87 @@ function migratePromptcutsIntoHooks() {
|
|
|
76
76
|
catch { /* best-effort */ }
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Move installed agent versions from the legacy single-root layout
|
|
81
|
+
* (~/.agents/versions/<agent>/<ver>/) into the system root
|
|
82
|
+
* (~/.agents-system/versions/<agent>/<ver>/).
|
|
83
|
+
*
|
|
84
|
+
* Pre-split installs put binaries and home dirs under ~/.agents/. After the
|
|
85
|
+
* split, the system code only scans ~/.agents-system/versions/, so without
|
|
86
|
+
* this migration the versions become invisible to listInstalledVersions and
|
|
87
|
+
* every command that depends on it (view, prune, run).
|
|
88
|
+
*
|
|
89
|
+
* Idempotent and non-destructive: if a same-named dest already exists we
|
|
90
|
+
* leave the legacy copy in place so the user can reconcile manually.
|
|
91
|
+
*/
|
|
92
|
+
function migrateUserVersionsToSystem() {
|
|
93
|
+
const userVersions = path.join(USER_DIR, 'versions');
|
|
94
|
+
const sysVersions = path.join(SYSTEM_DIR, 'versions');
|
|
95
|
+
if (!fs.existsSync(userVersions))
|
|
96
|
+
return;
|
|
97
|
+
let movedCount = 0;
|
|
98
|
+
let skippedCount = 0;
|
|
99
|
+
let agentEntries;
|
|
100
|
+
try {
|
|
101
|
+
agentEntries = fs.readdirSync(userVersions, { withFileTypes: true });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const agent of agentEntries) {
|
|
107
|
+
if (!agent.isDirectory())
|
|
108
|
+
continue;
|
|
109
|
+
const srcAgentDir = path.join(userVersions, agent.name);
|
|
110
|
+
const dstAgentDir = path.join(sysVersions, agent.name);
|
|
111
|
+
try {
|
|
112
|
+
fs.mkdirSync(dstAgentDir, { recursive: true, mode: 0o700 });
|
|
113
|
+
}
|
|
114
|
+
catch { /* best-effort */ }
|
|
115
|
+
let verEntries;
|
|
116
|
+
try {
|
|
117
|
+
verEntries = fs.readdirSync(srcAgentDir, { withFileTypes: true });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const ver of verEntries) {
|
|
123
|
+
if (!ver.isDirectory())
|
|
124
|
+
continue;
|
|
125
|
+
const src = path.join(srcAgentDir, ver.name);
|
|
126
|
+
const dst = path.join(dstAgentDir, ver.name);
|
|
127
|
+
if (fs.existsSync(dst)) {
|
|
128
|
+
skippedCount++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
fs.renameSync(src, dst);
|
|
133
|
+
movedCount++;
|
|
134
|
+
}
|
|
135
|
+
catch { /* best-effort, leave legacy in place */ }
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
if (fs.readdirSync(srcAgentDir).length === 0)
|
|
139
|
+
fs.rmdirSync(srcAgentDir);
|
|
140
|
+
}
|
|
141
|
+
catch { /* best-effort */ }
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (fs.readdirSync(userVersions).length === 0)
|
|
145
|
+
fs.rmdirSync(userVersions);
|
|
146
|
+
}
|
|
147
|
+
catch { /* best-effort */ }
|
|
148
|
+
if (movedCount > 0) {
|
|
149
|
+
console.log(`Migrated ${movedCount} version dir${movedCount === 1 ? '' : 's'} from ~/.agents/versions/ to ~/.agents-system/versions/`);
|
|
150
|
+
}
|
|
151
|
+
if (skippedCount > 0) {
|
|
152
|
+
console.log(`Skipped ${skippedCount} version dir${skippedCount === 1 ? '' : 's'} already present in ~/.agents-system/versions/ (kept legacy copy at ~/.agents/versions/)`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
79
155
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
80
156
|
export function runMigration() {
|
|
81
157
|
migrateAgentsYaml();
|
|
82
158
|
deleteSystemPromptsJson();
|
|
83
159
|
migrateSystemConfigJson();
|
|
84
160
|
migratePromptcutsIntoHooks();
|
|
161
|
+
migrateUserVersionsToSystem();
|
|
85
162
|
}
|
package/dist/lib/pty-client.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as fs from 'fs';
|
|
|
9
9
|
import { spawn, execSync } from 'child_process';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
-
import { getSocketPath, isPtyServerRunning } from './pty-server.js';
|
|
12
|
+
import { getSocketPath, getPtyLogPath, isPtyServerRunning } from './pty-server.js';
|
|
13
13
|
const CONNECT_TIMEOUT_MS = 5000;
|
|
14
14
|
const RESPONSE_TIMEOUT_MS = 30000;
|
|
15
15
|
/**
|
|
@@ -33,7 +33,7 @@ async function ensureServer() {
|
|
|
33
33
|
return;
|
|
34
34
|
// Find the entry point to spawn the server
|
|
35
35
|
const { bin, args } = getServerSpawnArgs();
|
|
36
|
-
const logPath =
|
|
36
|
+
const logPath = getPtyLogPath();
|
|
37
37
|
const logFd = fs.openSync(logPath, 'a');
|
|
38
38
|
const child = spawn(bin, args, {
|
|
39
39
|
stdio: ['ignore', logFd, logFd],
|
|
@@ -57,7 +57,7 @@ async function ensureServer() {
|
|
|
57
57
|
}
|
|
58
58
|
await new Promise(r => setTimeout(r, 100));
|
|
59
59
|
}
|
|
60
|
-
throw new Error('PTY server failed to start within 5 seconds. Check ~/.agents/pty.
|
|
60
|
+
throw new Error('PTY server failed to start within 5 seconds. Check ~/.agents-system/helpers/pty/logs.jsonl');
|
|
61
61
|
}
|
|
62
62
|
function getServerSpawnArgs() {
|
|
63
63
|
// Prefer the dist/index.js from the same installation as this code.
|
package/dist/lib/pty-server.js
CHANGED
|
@@ -53,9 +53,12 @@ export function captureProcessStartTime(pid) {
|
|
|
53
53
|
}
|
|
54
54
|
// --- Constants ---
|
|
55
55
|
const SENTINEL = '__AGENTS_PTY_DONE__';
|
|
56
|
+
const PTY_DIR = 'helpers/pty';
|
|
56
57
|
const SOCKET_NAME = 'pty.sock';
|
|
57
58
|
const PID_FILE = 'pty.pid';
|
|
58
|
-
const LOG_FILE = '
|
|
59
|
+
const LOG_FILE = 'logs.jsonl';
|
|
60
|
+
const LOG_MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
61
|
+
const LOG_ROTATE_COUNT = 3;
|
|
59
62
|
const SESSION_IDLE_MS = 30 * 60 * 1000; // 30 min
|
|
60
63
|
const SERVER_IDLE_MS = 60 * 60 * 1000; // 1 hour
|
|
61
64
|
// --- Path helpers ---
|
|
@@ -79,17 +82,24 @@ function buildPtyEnv() {
|
|
|
79
82
|
}
|
|
80
83
|
return env;
|
|
81
84
|
}
|
|
85
|
+
/** Get the PTY helper directory, creating it if needed. */
|
|
86
|
+
function getPtyDir() {
|
|
87
|
+
const dir = path.join(getAgentsDir(), PTY_DIR);
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
return dir;
|
|
90
|
+
}
|
|
82
91
|
/** Get the unix socket path for the PTY server. */
|
|
83
92
|
export function getSocketPath() {
|
|
84
|
-
return path.join(
|
|
93
|
+
return path.join(getPtyDir(), SOCKET_NAME);
|
|
85
94
|
}
|
|
86
95
|
/** Get the path to the PTY server PID file. */
|
|
87
96
|
export function getPtyPidPath() {
|
|
88
|
-
return path.join(
|
|
97
|
+
return path.join(getPtyDir(), PID_FILE);
|
|
89
98
|
}
|
|
90
99
|
/** Get the path to the PTY server log file. */
|
|
91
100
|
export function getPtyLogPath() {
|
|
92
|
-
|
|
101
|
+
const logDir = getPtyDir();
|
|
102
|
+
return path.join(logDir, LOG_FILE);
|
|
93
103
|
}
|
|
94
104
|
/** Check if the PTY server process is alive by probing the stored PID. */
|
|
95
105
|
export function isPtyServerRunning() {
|
|
@@ -112,11 +122,30 @@ export function isPtyServerRunning() {
|
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
// --- Logging ---
|
|
125
|
+
function rotateLogsIfNeeded(logPath) {
|
|
126
|
+
try {
|
|
127
|
+
const stat = fs.statSync(logPath);
|
|
128
|
+
if (stat.size < LOG_MAX_SIZE)
|
|
129
|
+
return;
|
|
130
|
+
for (let i = LOG_ROTATE_COUNT - 1; i >= 1; i--) {
|
|
131
|
+
const older = `${logPath}.${i}`;
|
|
132
|
+
const newer = i === 1 ? logPath : `${logPath}.${i - 1}`;
|
|
133
|
+
if (fs.existsSync(newer)) {
|
|
134
|
+
fs.renameSync(newer, older);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (fs.existsSync(logPath)) {
|
|
138
|
+
fs.renameSync(logPath, `${logPath}.1`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch { }
|
|
142
|
+
}
|
|
115
143
|
function log(level, message) {
|
|
116
|
-
const
|
|
117
|
-
|
|
144
|
+
const logPath = getPtyLogPath();
|
|
145
|
+
rotateLogsIfNeeded(logPath);
|
|
146
|
+
const entry = { ts: new Date().toISOString(), level, message };
|
|
118
147
|
try {
|
|
119
|
-
fs.appendFileSync(
|
|
148
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
120
149
|
}
|
|
121
150
|
catch { }
|
|
122
151
|
}
|
package/dist/lib/resources.js
CHANGED
|
@@ -8,7 +8,7 @@ import { AGENTS, listInstalledMcpsWithScope } from './agents.js';
|
|
|
8
8
|
import { listInstalledCommandsWithScope } from './commands.js';
|
|
9
9
|
import { listInstalledSkillsWithScope } from './skills.js';
|
|
10
10
|
import { listInstalledHooksWithScope } from './hooks.js';
|
|
11
|
-
import { listInstalledInstructionsWithScope } from './
|
|
11
|
+
import { listInstalledInstructionsWithScope } from './rules/rules.js';
|
|
12
12
|
import { getEffectiveHome } from './versions.js';
|
|
13
13
|
import { listMcpServerConfigs } from './mcp.js';
|
|
14
14
|
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from './state.js';
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -35,7 +35,14 @@ export declare const RUN_STRATEGIES: RunStrategy[];
|
|
|
35
35
|
export declare function normalizeRunStrategy(value: unknown): RunStrategy | null;
|
|
36
36
|
/** Read project-local run strategy from the nearest agents.yaml, if present. */
|
|
37
37
|
export declare function getProjectRunStrategy(agent: AgentId, startPath: string): RunStrategy | null;
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the configured strategy. Lookup order:
|
|
40
|
+
* 1. project-local agents.yaml (nearest to `startPath`)
|
|
41
|
+
* 2. ~/.agents-system/agents.yaml
|
|
42
|
+
* 3. default: `available` (use the pinned default version when healthy,
|
|
43
|
+
* otherwise fall through to a healthy account so a single rate-limited
|
|
44
|
+
* account doesn't block the run).
|
|
45
|
+
*/
|
|
39
46
|
export declare function getConfiguredRunStrategy(agent: AgentId, startPath?: string): RunStrategy;
|
|
40
47
|
/** Persist the global run strategy used by bare `agents run <agent>`. */
|
|
41
48
|
export declare function setGlobalRunStrategy(agent: AgentId, strategy: RunStrategy): void;
|
package/dist/lib/rotate.js
CHANGED
|
@@ -11,6 +11,12 @@ import { getAccountInfo } from './agents.js';
|
|
|
11
11
|
import { readMeta, writeMeta, getAgentsDir } from './state.js';
|
|
12
12
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
|
|
13
13
|
import { getUsageInfoByIdentity, getUsageLookupKey, isClaudeAuthValid, } from './usage.js';
|
|
14
|
+
const ROTATE_DIR = 'helpers/rotate';
|
|
15
|
+
function getRotateDir() {
|
|
16
|
+
const dir = path.join(getAgentsDir(), ROTATE_DIR);
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
14
20
|
export const RUN_STRATEGIES = ['pinned', 'available', 'balanced'];
|
|
15
21
|
/**
|
|
16
22
|
* Return a run strategy when the input is valid, otherwise null.
|
|
@@ -47,11 +53,18 @@ export function getProjectRunStrategy(agent, startPath) {
|
|
|
47
53
|
}
|
|
48
54
|
return null;
|
|
49
55
|
}
|
|
50
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the configured strategy. Lookup order:
|
|
58
|
+
* 1. project-local agents.yaml (nearest to `startPath`)
|
|
59
|
+
* 2. ~/.agents-system/agents.yaml
|
|
60
|
+
* 3. default: `available` (use the pinned default version when healthy,
|
|
61
|
+
* otherwise fall through to a healthy account so a single rate-limited
|
|
62
|
+
* account doesn't block the run).
|
|
63
|
+
*/
|
|
51
64
|
export function getConfiguredRunStrategy(agent, startPath = process.cwd()) {
|
|
52
65
|
return getProjectRunStrategy(agent, startPath)
|
|
53
66
|
?? normalizeRunStrategy(readMeta().run?.[agent]?.strategy)
|
|
54
|
-
?? '
|
|
67
|
+
?? 'available';
|
|
55
68
|
}
|
|
56
69
|
/** Persist the global run strategy used by bare `agents run <agent>`. */
|
|
57
70
|
export function setGlobalRunStrategy(agent, strategy) {
|
|
@@ -279,7 +292,7 @@ export async function selectAvailableVersion(agent, preferredVersion) {
|
|
|
279
292
|
* a torn write just means the next reader sees a stale timestamp (harmless).
|
|
280
293
|
*/
|
|
281
294
|
function recordRotationPick(agent, version) {
|
|
282
|
-
const stampPath = path.join(
|
|
295
|
+
const stampPath = path.join(getRotateDir(), `stamp-${agent}.json`);
|
|
283
296
|
try {
|
|
284
297
|
fs.writeFileSync(stampPath, JSON.stringify({ version, ts: Date.now() }), 'utf-8');
|
|
285
298
|
}
|
|
@@ -290,7 +303,7 @@ function recordRotationPick(agent, version) {
|
|
|
290
303
|
* or stamp is older than 60 seconds (stale).
|
|
291
304
|
*/
|
|
292
305
|
function readRotationStamp(agent) {
|
|
293
|
-
const stampPath = path.join(
|
|
306
|
+
const stampPath = path.join(getRotateDir(), `stamp-${agent}.json`);
|
|
294
307
|
try {
|
|
295
308
|
const raw = JSON.parse(fs.readFileSync(stampPath, 'utf-8'));
|
|
296
309
|
if (Date.now() - raw.ts < 60_000)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules file compilation -- resolving @-imports into a single flat file.
|
|
3
|
+
*
|
|
4
|
+
* Agents that do not natively resolve `@path/to/file` imports (Codex, Cursor)
|
|
5
|
+
* need a pre-compiled rules file with all imports inlined. This module
|
|
6
|
+
* handles that expansion for both user-scope (writes into version home) and
|
|
7
|
+
* project-scope (writes into the workspace).
|
|
8
|
+
*/
|
|
9
|
+
import type { AgentId } from '../types.js';
|
|
10
|
+
import { type RulesLayer } from './compose.js';
|
|
11
|
+
/** Sidecar manifest recording source file hashes for staleness detection. */
|
|
12
|
+
export interface CompileManifest {
|
|
13
|
+
compiledAt: string;
|
|
14
|
+
sources: {
|
|
15
|
+
path: string;
|
|
16
|
+
sha256: string;
|
|
17
|
+
mtime?: number;
|
|
18
|
+
size?: number;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
/** Result of resolving @-imports in a rules file. */
|
|
22
|
+
export interface ResolveResult {
|
|
23
|
+
/** Fully-inlined content. */
|
|
24
|
+
content: string;
|
|
25
|
+
/** Absolute paths of every file read during resolution (including the root). */
|
|
26
|
+
sources: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Expand all `@path/to/file` imports in `content`, recursively up to
|
|
30
|
+
* MAX_DEPTH. Imports inside fenced code blocks and inline code spans are
|
|
31
|
+
* left alone, matching Claude Code's parser. Missing files are left as-is
|
|
32
|
+
* (silent skip), matching the documented behavior.
|
|
33
|
+
*
|
|
34
|
+
* Relative paths resolve against `baseDir`; absolute and tilde-prefixed
|
|
35
|
+
* paths resolve against the filesystem root / home directory.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveImports(content: string, baseDir: string): ResolveResult;
|
|
38
|
+
/** True if the agent's native runtime resolves `@path` imports in its rules file. */
|
|
39
|
+
export declare function supportsRulesImports(agentId: AgentId): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Fast staleness check. Returns true when:
|
|
42
|
+
* - the compiled file or its manifest is missing
|
|
43
|
+
* - any recorded source file is missing
|
|
44
|
+
* - any recorded source's sha256 no longer matches
|
|
45
|
+
*
|
|
46
|
+
* For agents that support @-imports natively, always returns false — there's
|
|
47
|
+
* nothing to compile.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isRulesStale(agentId: AgentId, version: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the source `rules/AGENTS.md` (with all @-imports expanded) and
|
|
52
|
+
* write the result into the version home, alongside a sidecar manifest that
|
|
53
|
+
* records source file hashes for staleness detection.
|
|
54
|
+
*
|
|
55
|
+
* Agents that natively resolve @-imports are skipped (no-op) — their sync
|
|
56
|
+
* uses the standard copyFileSync path in `syncResourcesToVersion`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function compileRulesForAgent(agentId: AgentId, version: string): {
|
|
59
|
+
compiled: boolean;
|
|
60
|
+
compiledPath: string;
|
|
61
|
+
sources: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Recompile rules if stale. Safe to call on every agent invocation — the
|
|
65
|
+
* staleness check is fast (sha256 of 8-10 small files, ~10-20ms). Returns
|
|
66
|
+
* true if a recompile happened, false otherwise.
|
|
67
|
+
*/
|
|
68
|
+
export declare function ensureRulesFresh(agentId: AgentId, version: string): boolean;
|
|
69
|
+
export interface ProjectCompileResult {
|
|
70
|
+
/** True when cwd/AGENTS.md was newly written or rewritten. */
|
|
71
|
+
compiled: boolean;
|
|
72
|
+
/** Absolute path to cwd/AGENTS.md. Empty when no project rules dir was present. */
|
|
73
|
+
agentsPath: string;
|
|
74
|
+
/** Per-agent instruction filenames symlinked (or copied) to AGENTS.md. */
|
|
75
|
+
symlinks: string[];
|
|
76
|
+
/** Number of source files inlined (root + recursive @-imports). */
|
|
77
|
+
sources: number;
|
|
78
|
+
/** Per-agent files we left alone because the user wrote/owns them. */
|
|
79
|
+
skippedClobber: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compile project-scope rules into a workspace's root memory files so each
|
|
83
|
+
* agent's native loader picks them up.
|
|
84
|
+
*
|
|
85
|
+
* Composes rules from all available layers (project > user > extras > system)
|
|
86
|
+
* with project highest priority — so a project's `subrules/` and `rules.yaml`
|
|
87
|
+
* shadow user/system fragments and presets. Writes `cwd/AGENTS.md` with
|
|
88
|
+
* COMPILED_HEADER_PROJECT and creates symlinks (CLAUDE.md, GEMINI.md,
|
|
89
|
+
* .cursorrules, etc.) → AGENTS.md so every agent finds its expected file at
|
|
90
|
+
* cwd. The agent's own loader merges this project-level file with its
|
|
91
|
+
* user-level rules (in version home) at runtime.
|
|
92
|
+
*
|
|
93
|
+
* Don't-clobber guard: if `cwd/AGENTS.md` exists without our header, the user
|
|
94
|
+
* authored it — leave it alone and report via `skippedClobber`. Same for any
|
|
95
|
+
* pre-existing per-agent file or symlink that doesn't already point at
|
|
96
|
+
* AGENTS.md.
|
|
97
|
+
*
|
|
98
|
+
* No-op when `cwd/.agents/rules/` does not exist. Idempotent on repeated
|
|
99
|
+
* calls — content equality short-circuits the write.
|
|
100
|
+
*/
|
|
101
|
+
export declare function compileRulesForProject(cwd: string, opts?: {
|
|
102
|
+
preset?: string;
|
|
103
|
+
layers?: RulesLayer[];
|
|
104
|
+
}): ProjectCompileResult;
|
|
@@ -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
|
+
}
|