@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.
Files changed (101) hide show
  1. package/README.md +17 -7
  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/prune.js +9 -3
  8. package/dist/commands/refresh-rules.d.ts +15 -0
  9. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  10. package/dist/commands/routines.js +1 -1
  11. package/dist/commands/rules.js +100 -4
  12. package/dist/commands/secrets.js +198 -11
  13. package/dist/commands/sync.js +19 -0
  14. package/dist/commands/teams.js +162 -22
  15. package/dist/commands/trash.d.ts +10 -0
  16. package/dist/commands/trash.js +187 -0
  17. package/dist/commands/view.js +46 -13
  18. package/dist/index.js +62 -4
  19. package/dist/lib/agents.js +2 -2
  20. package/dist/lib/browser/cdp.d.ts +24 -0
  21. package/dist/lib/browser/cdp.js +94 -0
  22. package/dist/lib/browser/chrome.d.ts +16 -0
  23. package/dist/lib/browser/chrome.js +157 -0
  24. package/dist/lib/browser/drivers/local.d.ts +8 -0
  25. package/dist/lib/browser/drivers/local.js +22 -0
  26. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  27. package/dist/lib/browser/drivers/ssh.js +129 -0
  28. package/dist/lib/browser/index.d.ts +5 -0
  29. package/dist/lib/browser/index.js +5 -0
  30. package/dist/lib/browser/input.d.ts +6 -0
  31. package/dist/lib/browser/input.js +52 -0
  32. package/dist/lib/browser/ipc.d.ts +12 -0
  33. package/dist/lib/browser/ipc.js +223 -0
  34. package/dist/lib/browser/profiles.d.ts +11 -0
  35. package/dist/lib/browser/profiles.js +61 -0
  36. package/dist/lib/browser/refs.d.ts +21 -0
  37. package/dist/lib/browser/refs.js +88 -0
  38. package/dist/lib/browser/service.d.ts +45 -0
  39. package/dist/lib/browser/service.js +404 -0
  40. package/dist/lib/browser/types.d.ts +73 -0
  41. package/dist/lib/browser/types.js +7 -0
  42. package/dist/lib/cloud/codex.js +1 -1
  43. package/dist/lib/cloud/registry.js +2 -2
  44. package/dist/lib/cloud/rush.js +2 -2
  45. package/dist/lib/cloud/store.js +2 -2
  46. package/dist/lib/daemon.d.ts +1 -1
  47. package/dist/lib/daemon.js +47 -11
  48. package/dist/lib/diff-text.d.ts +25 -0
  49. package/dist/lib/diff-text.js +47 -0
  50. package/dist/lib/doctor-diff.d.ts +64 -0
  51. package/dist/lib/doctor-diff.js +497 -0
  52. package/dist/lib/git.js +3 -3
  53. package/dist/lib/hooks.d.ts +6 -0
  54. package/dist/lib/hooks.js +6 -1
  55. package/dist/lib/migrate.js +77 -0
  56. package/dist/lib/pty-client.js +3 -3
  57. package/dist/lib/pty-server.js +36 -7
  58. package/dist/lib/resources.js +1 -1
  59. package/dist/lib/rotate.d.ts +8 -1
  60. package/dist/lib/rotate.js +17 -4
  61. package/dist/lib/rules/compile.d.ts +104 -0
  62. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  63. package/dist/lib/rules/compose.d.ts +78 -0
  64. package/dist/lib/rules/compose.js +170 -0
  65. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  66. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  67. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  68. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  69. package/dist/lib/secrets/bundles.d.ts +61 -4
  70. package/dist/lib/secrets/bundles.js +222 -54
  71. package/dist/lib/secrets/index.d.ts +24 -5
  72. package/dist/lib/secrets/index.js +70 -41
  73. package/dist/lib/session/active.js +5 -5
  74. package/dist/lib/session/db.js +4 -4
  75. package/dist/lib/session/discover.js +2 -2
  76. package/dist/lib/session/render.js +21 -7
  77. package/dist/lib/shims.d.ts +28 -4
  78. package/dist/lib/shims.js +72 -14
  79. package/dist/lib/state.d.ts +22 -28
  80. package/dist/lib/state.js +83 -76
  81. package/dist/lib/sync-manifest.d.ts +2 -2
  82. package/dist/lib/sync-manifest.js +5 -5
  83. package/dist/lib/teams/agents.d.ts +4 -2
  84. package/dist/lib/teams/agents.js +11 -4
  85. package/dist/lib/teams/api.d.ts +1 -1
  86. package/dist/lib/teams/api.js +2 -2
  87. package/dist/lib/teams/index.d.ts +1 -0
  88. package/dist/lib/teams/index.js +1 -0
  89. package/dist/lib/teams/persistence.js +3 -3
  90. package/dist/lib/teams/registry.d.ts +8 -1
  91. package/dist/lib/teams/registry.js +8 -2
  92. package/dist/lib/teams/worktree.d.ts +30 -0
  93. package/dist/lib/teams/worktree.js +96 -0
  94. package/dist/lib/types.d.ts +12 -6
  95. package/dist/lib/types.js +3 -3
  96. package/dist/lib/versions.d.ts +30 -2
  97. package/dist/lib/versions.js +127 -105
  98. package/package.json +1 -1
  99. package/scripts/postinstall.js +29 -0
  100. package/dist/commands/refresh-memory.d.ts +0 -15
  101. package/dist/lib/memory-compile.d.ts +0 -66
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Tiny unified-diff helpers for human-readable doctor output.
3
+ *
4
+ * Wraps the `diff` package's createPatch into one call that returns a
5
+ * pre-coloured unified diff (red = removed, green = added, dim = context).
6
+ * Used by `agents doctor --diff`.
7
+ */
8
+ import chalk from 'chalk';
9
+ import { createPatch } from 'diff';
10
+ /**
11
+ * Build a unified-diff text comparing two strings. Returns an empty string
12
+ * when contents are identical.
13
+ */
14
+ export function unifiedDiff(expected, actual, options = {}) {
15
+ if (expected === actual)
16
+ return '';
17
+ const fromLabel = options.fromLabel ?? 'expected';
18
+ const toLabel = options.toLabel ?? 'actual';
19
+ const context = options.context ?? 3;
20
+ return createPatch(fromLabel, expected, actual, '', '', { context });
21
+ }
22
+ /**
23
+ * Colour a unified-diff string for terminal output. Indents each line with
24
+ * a constant prefix so it nests cleanly under a header.
25
+ */
26
+ export function colorizeUnifiedDiff(patch, indent = ' ') {
27
+ const lines = patch.split('\n');
28
+ const out = [];
29
+ for (const line of lines) {
30
+ if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('Index:') || line.startsWith('===')) {
31
+ out.push(indent + chalk.gray(line));
32
+ }
33
+ else if (line.startsWith('@@')) {
34
+ out.push(indent + chalk.cyan(line));
35
+ }
36
+ else if (line.startsWith('+')) {
37
+ out.push(indent + chalk.green(line));
38
+ }
39
+ else if (line.startsWith('-')) {
40
+ out.push(indent + chalk.red(line));
41
+ }
42
+ else {
43
+ out.push(indent + chalk.gray(line));
44
+ }
45
+ }
46
+ return out.join('\n');
47
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Per-version, per-cwd resource diff for `agents doctor <agent[@version]>`.
3
+ *
4
+ * Mirrors what `syncResourcesToVersion` writes into a version home, then
5
+ * compares each kind back against its resolved source (project > user > system
6
+ * > extra repos). Surfaces:
7
+ * - ok — present in home, content matches resolved source
8
+ * - diff — present in home, content differs from resolved source
9
+ * - missing — resolved source exists, not present in home
10
+ * - extra — present in home, no source in any layer
11
+ *
12
+ * Coverage:
13
+ * commands, skills, hooks, rules — full content compare with source layer.
14
+ * mcp, permissions, subagents, plugins, promptcuts — presence-only.
15
+ *
16
+ * Intentional asymmetries (must mirror sync):
17
+ * - hooks ignore the project layer (`syncResourcesToVersion` skips
18
+ * project/.agents/hooks/ for safety).
19
+ * - rules/AGENTS.md on agents without native @-import support is compared
20
+ * against the compiled artifact, not the raw source file.
21
+ */
22
+ import type { AgentId } from './types.js';
23
+ export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'promptcuts';
24
+ export type DiffStatus = 'ok' | 'diff' | 'missing' | 'extra';
25
+ export type SourceLayer = 'project' | 'user' | 'system' | 'extra';
26
+ export interface ResourceDiff {
27
+ kind: DoctorKind;
28
+ name: string;
29
+ status: DiffStatus;
30
+ source?: SourceLayer;
31
+ /** Absolute path to the resolved source file/dir (when source is known). */
32
+ sourcePath?: string;
33
+ /** Absolute path to the file/dir inside the version home (when present). */
34
+ homePath?: string;
35
+ }
36
+ export interface VersionResourceReport {
37
+ agent: AgentId;
38
+ version: string;
39
+ home: string;
40
+ cwd: string;
41
+ layers: {
42
+ project: string | null;
43
+ user: string;
44
+ system: string;
45
+ extras: Array<{
46
+ alias: string;
47
+ dir: string;
48
+ }>;
49
+ };
50
+ kinds: Record<DoctorKind, ResourceDiff[]>;
51
+ summary: {
52
+ ok: number;
53
+ diff: number;
54
+ missing: number;
55
+ extra: number;
56
+ };
57
+ }
58
+ export interface DiffOptions {
59
+ cwd?: string;
60
+ /** Restrict to specific kinds; undefined = all. */
61
+ kinds?: DoctorKind[];
62
+ }
63
+ export declare function diffVersionResources(agent: AgentId, version: string, options?: DiffOptions): VersionResourceReport;
64
+ export declare const DOCTOR_ALL_KINDS: DoctorKind[];
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Per-version, per-cwd resource diff for `agents doctor <agent[@version]>`.
3
+ *
4
+ * Mirrors what `syncResourcesToVersion` writes into a version home, then
5
+ * compares each kind back against its resolved source (project > user > system
6
+ * > extra repos). Surfaces:
7
+ * - ok — present in home, content matches resolved source
8
+ * - diff — present in home, content differs from resolved source
9
+ * - missing — resolved source exists, not present in home
10
+ * - extra — present in home, no source in any layer
11
+ *
12
+ * Coverage:
13
+ * commands, skills, hooks, rules — full content compare with source layer.
14
+ * mcp, permissions, subagents, plugins, promptcuts — presence-only.
15
+ *
16
+ * Intentional asymmetries (must mirror sync):
17
+ * - hooks ignore the project layer (`syncResourcesToVersion` skips
18
+ * project/.agents/hooks/ for safety).
19
+ * - rules/AGENTS.md on agents without native @-import support is compared
20
+ * against the compiled artifact, not the raw source file.
21
+ */
22
+ import * as fs from 'fs';
23
+ import * as path from 'path';
24
+ import { AGENTS } from './agents.js';
25
+ import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir, getPromptcutsPath, } from './state.js';
26
+ import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, } from './versions.js';
27
+ import { markdownToToml } from './convert.js';
28
+ import { resolveImports, supportsRulesImports } from './rules/compile.js';
29
+ import { listCommandsInVersionHome, getVersionCommandsDir } from './commands.js';
30
+ import { listSkillsInVersionHome, getVersionSkillsDir } from './skills.js';
31
+ import { listHooksInVersionHome, listHookEntriesFromDir } from './hooks.js';
32
+ const RULES_DOC_FILENAME = 'README.md';
33
+ const COMPILED_HEADER = '<!-- Auto-compiled by agents-cli from ~/.agents/rules/AGENTS.md + imports.\n' +
34
+ ' Edit the source files under ~/.agents/rules/ — edits to this file will be overwritten on next sync. -->\n\n';
35
+ const ALL_KINDS = [
36
+ 'commands',
37
+ 'skills',
38
+ 'hooks',
39
+ 'rules',
40
+ 'mcp',
41
+ 'permissions',
42
+ 'subagents',
43
+ 'plugins',
44
+ 'promptcuts',
45
+ ];
46
+ function normalize(content) {
47
+ return content.replace(/\r\n/g, '\n').trim();
48
+ }
49
+ function readSafe(file) {
50
+ try {
51
+ return fs.readFileSync(file, 'utf-8');
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function fileExists(p) {
58
+ return !!p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink();
59
+ }
60
+ function findFirst(candidates) {
61
+ for (const c of candidates) {
62
+ if (fileExists(c.path) || (fs.existsSync(c.path) && fs.lstatSync(c.path).isDirectory())) {
63
+ return c;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ function buildLayerBases(cwd, kind, opts = {}) {
69
+ const projectDir = opts.excludeProject ? null : getProjectAgentsDir(cwd);
70
+ const userDir = getUserAgentsDir();
71
+ const systemDir = getSystemAgentsDir();
72
+ const extras = getEnabledExtraRepos();
73
+ const out = [];
74
+ if (projectDir)
75
+ out.push({ layer: 'project', path: path.join(projectDir, kind) });
76
+ out.push({ layer: 'user', path: path.join(userDir, kind) });
77
+ out.push({ layer: 'system', path: path.join(systemDir, kind) });
78
+ for (const e of extras)
79
+ out.push({ layer: 'extra', path: path.join(e.dir, kind), alias: e.alias });
80
+ return out;
81
+ }
82
+ // ─── commands ─────────────────────────────────────────────────────────────────
83
+ function diffCommands(agent, version, cwd) {
84
+ const agentConfig = AGENTS[agent];
85
+ const isToml = agentConfig.format === 'toml';
86
+ const ext = isToml ? '.toml' : '.md';
87
+ const homeDir = getVersionCommandsDir(agent, version);
88
+ const installed = new Set(listCommandsInVersionHome(agent, version));
89
+ const layerBases = buildLayerBases(cwd, 'commands');
90
+ const sourceByName = new Map();
91
+ for (const base of layerBases) {
92
+ if (!fs.existsSync(base.path))
93
+ continue;
94
+ let entries;
95
+ try {
96
+ entries = fs.readdirSync(base.path, { withFileTypes: true });
97
+ }
98
+ catch {
99
+ continue;
100
+ }
101
+ for (const entry of entries) {
102
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
103
+ continue;
104
+ const name = entry.name.replace(/\.md$/, '');
105
+ if (sourceByName.has(name))
106
+ continue;
107
+ sourceByName.set(name, { layer: base.layer, path: path.join(base.path, entry.name), alias: base.alias });
108
+ }
109
+ }
110
+ const rows = [];
111
+ const seen = new Set();
112
+ for (const [name, src] of sourceByName) {
113
+ seen.add(name);
114
+ const homePath = path.join(homeDir, `${name}${ext}`);
115
+ if (!installed.has(name)) {
116
+ rows.push({ kind: 'commands', name, status: 'missing', source: src.layer, sourcePath: src.path });
117
+ continue;
118
+ }
119
+ const installedContent = readSafe(homePath);
120
+ const sourceContent = readSafe(src.path);
121
+ if (installedContent == null || sourceContent == null) {
122
+ rows.push({ kind: 'commands', name, status: 'diff', source: src.layer, sourcePath: src.path, homePath });
123
+ continue;
124
+ }
125
+ const expected = isToml ? markdownToToml(name, sourceContent) : sourceContent;
126
+ const matches = normalize(installedContent) === normalize(expected);
127
+ rows.push({
128
+ kind: 'commands',
129
+ name,
130
+ status: matches ? 'ok' : 'diff',
131
+ source: src.layer,
132
+ sourcePath: src.path,
133
+ homePath,
134
+ });
135
+ }
136
+ for (const name of installed) {
137
+ if (seen.has(name))
138
+ continue;
139
+ rows.push({ kind: 'commands', name, status: 'extra', homePath: path.join(homeDir, `${name}${ext}`) });
140
+ }
141
+ return rows.sort((a, b) => a.name.localeCompare(b.name));
142
+ }
143
+ // ─── skills ───────────────────────────────────────────────────────────────────
144
+ function dirsContentMatch(src, dst) {
145
+ const ignore = new Set(['.DS_Store', '.git', '.gitignore', '.venv', '__pycache__', 'node_modules']);
146
+ const srcEntries = (() => {
147
+ try {
148
+ return fs.readdirSync(src, { withFileTypes: true });
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ })();
154
+ const dstEntries = (() => {
155
+ try {
156
+ return fs.readdirSync(dst, { withFileTypes: true });
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ })();
162
+ if (!srcEntries || !dstEntries)
163
+ return false;
164
+ const filter = (es) => es.filter((e) => !e.isSymbolicLink() && !ignore.has(e.name)).sort((a, b) => a.name.localeCompare(b.name));
165
+ const srcF = filter(srcEntries);
166
+ const dstF = filter(dstEntries);
167
+ if (srcF.length !== dstF.length)
168
+ return false;
169
+ for (let i = 0; i < srcF.length; i++) {
170
+ if (srcF[i].name !== dstF[i].name)
171
+ return false;
172
+ const a = path.join(src, srcF[i].name);
173
+ const b = path.join(dst, dstF[i].name);
174
+ if (srcF[i].isDirectory()) {
175
+ if (!dstF[i].isDirectory())
176
+ return false;
177
+ if (!dirsContentMatch(a, b))
178
+ return false;
179
+ }
180
+ else if (srcF[i].isFile()) {
181
+ if (!dstF[i].isFile())
182
+ return false;
183
+ const ac = readSafe(a);
184
+ const bc = readSafe(b);
185
+ if (ac == null || bc == null)
186
+ return false;
187
+ if (normalize(ac) !== normalize(bc))
188
+ return false;
189
+ }
190
+ }
191
+ return true;
192
+ }
193
+ function diffSkills(agent, version, cwd) {
194
+ const homeDir = getVersionSkillsDir(agent, version);
195
+ const installed = new Set(listSkillsInVersionHome(agent, version));
196
+ const layerBases = buildLayerBases(cwd, 'skills');
197
+ const sourceByName = new Map();
198
+ for (const base of layerBases) {
199
+ if (!fs.existsSync(base.path))
200
+ continue;
201
+ let entries;
202
+ try {
203
+ entries = fs.readdirSync(base.path, { withFileTypes: true });
204
+ }
205
+ catch {
206
+ continue;
207
+ }
208
+ for (const entry of entries) {
209
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
210
+ continue;
211
+ if (!fs.existsSync(path.join(base.path, entry.name, 'SKILL.md')))
212
+ continue;
213
+ if (sourceByName.has(entry.name))
214
+ continue;
215
+ sourceByName.set(entry.name, { layer: base.layer, path: path.join(base.path, entry.name), alias: base.alias });
216
+ }
217
+ }
218
+ const rows = [];
219
+ const seen = new Set();
220
+ for (const [name, src] of sourceByName) {
221
+ seen.add(name);
222
+ const homePath = path.join(homeDir, name);
223
+ if (!installed.has(name)) {
224
+ rows.push({ kind: 'skills', name, status: 'missing', source: src.layer, sourcePath: src.path });
225
+ continue;
226
+ }
227
+ const matches = dirsContentMatch(src.path, homePath);
228
+ rows.push({
229
+ kind: 'skills',
230
+ name,
231
+ status: matches ? 'ok' : 'diff',
232
+ source: src.layer,
233
+ sourcePath: src.path,
234
+ homePath,
235
+ });
236
+ }
237
+ for (const name of installed) {
238
+ if (seen.has(name))
239
+ continue;
240
+ rows.push({ kind: 'skills', name, status: 'extra', homePath: path.join(homeDir, name) });
241
+ }
242
+ return rows.sort((a, b) => a.name.localeCompare(b.name));
243
+ }
244
+ // ─── hooks ────────────────────────────────────────────────────────────────────
245
+ function diffHooks(agent, version, cwd) {
246
+ if (!AGENTS[agent].supportsHooks)
247
+ return [];
248
+ const installedEntries = listHooksInVersionHome(agent, version);
249
+ const installedByName = new Map(installedEntries.map((e) => [e.name, e]));
250
+ // Sync intentionally excludes project/.agents/hooks/ — mirror that.
251
+ const layerBases = buildLayerBases(cwd, 'hooks', { excludeProject: true });
252
+ // Group source files the same way the hook installer does (basename across
253
+ // script + sidecar data file); first-layer wins on name collision.
254
+ const sourceByName = new Map();
255
+ for (const base of layerBases) {
256
+ if (!fs.existsSync(base.path))
257
+ continue;
258
+ for (const entry of listHookEntriesFromDir(base.path)) {
259
+ if (sourceByName.has(entry.name))
260
+ continue;
261
+ sourceByName.set(entry.name, { layer: base.layer, alias: base.alias, entry });
262
+ }
263
+ }
264
+ const rows = [];
265
+ const seen = new Set();
266
+ for (const [name, src] of sourceByName) {
267
+ seen.add(name);
268
+ const installed = installedByName.get(name);
269
+ if (!installed) {
270
+ rows.push({ kind: 'hooks', name, status: 'missing', source: src.layer, sourcePath: src.entry.scriptPath });
271
+ continue;
272
+ }
273
+ const a = readSafe(src.entry.scriptPath);
274
+ const b = readSafe(installed.scriptPath);
275
+ let matches = a != null && b != null && normalize(a) === normalize(b);
276
+ if (matches && src.entry.dataFile && installed.dataFile) {
277
+ const ad = readSafe(src.entry.dataFile);
278
+ const bd = readSafe(installed.dataFile);
279
+ matches = ad != null && bd != null && normalize(ad) === normalize(bd);
280
+ }
281
+ else if (matches && (!!src.entry.dataFile !== !!installed.dataFile)) {
282
+ matches = false;
283
+ }
284
+ rows.push({
285
+ kind: 'hooks',
286
+ name,
287
+ status: matches ? 'ok' : 'diff',
288
+ source: src.layer,
289
+ sourcePath: src.entry.scriptPath,
290
+ homePath: installed.scriptPath,
291
+ });
292
+ }
293
+ for (const [name, installed] of installedByName) {
294
+ if (seen.has(name))
295
+ continue;
296
+ rows.push({ kind: 'hooks', name, status: 'extra', homePath: installed.scriptPath });
297
+ }
298
+ return rows.sort((a, b) => a.name.localeCompare(b.name));
299
+ }
300
+ // ─── rules / memory ───────────────────────────────────────────────────────────
301
+ function listRulesNames(cwd) {
302
+ const projectDir = getProjectAgentsDir(cwd);
303
+ const userRules = getUserRulesDir();
304
+ const systemRules = getResolvedRulesDir();
305
+ const extras = getEnabledExtraRepos();
306
+ const layers = [];
307
+ if (projectDir)
308
+ layers.push({ layer: 'project', path: path.join(projectDir, 'rules') });
309
+ layers.push({ layer: 'user', path: userRules });
310
+ layers.push({ layer: 'system', path: systemRules });
311
+ for (const e of extras)
312
+ layers.push({ layer: 'extra', path: path.join(e.dir, 'rules'), alias: e.alias });
313
+ const out = new Map();
314
+ for (const base of layers) {
315
+ if (!fs.existsSync(base.path))
316
+ continue;
317
+ let entries;
318
+ try {
319
+ entries = fs.readdirSync(base.path);
320
+ }
321
+ catch {
322
+ continue;
323
+ }
324
+ for (const file of entries) {
325
+ if (!file.endsWith('.md') || file === RULES_DOC_FILENAME)
326
+ continue;
327
+ const stat = fs.lstatSync(path.join(base.path, file));
328
+ if (stat.isSymbolicLink())
329
+ continue;
330
+ const name = file.replace(/\.md$/, '');
331
+ if (out.has(name))
332
+ continue;
333
+ out.set(name, { layer: base.layer, path: path.join(base.path, file), alias: base.alias });
334
+ }
335
+ }
336
+ return out;
337
+ }
338
+ function expectedRuleContent(agent, name, sourcePath) {
339
+ // AGENTS.md on agents without native @-import support is compiled with imports inlined.
340
+ if (name === 'AGENTS' && !supportsRulesImports(agent)) {
341
+ const root = readSafe(sourcePath);
342
+ if (root == null)
343
+ return null;
344
+ // Compile relative to the source's own dir so project-layer AGENTS.md resolves
345
+ // its imports relative to the project rules dir, not the user one.
346
+ const baseDir = path.dirname(sourcePath);
347
+ const { content } = resolveImports(root, baseDir);
348
+ return COMPILED_HEADER + content;
349
+ }
350
+ return readSafe(sourcePath);
351
+ }
352
+ function diffRules(agent, version, cwd) {
353
+ const agentConfig = AGENTS[agent];
354
+ const versionHome = getVersionHomePath(agent, version);
355
+ const configDir = path.join(versionHome, `.${agent}`);
356
+ const sourcesByName = listRulesNames(cwd);
357
+ // Files actually present in the version home.
358
+ const homeFiles = new Set();
359
+ if (fs.existsSync(configDir)) {
360
+ for (const f of fs.readdirSync(configDir)) {
361
+ if (!f.endsWith('.md'))
362
+ continue;
363
+ homeFiles.add(f);
364
+ }
365
+ }
366
+ const rows = [];
367
+ const homeSeen = new Set();
368
+ for (const [name, src] of sourcesByName) {
369
+ const targetName = name === 'AGENTS' ? agentConfig.instructionsFile : `${name}.md`;
370
+ homeSeen.add(targetName);
371
+ const homePath = path.join(configDir, targetName);
372
+ if (!homeFiles.has(targetName)) {
373
+ rows.push({ kind: 'rules', name, status: 'missing', source: src.layer, sourcePath: src.path });
374
+ continue;
375
+ }
376
+ const expected = expectedRuleContent(agent, name, src.path);
377
+ const actual = readSafe(homePath);
378
+ if (expected == null || actual == null) {
379
+ rows.push({ kind: 'rules', name, status: 'diff', source: src.layer, sourcePath: src.path, homePath });
380
+ continue;
381
+ }
382
+ rows.push({
383
+ kind: 'rules',
384
+ name,
385
+ status: normalize(expected) === normalize(actual) ? 'ok' : 'diff',
386
+ source: src.layer,
387
+ sourcePath: src.path,
388
+ homePath,
389
+ });
390
+ }
391
+ // Anything in the configDir matching an instructions filename or AGENTS.md
392
+ // but with no source is an extra. We only report files that look like rules
393
+ // — i.e. the agent's instructionsFile, plus any *.md siblings that came
394
+ // from the rules sync.
395
+ const rulesFilenames = new Set();
396
+ rulesFilenames.add(agentConfig.instructionsFile);
397
+ for (const targetName of homeSeen)
398
+ rulesFilenames.add(targetName);
399
+ for (const f of homeFiles) {
400
+ if (homeSeen.has(f))
401
+ continue;
402
+ if (!rulesFilenames.has(f) && f !== agentConfig.instructionsFile)
403
+ continue;
404
+ const name = f === agentConfig.instructionsFile ? 'AGENTS' : f.replace(/\.md$/, '');
405
+ rows.push({ kind: 'rules', name, status: 'extra', homePath: path.join(configDir, f) });
406
+ }
407
+ return rows.sort((a, b) => a.name.localeCompare(b.name));
408
+ }
409
+ // ─── presence-only kinds ──────────────────────────────────────────────────────
410
+ function diffPresenceOnly(kind, available, synced) {
411
+ const availableSet = new Set(available);
412
+ const syncedSet = new Set(synced);
413
+ const rows = [];
414
+ for (const name of available) {
415
+ rows.push({
416
+ kind,
417
+ name,
418
+ status: syncedSet.has(name) ? 'ok' : 'missing',
419
+ });
420
+ }
421
+ for (const name of synced) {
422
+ if (availableSet.has(name))
423
+ continue;
424
+ rows.push({ kind, name, status: 'extra' });
425
+ }
426
+ return rows.sort((a, b) => a.name.localeCompare(b.name));
427
+ }
428
+ function diffPromptcuts() {
429
+ const exists = fs.existsSync(getPromptcutsPath());
430
+ if (!exists)
431
+ return [];
432
+ return [{ kind: 'promptcuts', name: 'promptcuts.yaml', status: 'ok', sourcePath: getPromptcutsPath() }];
433
+ }
434
+ export function diffVersionResources(agent, version, options = {}) {
435
+ const cwd = options.cwd ?? process.cwd();
436
+ const home = getVersionHomePath(agent, version);
437
+ const requested = new Set(options.kinds ?? ALL_KINDS);
438
+ const available = getAvailableResources(cwd);
439
+ const synced = getActuallySyncedResources(agent, version, { cwd });
440
+ const empty = {
441
+ commands: [],
442
+ skills: [],
443
+ hooks: [],
444
+ rules: [],
445
+ mcp: [],
446
+ permissions: [],
447
+ subagents: [],
448
+ plugins: [],
449
+ promptcuts: [],
450
+ };
451
+ if (requested.has('commands'))
452
+ empty.commands = diffCommands(agent, version, cwd);
453
+ if (requested.has('skills'))
454
+ empty.skills = diffSkills(agent, version, cwd);
455
+ if (requested.has('hooks'))
456
+ empty.hooks = diffHooks(agent, version, cwd);
457
+ if (requested.has('rules'))
458
+ empty.rules = diffRules(agent, version, cwd);
459
+ if (requested.has('mcp'))
460
+ empty.mcp = diffPresenceOnly('mcp', available.mcp, synced.mcp);
461
+ if (requested.has('permissions'))
462
+ empty.permissions = diffPresenceOnly('permissions', available.permissions, synced.permissions);
463
+ if (requested.has('subagents'))
464
+ empty.subagents = diffPresenceOnly('subagents', available.subagents, synced.subagents);
465
+ if (requested.has('plugins'))
466
+ empty.plugins = diffPresenceOnly('plugins', available.plugins, synced.plugins);
467
+ if (requested.has('promptcuts'))
468
+ empty.promptcuts = diffPromptcuts();
469
+ let ok = 0, diff = 0, missing = 0, extra = 0;
470
+ for (const list of Object.values(empty)) {
471
+ for (const r of list) {
472
+ if (r.status === 'ok')
473
+ ok++;
474
+ else if (r.status === 'diff')
475
+ diff++;
476
+ else if (r.status === 'missing')
477
+ missing++;
478
+ else if (r.status === 'extra')
479
+ extra++;
480
+ }
481
+ }
482
+ return {
483
+ agent,
484
+ version,
485
+ home,
486
+ cwd,
487
+ layers: {
488
+ project: getProjectAgentsDir(cwd),
489
+ user: getUserAgentsDir(),
490
+ system: getSystemAgentsDir(),
491
+ extras: getEnabledExtraRepos().map((e) => ({ alias: e.alias, dir: e.dir })),
492
+ },
493
+ kinds: empty,
494
+ summary: { ok, diff, missing, extra },
495
+ };
496
+ }
497
+ export const DOCTOR_ALL_KINDS = ALL_KINDS;
package/dist/lib/git.js CHANGED
@@ -9,7 +9,7 @@ import simpleGit from 'simple-git';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import { getPackageLocalPath } from './state.js';
12
- import { DEFAULT_SYSTEM_REPO, LEGACY_SYSTEM_REPO, systemRepoSlug } from './types.js';
12
+ import { DEFAULT_SYSTEM_REPO, MIRROR_SYSTEM_REPO, systemRepoSlug } from './types.js';
13
13
  /**
14
14
  * Install hooks from `.githooks/` by symlinking each entry into `.git/hooks/`.
15
15
  *
@@ -401,8 +401,8 @@ export async function isSystemRepoOrigin(dir) {
401
401
  // Check if origin points at the current or legacy system repo.
402
402
  const url = origin.refs.fetch.toLowerCase();
403
403
  const currentSlug = systemRepoSlug(DEFAULT_SYSTEM_REPO).toLowerCase();
404
- const legacySlug = systemRepoSlug(LEGACY_SYSTEM_REPO).toLowerCase();
405
- return url.includes(currentSlug) || url.includes(legacySlug);
404
+ const mirrorSlug = systemRepoSlug(MIRROR_SYSTEM_REPO).toLowerCase();
405
+ return url.includes(currentSlug) || url.includes(mirrorSlug);
406
406
  }
407
407
  catch {
408
408
  /* not a git repo or no remotes */
@@ -13,6 +13,12 @@ export type HookEntry = {
13
13
  scriptPath: string;
14
14
  dataFile?: string;
15
15
  };
16
+ /**
17
+ * List hook entries in a single directory, grouping script + data files by
18
+ * basename. Exported so doctor-diff can reuse the same grouping the sync path
19
+ * applies; without this, doctor would double-count `foo.sh` and `foo.yaml`.
20
+ */
21
+ export declare function listHookEntriesFromDir(dir: string): HookEntry[];
16
22
  /**
17
23
  * Check if a hook exists for an agent.
18
24
  */
package/dist/lib/hooks.js CHANGED
@@ -72,7 +72,12 @@ function removeHookFiles(dir, name) {
72
72
  }
73
73
  }
74
74
  }
75
- function listHookEntriesFromDir(dir) {
75
+ /**
76
+ * List hook entries in a single directory, grouping script + data files by
77
+ * basename. Exported so doctor-diff can reuse the same grouping the sync path
78
+ * applies; without this, doctor would double-count `foo.sh` and `foo.yaml`.
79
+ */
80
+ export function listHookEntriesFromDir(dir) {
76
81
  if (!fs.existsSync(dir)) {
77
82
  return [];
78
83
  }