@phnx-labs/agents-cli 1.18.1 → 1.18.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 (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/commands/doctor.js +19 -5
  3. package/dist/commands/exec.js +9 -4
  4. package/dist/commands/plugins.js +58 -14
  5. package/dist/commands/view.js +16 -7
  6. package/dist/index.js +30 -0
  7. package/dist/lib/hooks.js +21 -3
  8. package/dist/lib/migrate.js +35 -12
  9. package/dist/lib/plugin-marketplace.d.ts +93 -0
  10. package/dist/lib/plugin-marketplace.js +239 -0
  11. package/dist/lib/plugins.d.ts +25 -13
  12. package/dist/lib/plugins.js +350 -566
  13. package/dist/lib/shims.d.ts +3 -1
  14. package/dist/lib/shims.js +81 -7
  15. package/dist/lib/staleness/checkers/commands.d.ts +7 -0
  16. package/dist/lib/staleness/checkers/commands.js +27 -0
  17. package/dist/lib/staleness/checkers/hooks.d.ts +13 -0
  18. package/dist/lib/staleness/checkers/hooks.js +63 -0
  19. package/dist/lib/staleness/checkers/mcp.d.ts +12 -0
  20. package/dist/lib/staleness/checkers/mcp.js +38 -0
  21. package/dist/lib/staleness/checkers/permissions.d.ts +17 -0
  22. package/dist/lib/staleness/checkers/permissions.js +73 -0
  23. package/dist/lib/staleness/checkers/plugins.d.ts +11 -0
  24. package/dist/lib/staleness/checkers/plugins.js +39 -0
  25. package/dist/lib/staleness/checkers/rules.d.ts +19 -0
  26. package/dist/lib/staleness/checkers/rules.js +86 -0
  27. package/dist/lib/staleness/checkers/skills.d.ts +7 -0
  28. package/dist/lib/staleness/checkers/skills.js +34 -0
  29. package/dist/lib/staleness/checkers/subagents.d.ts +12 -0
  30. package/dist/lib/staleness/checkers/subagents.js +39 -0
  31. package/dist/lib/staleness/checkers/types.d.ts +44 -0
  32. package/dist/lib/staleness/checkers/types.js +20 -0
  33. package/dist/lib/staleness/checkers/workflows.d.ts +10 -0
  34. package/dist/lib/staleness/checkers/workflows.js +37 -0
  35. package/dist/lib/staleness/fingerprint.d.ts +38 -0
  36. package/dist/lib/staleness/fingerprint.js +154 -0
  37. package/dist/lib/staleness/index.d.ts +26 -0
  38. package/dist/lib/staleness/index.js +122 -0
  39. package/dist/lib/staleness/layers.d.ts +37 -0
  40. package/dist/lib/staleness/layers.js +100 -0
  41. package/dist/lib/staleness/types.d.ts +56 -0
  42. package/dist/lib/staleness/types.js +6 -0
  43. package/dist/lib/state.d.ts +2 -0
  44. package/dist/lib/state.js +2 -0
  45. package/dist/lib/teams/agents.d.ts +11 -20
  46. package/dist/lib/teams/agents.js +55 -202
  47. package/dist/lib/teams/index.d.ts +3 -2
  48. package/dist/lib/teams/index.js +2 -2
  49. package/dist/lib/teams/persistence.d.ts +0 -38
  50. package/dist/lib/teams/persistence.js +7 -329
  51. package/dist/lib/teams/registry.js +7 -5
  52. package/dist/lib/types.d.ts +6 -0
  53. package/dist/lib/versions.js +34 -12
  54. package/package.json +1 -1
  55. package/dist/lib/sync-manifest.d.ts +0 -81
  56. package/dist/lib/sync-manifest.js +0 -450
@@ -1,450 +0,0 @@
1
- /**
2
- * Sync manifest — fast staleness guard for syncResourcesToVersion.
3
- *
4
- * Written after each full sync; read before the next to skip unconditional
5
- * re-copies when nothing has changed. Two-tier check:
6
- * Tier 1 — stat mtime+size ~0.01ms/file (kernel VFS cache)
7
- * Tier 2 — sha256 on miss ~0.1–0.5ms/file
8
- *
9
- * Env vars in MCP YAML are NOT substituted by agents-cli — ${VAR} is stored
10
- * verbatim and passed as-is to `claude mcp add`. Value-only env var changes
11
- * (YAML content unchanged) are not detected and are not in scope.
12
- *
13
- * Layering model:
14
- * First-wins (commands, skills, hooks, MCP, rules): manifest stores fingerprint
15
- * of the WINNING source (project > user > system > extra). A scope change
16
- * (e.g. project file added/removed) is detected via resolved-path mismatch.
17
- *
18
- * Merged (permissions): all group files across all scopes contribute. Any
19
- * change in any scope file triggers re-sync.
20
- *
21
- * The manifest is written only after a full (unselected) sync and is only used
22
- * as a guard for full syncs — partial/interactive syncs bypass it entirely.
23
- */
24
- import * as fs from 'fs';
25
- import * as path from 'path';
26
- import * as crypto from 'crypto';
27
- import { getVersionsDir, getProjectAgentsDir, getUserAgentsDir, getSkillsDir, getUserHooksDir, getHooksDir, getUserRulesDir, getResolvedRulesDir, getUserPermissionsDir, getPermissionsDir, getEnabledExtraRepos, } from './state.js';
28
- import { resolveResource } from './resources.js';
29
- import { listMcpServerConfigs } from './mcp.js';
30
- import { isRulesStale } from './rules/compile.js';
31
- import { getActivePermissionPresetName } from './permissions.js';
32
- import { listInstalledSubagents } from './subagents.js';
33
- import { safeJoin } from './paths.js';
34
- // ─── Types ────────────────────────────────────────────────────────────────────
35
- const MANIFEST_VERSION = 1;
36
- // ─── Path helpers ─────────────────────────────────────────────────────────────
37
- function manifestPath(agent, version) {
38
- return path.join(getVersionsDir(), agent, version, 'home', '.sync-manifest.json');
39
- }
40
- // ─── Fingerprinting ───────────────────────────────────────────────────────────
41
- function sha256(content) {
42
- return crypto.createHash('sha256').update(content).digest('hex');
43
- }
44
- /** Compute fingerprint for a single file. Returns null if the file can't be read. */
45
- function fingerprintFile(filePath) {
46
- try {
47
- const stat = fs.statSync(filePath);
48
- const content = fs.readFileSync(filePath, 'utf-8');
49
- return {
50
- path: filePath,
51
- mtime: stat.mtimeMs,
52
- size: stat.size,
53
- sha256: sha256(content),
54
- };
55
- }
56
- catch {
57
- return null;
58
- }
59
- }
60
- /**
61
- * Fingerprint all files in a directory recursively.
62
- * Returns sorted by absolute path so ordering is deterministic.
63
- */
64
- function fingerprintDir(dirPath) {
65
- const results = [];
66
- function walk(dir) {
67
- let entries;
68
- try {
69
- entries = fs.readdirSync(dir, { withFileTypes: true });
70
- }
71
- catch {
72
- return;
73
- }
74
- for (const entry of entries) {
75
- if (entry.name.startsWith('.'))
76
- continue;
77
- const full = path.join(dir, entry.name);
78
- if (entry.isDirectory()) {
79
- walk(full);
80
- }
81
- else if (entry.isFile()) {
82
- const fp = fingerprintFile(full);
83
- if (fp)
84
- results.push(fp);
85
- }
86
- }
87
- }
88
- walk(dirPath);
89
- results.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
90
- return results;
91
- }
92
- // ─── Staleness check ──────────────────────────────────────────────────────────
93
- /**
94
- * Check if a stored fingerprint is still valid for the file at its recorded path.
95
- * Tier 1: mtime+size match → clean (no read needed).
96
- * Tier 2: sha256 match → clean (mtime drifted, content same).
97
- * Path mismatch → immediately stale (scope changed).
98
- */
99
- function isFileStale(stored, currentPath) {
100
- if (stored.path !== currentPath)
101
- return true;
102
- try {
103
- const stat = fs.statSync(currentPath);
104
- if (stat.mtimeMs === stored.mtime && stat.size === stored.size)
105
- return false;
106
- return sha256(fs.readFileSync(currentPath, 'utf-8')) !== stored.sha256;
107
- }
108
- catch {
109
- return true; // file disappeared
110
- }
111
- }
112
- /**
113
- * Walk a directory recursively and return sorted absolute file paths.
114
- * No file reads — used for the hot-path check where we want stat-only speed.
115
- */
116
- function walkDirPaths(dirPath) {
117
- const results = [];
118
- function walk(dir) {
119
- let entries;
120
- try {
121
- entries = fs.readdirSync(dir, { withFileTypes: true });
122
- }
123
- catch {
124
- return;
125
- }
126
- for (const entry of entries) {
127
- if (entry.name.startsWith('.'))
128
- continue;
129
- const full = path.join(dir, entry.name);
130
- if (entry.isDirectory()) {
131
- walk(full);
132
- }
133
- else if (entry.isFile()) {
134
- results.push(full);
135
- }
136
- }
137
- }
138
- walk(dirPath);
139
- results.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
140
- return results;
141
- }
142
- /**
143
- * Check if a DirEntry is stale for the given source directory.
144
- * Hot path: stat only (no file reads) when mtime+size match.
145
- */
146
- function isDirStale(stored, currentDirPath) {
147
- if (stored.dirPath !== currentDirPath)
148
- return true;
149
- // Get current file list without reading content
150
- const currentPaths = walkDirPaths(currentDirPath);
151
- if (currentPaths.length !== stored.files.length)
152
- return true;
153
- for (let i = 0; i < currentPaths.length; i++) {
154
- const storedFp = stored.files[i];
155
- const currentPath = currentPaths[i];
156
- if (storedFp.path !== currentPath)
157
- return true; // added/removed/renamed
158
- // Tier 1: mtime+size stat only
159
- try {
160
- const stat = fs.statSync(currentPath);
161
- if (stat.mtimeMs === storedFp.mtime && stat.size === storedFp.size)
162
- continue;
163
- // Tier 2: sha256 (only on mismatch)
164
- if (sha256(fs.readFileSync(currentPath, 'utf-8')) !== storedFp.sha256)
165
- return true;
166
- }
167
- catch {
168
- return true;
169
- }
170
- }
171
- return false;
172
- }
173
- /** Compare sorted name sets. Returns true if they differ. */
174
- function nameSetDiffers(manifestKeys, available) {
175
- if (manifestKeys.length !== available.length)
176
- return true;
177
- const sorted = [...available].sort();
178
- const mSorted = [...manifestKeys].sort();
179
- return sorted.some((name, i) => name !== mSorted[i]);
180
- }
181
- // ─── Source resolution helpers ────────────────────────────────────────────────
182
- function resolveSkillDir(skill, cwd) {
183
- const projectDir = getProjectAgentsDir(cwd);
184
- const candidates = [
185
- projectDir ? path.join(projectDir, 'skills', skill) : null,
186
- path.join(getUserAgentsDir(), 'skills', skill),
187
- path.join(getSkillsDir(), skill),
188
- ...getEnabledExtraRepos().map(e => path.join(e.dir, 'skills', skill)),
189
- ];
190
- return candidates.find(p => p && fs.existsSync(p) && fs.statSync(p).isDirectory()) ?? null;
191
- }
192
- function resolveRuleFile(mem, cwd) {
193
- const projectDir = getProjectAgentsDir(cwd);
194
- const candidates = [
195
- projectDir ? safeJoin(path.join(projectDir, 'rules'), `${mem}.md`) : null,
196
- safeJoin(getUserRulesDir(), `${mem}.md`),
197
- safeJoin(getResolvedRulesDir(), `${mem}.md`),
198
- ...getEnabledExtraRepos().map(e => safeJoin(path.join(e.dir, 'rules'), `${mem}.md`)),
199
- ];
200
- return candidates.find(p => {
201
- if (!p)
202
- return false;
203
- try {
204
- return !fs.lstatSync(p).isSymbolicLink() && fs.existsSync(p);
205
- }
206
- catch {
207
- return false;
208
- }
209
- }) ?? null;
210
- }
211
- /** Collect all permission group files across all scopes (first-wins per name). */
212
- function collectPermissionGroupFiles() {
213
- const seen = new Map(); // name → filePath (first-wins: user > system)
214
- for (const baseDir of [getUserPermissionsDir(), getPermissionsDir()]) {
215
- const groupsDir = path.join(baseDir, 'groups');
216
- if (!fs.existsSync(groupsDir))
217
- continue;
218
- let entries;
219
- try {
220
- entries = fs.readdirSync(groupsDir, { withFileTypes: true });
221
- }
222
- catch {
223
- continue;
224
- }
225
- for (const entry of entries) {
226
- if (!entry.isFile())
227
- continue;
228
- if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.yml'))
229
- continue;
230
- const name = entry.name.replace(/\.(yaml|yml)$/, '');
231
- if (!seen.has(name))
232
- seen.set(name, path.join(groupsDir, entry.name));
233
- }
234
- }
235
- return Object.fromEntries(seen);
236
- }
237
- // ─── Public API ───────────────────────────────────────────────────────────────
238
- /** Load the manifest for a given agent+version. Returns null on miss or parse error. */
239
- export function loadSyncManifest(agent, version) {
240
- const p = manifestPath(agent, version);
241
- try {
242
- const raw = JSON.parse(fs.readFileSync(p, 'utf-8'));
243
- if (raw.v !== MANIFEST_VERSION)
244
- return null;
245
- return raw;
246
- }
247
- catch {
248
- return null;
249
- }
250
- }
251
- /** Write the manifest atomically (tmp + rename). */
252
- export function saveSyncManifest(agent, version, manifest) {
253
- const p = manifestPath(agent, version);
254
- const tmp = p + '.tmp';
255
- try {
256
- fs.mkdirSync(path.dirname(p), { recursive: true });
257
- fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
258
- fs.renameSync(tmp, p);
259
- }
260
- catch {
261
- try {
262
- fs.unlinkSync(tmp);
263
- }
264
- catch { /* ignore */ }
265
- }
266
- }
267
- /**
268
- * Build a manifest by fingerprinting current source state.
269
- * Call this after a successful full sync.
270
- */
271
- export function buildManifest(agent, version, available, cwd) {
272
- const commands = {};
273
- for (const name of available.commands) {
274
- const resolved = resolveResource('commands', `${name}.md`, cwd);
275
- if (!resolved)
276
- continue;
277
- const fp = fingerprintFile(resolved.path);
278
- if (fp)
279
- commands[name] = { source: fp };
280
- }
281
- const skills = {};
282
- for (const name of available.skills) {
283
- const dirPath = resolveSkillDir(name, cwd);
284
- if (!dirPath)
285
- continue;
286
- skills[name] = { dirPath, files: fingerprintDir(dirPath) };
287
- }
288
- const hooks = {};
289
- for (const name of available.hooks) {
290
- // Hooks: resolve winning source manually (project > user > system > extra)
291
- const projectDir = getProjectAgentsDir(cwd);
292
- const candidates = [
293
- projectDir ? path.join(projectDir, 'hooks', name) : null,
294
- path.join(getUserHooksDir(), name),
295
- path.join(getHooksDir(), name),
296
- ...getEnabledExtraRepos().map(e => path.join(e.dir, 'hooks', name)),
297
- ];
298
- const hookPath = candidates.find(p => p && fs.existsSync(p)) ?? null;
299
- if (!hookPath)
300
- continue;
301
- const fp = fingerprintFile(hookPath);
302
- if (fp)
303
- hooks[name] = { source: fp };
304
- }
305
- const ruleFiles = {};
306
- for (const name of available.memory) {
307
- const srcPath = resolveRuleFile(name, cwd);
308
- if (!srcPath)
309
- continue;
310
- const fp = fingerprintFile(srcPath);
311
- if (fp)
312
- ruleFiles[name] = { source: fp };
313
- }
314
- const mcp = {};
315
- for (const server of listMcpServerConfigs(cwd)) {
316
- const fp = fingerprintFile(server.path);
317
- if (fp)
318
- mcp[server.name] = { source: fp };
319
- }
320
- const groupFiles = collectPermissionGroupFiles();
321
- const permGroups = {};
322
- for (const [name, filePath] of Object.entries(groupFiles)) {
323
- const fp = fingerprintFile(filePath);
324
- if (fp)
325
- permGroups[name] = { source: fp };
326
- }
327
- // Subagents: user > system, first-wins per name (mirrors listInstalledSubagents())
328
- const subagents = {};
329
- for (const sub of listInstalledSubagents()) {
330
- if (!available.subagents.includes(sub.name))
331
- continue;
332
- subagents[sub.name] = { dirPath: sub.path, files: fingerprintDir(sub.path) };
333
- }
334
- return {
335
- v: MANIFEST_VERSION,
336
- syncedAt: new Date().toISOString(),
337
- commands,
338
- skills,
339
- hooks,
340
- rules: { files: ruleFiles },
341
- mcp,
342
- permissions: {
343
- groups: permGroups,
344
- permissionPreset: getActivePermissionPresetName(),
345
- },
346
- subagents,
347
- };
348
- }
349
- /**
350
- * Check if sources have changed since the manifest was written.
351
- * Returns true (stale) at the first detected mismatch — no need to scan everything.
352
- * Returns false (clean) only after all checks pass.
353
- *
354
- * For rules, also delegates to isRulesStale() to catch @-import changes
355
- * for agents that pre-compile their rules file.
356
- */
357
- export function isSyncStale(manifest, available, agent, version, cwd) {
358
- // ── Commands ──────────────────────────────────────────────────────────────
359
- if (nameSetDiffers(Object.keys(manifest.commands), available.commands))
360
- return true;
361
- for (const name of available.commands) {
362
- const resolved = resolveResource('commands', `${name}.md`, cwd);
363
- if (!resolved)
364
- return true;
365
- const entry = manifest.commands[name];
366
- if (!entry || isFileStale(entry.source, resolved.path))
367
- return true;
368
- }
369
- // ── Skills ────────────────────────────────────────────────────────────────
370
- if (nameSetDiffers(Object.keys(manifest.skills), available.skills))
371
- return true;
372
- for (const name of available.skills) {
373
- const dirPath = resolveSkillDir(name, cwd);
374
- if (!dirPath)
375
- return true;
376
- const entry = manifest.skills[name];
377
- if (!entry || isDirStale(entry, dirPath))
378
- return true;
379
- }
380
- // ── Hooks ─────────────────────────────────────────────────────────────────
381
- if (nameSetDiffers(Object.keys(manifest.hooks), available.hooks))
382
- return true;
383
- for (const name of available.hooks) {
384
- const projectDir = getProjectAgentsDir(cwd);
385
- const candidates = [
386
- projectDir ? path.join(projectDir, 'hooks', name) : null,
387
- path.join(getUserHooksDir(), name),
388
- path.join(getHooksDir(), name),
389
- ...getEnabledExtraRepos().map(e => path.join(e.dir, 'hooks', name)),
390
- ];
391
- const hookPath = candidates.find(p => p && fs.existsSync(p)) ?? null;
392
- if (!hookPath)
393
- return true;
394
- const entry = manifest.hooks[name];
395
- if (!entry || isFileStale(entry.source, hookPath))
396
- return true;
397
- }
398
- // ── Rules ─────────────────────────────────────────────────────────────────
399
- if (nameSetDiffers(Object.keys(manifest.rules.files), available.memory))
400
- return true;
401
- for (const name of available.memory) {
402
- const srcPath = resolveRuleFile(name, cwd);
403
- if (!srcPath)
404
- return true;
405
- const entry = manifest.rules.files[name];
406
- if (!entry || isFileStale(entry.source, srcPath))
407
- return true;
408
- }
409
- // Also catch @-import changes for non-native-import agents
410
- if (isRulesStale(agent, version))
411
- return true;
412
- // ── MCP ───────────────────────────────────────────────────────────────────
413
- const mcpServers = listMcpServerConfigs(cwd);
414
- const mcpNames = mcpServers.map(s => s.name);
415
- if (nameSetDiffers(Object.keys(manifest.mcp), mcpNames))
416
- return true;
417
- for (const server of mcpServers) {
418
- const entry = manifest.mcp[server.name];
419
- if (!entry || isFileStale(entry.source, server.path))
420
- return true;
421
- }
422
- // ── Permissions ───────────────────────────────────────────────────────────
423
- if (manifest.permissions.permissionPreset !== getActivePermissionPresetName())
424
- return true;
425
- const currentGroups = collectPermissionGroupFiles();
426
- if (nameSetDiffers(Object.keys(manifest.permissions.groups), Object.keys(currentGroups)))
427
- return true;
428
- for (const [name, filePath] of Object.entries(currentGroups)) {
429
- const entry = manifest.permissions.groups[name];
430
- if (!entry || isFileStale(entry.source, filePath))
431
- return true;
432
- }
433
- // ── Subagents ─────────────────────────────────────────────────────────────
434
- const storedSubagents = manifest.subagents ?? {};
435
- if (nameSetDiffers(Object.keys(storedSubagents), available.subagents))
436
- return true;
437
- if (available.subagents.length > 0) {
438
- const allSubagents = listInstalledSubagents();
439
- const subagentMap = new Map(allSubagents.map(s => [s.name, s]));
440
- for (const name of available.subagents) {
441
- const sub = subagentMap.get(name);
442
- if (!sub)
443
- return true;
444
- const entry = storedSubagents[name];
445
- if (!entry || isDirStale(entry, sub.path))
446
- return true;
447
- }
448
- }
449
- return false;
450
- }