@phnx-labs/agents-cli 1.18.0 → 1.18.2
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/CHANGELOG.md +20 -1
- package/dist/commands/doctor.js +19 -5
- package/dist/commands/exec.js +9 -4
- package/dist/index.js +30 -0
- package/dist/lib/hooks.js +21 -3
- package/dist/lib/migrate.js +35 -12
- package/dist/lib/shims.d.ts +3 -1
- package/dist/lib/shims.js +81 -7
- package/dist/lib/staleness/checkers/commands.d.ts +7 -0
- package/dist/lib/staleness/checkers/commands.js +27 -0
- package/dist/lib/staleness/checkers/hooks.d.ts +13 -0
- package/dist/lib/staleness/checkers/hooks.js +63 -0
- package/dist/lib/staleness/checkers/mcp.d.ts +12 -0
- package/dist/lib/staleness/checkers/mcp.js +38 -0
- package/dist/lib/staleness/checkers/permissions.d.ts +17 -0
- package/dist/lib/staleness/checkers/permissions.js +73 -0
- package/dist/lib/staleness/checkers/plugins.d.ts +11 -0
- package/dist/lib/staleness/checkers/plugins.js +39 -0
- package/dist/lib/staleness/checkers/rules.d.ts +19 -0
- package/dist/lib/staleness/checkers/rules.js +86 -0
- package/dist/lib/staleness/checkers/skills.d.ts +7 -0
- package/dist/lib/staleness/checkers/skills.js +34 -0
- package/dist/lib/staleness/checkers/subagents.d.ts +12 -0
- package/dist/lib/staleness/checkers/subagents.js +39 -0
- package/dist/lib/staleness/checkers/types.d.ts +44 -0
- package/dist/lib/staleness/checkers/types.js +20 -0
- package/dist/lib/staleness/checkers/workflows.d.ts +10 -0
- package/dist/lib/staleness/checkers/workflows.js +37 -0
- package/dist/lib/staleness/fingerprint.d.ts +38 -0
- package/dist/lib/staleness/fingerprint.js +154 -0
- package/dist/lib/staleness/index.d.ts +26 -0
- package/dist/lib/staleness/index.js +122 -0
- package/dist/lib/staleness/layers.d.ts +37 -0
- package/dist/lib/staleness/layers.js +100 -0
- package/dist/lib/staleness/types.d.ts +56 -0
- package/dist/lib/staleness/types.js +6 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/teams/agents.d.ts +11 -20
- package/dist/lib/teams/agents.js +55 -202
- package/dist/lib/teams/index.d.ts +3 -2
- package/dist/lib/teams/index.js +2 -2
- package/dist/lib/teams/persistence.d.ts +0 -38
- package/dist/lib/teams/persistence.js +7 -329
- package/dist/lib/teams/registry.js +7 -5
- package/dist/lib/versions.js +34 -12
- package/package.json +1 -1
- package/dist/lib/sync-manifest.d.ts +0 -81
- package/dist/lib/sync-manifest.js +0 -450
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.18.2
|
|
4
|
+
|
|
5
|
+
**Teams**
|
|
6
|
+
|
|
7
|
+
- Dropped `~/.agents/teams/config.json` entirely. It duplicated information agents-cli already has — agent commands, enabled flags, model defaults, provider endpoints — none of which the team runner was actually reading. Teams now discover agents via `listInstalledVersions()` (the same source `agents view` uses) and invoke them via the canonical `agents run` subcommand. One spawn path, one canonical exec module (`src/lib/exec.ts`). The deprecated `AGENT_COMMANDS`, `applyEditMode`, `applyFullMode`, `readConfig`, `writeConfig`, `setAgentEnabled`, `AgentConfig`, `SwarmConfig`, `ProviderConfig`, `ModelOverrides`, `ReadConfigResult`, and `EffortLevel` (the persistence-module copy) exports are removed from `@phnx-labs/agents-cli/teams`. Migration deletes both `~/.agents/teams/config.json` and the legacy `~/.agents/config.json`.
|
|
8
|
+
- `~/.agents/teams/registry.json` moves to `~/.agents/.history/teams/registry.json` — it's per-machine runtime state (timestamps + absolute worktree paths) and shouldn't be synced across machines via `agents repo push`.
|
|
9
|
+
- New `agents run --quiet` flag suppresses the rotation banner and `Running: …` preamble lines. Used by the team runner so stream-json events reach the parser without non-JSON preamble.
|
|
10
|
+
|
|
11
|
+
**Dev builds**
|
|
12
|
+
|
|
13
|
+
- The CLI auto-detects dev builds (version stamped `0.0.0-dev.<sha>` by `scripts/install.sh`, or invoked from a working tree where `<cli-dir>/../.git/` exists) and defaults `AGENTS_NO_AUTOPULL=1`, `AGENTS_SKIP_MIGRATION=1`, and `AGENTS_CLI_DISABLE_AUTO_UPDATE=1`. No more typing those three env vars on every iteration. Production installs (registry global, no `.git/` at package root) are unaffected.
|
|
14
|
+
|
|
15
|
+
## 1.18.1
|
|
16
|
+
|
|
17
|
+
**Fixes**
|
|
18
|
+
|
|
19
|
+
- `scripts/build.sh` now sets mode `0o755` on every file declared in `package.json#bin` after `tsc` emits dist/. Newer npm versions preserve file mode from the published tarball and do NOT auto-chmod the bin target during `npm install -g`, so 1.18.0 shipped with mode-644 entrypoints. Users hit `zsh: permission denied: agents` after auto-update. Re-install to recover: `npm install -g @phnx-labs/agents-cli@latest`.
|
|
20
|
+
- New `scripts/install.sh` builds the working tree as a side-by-side dev install at `$HOME/.local/agents-cli-dev/`, symlinked into `$HOME/.local/bin/agents`. The registry install is never touched — `agents --version` shows `0.0.0-dev.<sha>[-dirty]` when the dev build is on PATH.
|
|
21
|
+
|
|
22
|
+
## 1.18.0
|
|
4
23
|
|
|
5
24
|
**Plugins**
|
|
6
25
|
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { checkAllClis } from '../lib/teams/agents.js';
|
|
3
3
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../lib/agents.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
|
|
5
|
+
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
6
6
|
import { diffVersionCommands, iterCommandsCapableVersions } from '../lib/commands.js';
|
|
7
7
|
import { diffVersionSkills, iterSkillsCapableVersions } from '../lib/skills.js';
|
|
8
8
|
import { diffVersionHooks, iterHooksCapableVersions } from '../lib/hooks.js';
|
|
@@ -17,13 +17,12 @@ function checkSyncStatus(cwd) {
|
|
|
17
17
|
const version = getGlobalDefault(agent);
|
|
18
18
|
if (!version)
|
|
19
19
|
continue;
|
|
20
|
-
const manifest =
|
|
20
|
+
const manifest = loadManifest(agent, version);
|
|
21
21
|
if (!manifest) {
|
|
22
22
|
rows.push({ agent, version, status: 'never-synced' });
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
25
|
-
const
|
|
26
|
-
const stale = isSyncStale(manifest, available, agent, version, cwd);
|
|
25
|
+
const stale = isStale(manifest, agent, version, cwd);
|
|
27
26
|
rows.push({ agent, version, status: stale ? 'stale' : 'fresh' });
|
|
28
27
|
}
|
|
29
28
|
return rows;
|
|
@@ -270,6 +269,21 @@ function renderTargetText(report, options) {
|
|
|
270
269
|
: null,
|
|
271
270
|
].filter(Boolean).join(' ');
|
|
272
271
|
console.log(chalk.gray(` layers: ${layerStr}`));
|
|
272
|
+
// Staleness manifest verdict — single-line summary from the staleness
|
|
273
|
+
// library, sitting alongside the detailed per-resource diff below.
|
|
274
|
+
const manifest = loadManifest(report.agent, report.version);
|
|
275
|
+
if (!manifest) {
|
|
276
|
+
console.log(chalk.gray(` manifest: ${chalk.gray('cold')} (never synced)`));
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const stale = isStale(manifest, report.agent, report.version, report.cwd);
|
|
280
|
+
if (stale) {
|
|
281
|
+
console.log(chalk.gray(' manifest: ') + chalk.yellow('stale') + chalk.gray(' (sources changed since last sync)'));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.log(chalk.gray(' manifest: ') + chalk.green('fresh'));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
273
287
|
console.log();
|
|
274
288
|
for (const kind of DOCTOR_ALL_KINDS) {
|
|
275
289
|
const rows = report.kinds[kind];
|
package/dist/commands/exec.js
CHANGED
|
@@ -51,6 +51,7 @@ export function registerRunCommand(program) {
|
|
|
51
51
|
.option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
|
|
52
52
|
.option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
|
|
53
53
|
.option('--json', 'Stream events as JSON lines (for parsing by other tools)')
|
|
54
|
+
.option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
|
|
54
55
|
.option('--headless', 'Non-interactive mode (auto-enabled when prompt provided)', false)
|
|
55
56
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided')
|
|
56
57
|
.option('--session-id <id>', 'Resume a previous conversation (Claude only)')
|
|
@@ -249,17 +250,19 @@ Examples:
|
|
|
249
250
|
const resolved = await resolveRunVersion(agent, strategy, cwd);
|
|
250
251
|
if (resolved.version) {
|
|
251
252
|
version = resolved.version;
|
|
252
|
-
if (resolved.rotation) {
|
|
253
|
+
if (resolved.rotation && !options.quiet) {
|
|
253
254
|
const banner = formatRotationBanner(resolved.rotation, strategy);
|
|
254
255
|
process.stderr.write(chalk.gray(banner + '\n'));
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
|
-
else {
|
|
258
|
+
else if (!options.quiet) {
|
|
258
259
|
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} found no usable ${agent} version; falling back to defaults\n`));
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
catch (err) {
|
|
262
|
-
|
|
263
|
+
if (!options.quiet) {
|
|
264
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} skipped: ${err.message}\n`));
|
|
265
|
+
}
|
|
263
266
|
}
|
|
264
267
|
}
|
|
265
268
|
}
|
|
@@ -388,7 +391,9 @@ Examples:
|
|
|
388
391
|
}
|
|
389
392
|
}
|
|
390
393
|
const cmd = buildExecCommand(execOptions);
|
|
391
|
-
|
|
394
|
+
if (!options.quiet) {
|
|
395
|
+
process.stderr.write(chalk.gray(`Running: ${cmd.join(' ')}\n\n`));
|
|
396
|
+
}
|
|
392
397
|
try {
|
|
393
398
|
let exitCode;
|
|
394
399
|
if (fallback.length > 0) {
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,36 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
23
23
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
24
24
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
25
25
|
const VERSION = packageJson.version;
|
|
26
|
+
// Detect dev/working-tree builds and default the noisy startup steps off.
|
|
27
|
+
// Three cases trip this:
|
|
28
|
+
// 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
|
|
29
|
+
// 2. Running `node dist/index.js` from a working tree — repo root has .git/
|
|
30
|
+
// 3. Running tsx/ts-node from src/ — also has .git/ at the repo root
|
|
31
|
+
// For all three: skip auto-pull (no network noise + no surprise FF on the
|
|
32
|
+
// system repo while iterating), skip migration (a buggy in-progress migration
|
|
33
|
+
// must not scribble on the user's real ~/.agents/), and skip the update prompt
|
|
34
|
+
// (the "0.0.0-dev -> 1.x.y" message is misleading). Each individual env var
|
|
35
|
+
// can still be set explicitly to override (set to '0' to re-enable).
|
|
36
|
+
const IS_DEV_BUILD = (() => {
|
|
37
|
+
if (VERSION.startsWith('0.0.0-dev'))
|
|
38
|
+
return true;
|
|
39
|
+
try {
|
|
40
|
+
const cliPath = process.argv[1] || '';
|
|
41
|
+
const repoRoot = path.dirname(path.dirname(cliPath));
|
|
42
|
+
return fs.existsSync(path.join(repoRoot, '.git'));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
if (IS_DEV_BUILD) {
|
|
49
|
+
if (process.env.AGENTS_NO_AUTOPULL === undefined)
|
|
50
|
+
process.env.AGENTS_NO_AUTOPULL = '1';
|
|
51
|
+
if (process.env.AGENTS_SKIP_MIGRATION === undefined)
|
|
52
|
+
process.env.AGENTS_SKIP_MIGRATION = '1';
|
|
53
|
+
if (process.env.AGENTS_CLI_DISABLE_AUTO_UPDATE === undefined)
|
|
54
|
+
process.env.AGENTS_CLI_DISABLE_AUTO_UPDATE = '1';
|
|
55
|
+
}
|
|
26
56
|
// Import command registrations
|
|
27
57
|
import { registerPullCommand } from './commands/pull.js';
|
|
28
58
|
import { registerRepoCommands } from './commands/repo.js';
|
package/dist/lib/hooks.js
CHANGED
|
@@ -51,6 +51,18 @@ function isManagedHookCommand(command, prefixes) {
|
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
53
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
54
|
+
/**
|
|
55
|
+
* Extensions that are NEVER hooks — docs, configuration, plain data. A file
|
|
56
|
+
* in hooks/ with one of these extensions is auxiliary content (e.g., the
|
|
57
|
+
* `promptcuts.yaml` data file read directly by the expand-promptcuts
|
|
58
|
+
* script, or the `README.md` that documents the hooks directory). They
|
|
59
|
+
* sometimes carry an exec bit by accident (older sync runs chmod 0o755'd
|
|
60
|
+
* everything) but they are not scripts.
|
|
61
|
+
*/
|
|
62
|
+
const NON_SCRIPT_EXTENSIONS = new Set([
|
|
63
|
+
'.md', '.markdown', '.rst', '.txt',
|
|
64
|
+
'.yaml', '.yml', '.json', '.toml', '.ini', '.conf',
|
|
65
|
+
]);
|
|
54
66
|
const SCRIPT_EXTENSIONS = new Set([
|
|
55
67
|
'.sh',
|
|
56
68
|
'.bash',
|
|
@@ -140,9 +152,15 @@ export function listHookEntriesFromDir(dir) {
|
|
|
140
152
|
const entries = [];
|
|
141
153
|
for (const [base, group] of grouped) {
|
|
142
154
|
group.sort((a, b) => a.name.localeCompare(b.name));
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
// A group is a hook only if it has an actual script: a script extension,
|
|
156
|
+
// OR an executable bit on a file whose extension is not a known data /
|
|
157
|
+
// docs type. Files like `README.md` (docs) or `promptcuts.yaml` (data
|
|
158
|
+
// the expand-promptcuts hook reads directly) sit alongside hooks but
|
|
159
|
+
// are NOT hooks themselves and must not surface in the hooks list
|
|
160
|
+
// anywhere — doctor, sync, view, or otherwise. Older sync runs may have
|
|
161
|
+
// chmod 0o755'd these files; an exec bit alone is not enough.
|
|
162
|
+
const script = group.find((f) => SCRIPT_EXTENSIONS.has(f.ext.toLowerCase())) ||
|
|
163
|
+
group.find((f) => f.isExec && !NON_SCRIPT_EXTENSIONS.has(f.ext.toLowerCase()));
|
|
146
164
|
if (!script)
|
|
147
165
|
continue;
|
|
148
166
|
const data = group.find((f) => f !== script);
|
package/dist/lib/migrate.js
CHANGED
|
@@ -58,14 +58,14 @@ function deleteSystemPromptsJson() {
|
|
|
58
58
|
* The teams persistence layer already reads the legacy path as a fallback;
|
|
59
59
|
* moving it here keeps the canonical location consistent.
|
|
60
60
|
*/
|
|
61
|
+
// Delete the legacy ~/.agents-system/config.json. This was the teams agent
|
|
62
|
+
// registry, which no longer exists — `agents teams` discovers agents through
|
|
63
|
+
// `listInstalledVersions` and invokes them through `agents run`.
|
|
61
64
|
function migrateSystemConfigJson() {
|
|
62
65
|
const src = path.join(SYSTEM_DIR, 'config.json');
|
|
63
|
-
|
|
64
|
-
if (!fs.existsSync(src) || fs.existsSync(dest))
|
|
66
|
+
if (!fs.existsSync(src))
|
|
65
67
|
return;
|
|
66
68
|
try {
|
|
67
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
68
|
-
fs.copyFileSync(src, dest);
|
|
69
69
|
fs.unlinkSync(src);
|
|
70
70
|
}
|
|
71
71
|
catch { /* best-effort */ }
|
|
@@ -375,20 +375,41 @@ function deleteUserPromptsJson() {
|
|
|
375
375
|
catch { /* best-effort */ }
|
|
376
376
|
}
|
|
377
377
|
/**
|
|
378
|
-
* Delete ~/.agents/config.json. The
|
|
379
|
-
*
|
|
380
|
-
*
|
|
378
|
+
* Delete ~/.agents/teams/config.json. The teams subsystem no longer carries
|
|
379
|
+
* its own agent registry — agent discovery flows through `listInstalledVersions`
|
|
380
|
+
* (the same source `agents view` uses) and invocation flows through
|
|
381
|
+
* `agents run`. The on-disk file is pure dead state on existing installs.
|
|
382
|
+
*/
|
|
383
|
+
function deleteTeamsConfigJson() {
|
|
384
|
+
const f = path.join(USER_DIR, 'teams', 'config.json');
|
|
385
|
+
if (!fs.existsSync(f))
|
|
386
|
+
return;
|
|
387
|
+
try {
|
|
388
|
+
fs.unlinkSync(f);
|
|
389
|
+
}
|
|
390
|
+
catch { /* best-effort */ }
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Move ~/.agents/teams/registry.json → ~/.agents/.history/teams/registry.json.
|
|
394
|
+
* The registry is per-machine runtime state (timestamps + absolute worktree
|
|
395
|
+
* paths) and belongs in the durable-runtime bucket, not at the user-root
|
|
396
|
+
* where `agents repo push` would sync it across machines.
|
|
397
|
+
*/
|
|
398
|
+
function moveTeamsRegistryToHistory() {
|
|
399
|
+
const src = path.join(USER_DIR, 'teams', 'registry.json');
|
|
400
|
+
const dest = path.join(HISTORY_DIR, 'teams', 'registry.json');
|
|
401
|
+
moveFileOnce(src, dest);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Delete ~/.agents/config.json. This was the legacy teams config location;
|
|
405
|
+
* the teams subsystem no longer carries a config file at all, so the legacy
|
|
406
|
+
* copy is simply removed.
|
|
381
407
|
*/
|
|
382
408
|
function cleanupUserConfigJson() {
|
|
383
409
|
const legacy = path.join(USER_DIR, 'config.json');
|
|
384
410
|
if (!fs.existsSync(legacy))
|
|
385
411
|
return;
|
|
386
|
-
const canonical = path.join(USER_DIR, 'teams', 'config.json');
|
|
387
412
|
try {
|
|
388
|
-
if (!fs.existsSync(canonical)) {
|
|
389
|
-
fs.mkdirSync(path.dirname(canonical), { recursive: true, mode: 0o700 });
|
|
390
|
-
fs.copyFileSync(legacy, canonical);
|
|
391
|
-
}
|
|
392
413
|
fs.unlinkSync(legacy);
|
|
393
414
|
}
|
|
394
415
|
catch { /* best-effort */ }
|
|
@@ -1455,6 +1476,8 @@ export async function runMigration() {
|
|
|
1455
1476
|
migratePermissionSetsToPresets();
|
|
1456
1477
|
deleteUserLinearJson();
|
|
1457
1478
|
deleteUserPromptsJson();
|
|
1479
|
+
deleteTeamsConfigJson();
|
|
1480
|
+
moveTeamsRegistryToHistory();
|
|
1458
1481
|
cleanupUserConfigJson();
|
|
1459
1482
|
cleanupEmptyTopLevelRuns();
|
|
1460
1483
|
foldUserHooksYamlIntoAgentsYaml();
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -50,8 +50,10 @@ export interface ConflictInfo {
|
|
|
50
50
|
* (two-repo split: system = shipped defaults, user = operational state).
|
|
51
51
|
* v9 — claude shim exports CLAUDE_CODE_OAUTH_TOKEN from per-version
|
|
52
52
|
* .oauth_token file on Linux (keychain-less sandbox fallback).
|
|
53
|
+
* v11 — when no default is set or the configured version is not installed,
|
|
54
|
+
* interactively propose the latest already-installed version.
|
|
53
55
|
*/
|
|
54
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
56
|
+
export declare const SHIM_SCHEMA_VERSION = 11;
|
|
55
57
|
/**
|
|
56
58
|
* Generate the full bash shim script for the given agent. The returned string
|
|
57
59
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
package/dist/lib/shims.js
CHANGED
|
@@ -173,8 +173,10 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
173
173
|
* (two-repo split: system = shipped defaults, user = operational state).
|
|
174
174
|
* v9 — claude shim exports CLAUDE_CODE_OAUTH_TOKEN from per-version
|
|
175
175
|
* .oauth_token file on Linux (keychain-less sandbox fallback).
|
|
176
|
+
* v11 — when no default is set or the configured version is not installed,
|
|
177
|
+
* interactively propose the latest already-installed version.
|
|
176
178
|
*/
|
|
177
|
-
export const SHIM_SCHEMA_VERSION =
|
|
179
|
+
export const SHIM_SCHEMA_VERSION = 11;
|
|
178
180
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
179
181
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
180
182
|
/**
|
|
@@ -281,6 +283,31 @@ find_project_agents_dir() {
|
|
|
281
283
|
return 1
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
# Find the latest installed version by numeric component comparison.
|
|
287
|
+
# Handles both semver (2.1.138) and date-based (2026.5.7) version strings.
|
|
288
|
+
find_latest_installed() {
|
|
289
|
+
local versions_dir="$AGENTS_USER_DIR/.history/versions/$AGENT"
|
|
290
|
+
[ -d "$versions_dir" ] || return
|
|
291
|
+
ls "$versions_dir" 2>/dev/null | awk '
|
|
292
|
+
BEGIN { best="" }
|
|
293
|
+
{
|
|
294
|
+
cur = $0
|
|
295
|
+
n = split(cur, a, /[^0-9]+/)
|
|
296
|
+
m = split(best, b, /[^0-9]+/)
|
|
297
|
+
maxn = (n > m) ? n : m
|
|
298
|
+
winner = cur
|
|
299
|
+
for (i=1; i<=maxn; i++) {
|
|
300
|
+
ai = (i<=n) ? a[i]+0 : 0
|
|
301
|
+
bi = (i<=m) ? b[i]+0 : 0
|
|
302
|
+
if (ai > bi) { winner=cur; break }
|
|
303
|
+
if (ai < bi) { winner=best; break }
|
|
304
|
+
}
|
|
305
|
+
best = winner
|
|
306
|
+
}
|
|
307
|
+
END { print best }
|
|
308
|
+
'
|
|
309
|
+
}
|
|
310
|
+
|
|
284
311
|
# Try project version first, then global default
|
|
285
312
|
VERSION=$(find_project_version)
|
|
286
313
|
VERSION_SOURCE="project"
|
|
@@ -290,9 +317,32 @@ if [ -z "$VERSION" ]; then
|
|
|
290
317
|
fi
|
|
291
318
|
|
|
292
319
|
if [ -z "$VERSION" ]; then
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
LATEST=$(find_latest_installed)
|
|
321
|
+
if [ -n "$LATEST" ]; then
|
|
322
|
+
echo "agents: no default set for $AGENT — found $AGENT@$LATEST installed" >&2
|
|
323
|
+
if [ -t 2 ]; then
|
|
324
|
+
printf " Set as default and continue? [Y/n] " >&2
|
|
325
|
+
read -r _ans </dev/tty
|
|
326
|
+
case "$_ans" in
|
|
327
|
+
""|y|Y)
|
|
328
|
+
agents use "$AGENT" "$LATEST" >/dev/null 2>&1
|
|
329
|
+
VERSION="$LATEST"
|
|
330
|
+
VERSION_SOURCE="default"
|
|
331
|
+
;;
|
|
332
|
+
*)
|
|
333
|
+
echo " Run: agents use $AGENT <version>" >&2
|
|
334
|
+
exit 1
|
|
335
|
+
;;
|
|
336
|
+
esac
|
|
337
|
+
else
|
|
338
|
+
echo " Run: agents use $AGENT <version>" >&2
|
|
339
|
+
exit 1
|
|
340
|
+
fi
|
|
341
|
+
else
|
|
342
|
+
echo "agents: no version of $AGENT configured" >&2
|
|
343
|
+
echo " Run: agents add $AGENT@<version>" >&2
|
|
344
|
+
exit 1
|
|
345
|
+
fi
|
|
296
346
|
fi
|
|
297
347
|
|
|
298
348
|
VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
|
|
@@ -329,9 +379,33 @@ if [ ! -x "$BINARY" ]; then
|
|
|
329
379
|
exit 1
|
|
330
380
|
fi
|
|
331
381
|
else
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
382
|
+
LATEST=$(find_latest_installed)
|
|
383
|
+
if [ -n "$LATEST" ] && [ "$LATEST" != "$VERSION" ]; then
|
|
384
|
+
echo "agents: $AGENT@$VERSION not installed — found $AGENT@$LATEST installed" >&2
|
|
385
|
+
if [ -t 2 ]; then
|
|
386
|
+
printf " Switch default to $AGENT@$LATEST and continue? [Y/n] " >&2
|
|
387
|
+
read -r _ans </dev/tty
|
|
388
|
+
case "$_ans" in
|
|
389
|
+
""|y|Y)
|
|
390
|
+
agents use "$AGENT" "$LATEST" >/dev/null 2>&1
|
|
391
|
+
VERSION="$LATEST"
|
|
392
|
+
VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
|
|
393
|
+
BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
|
|
394
|
+
;;
|
|
395
|
+
*)
|
|
396
|
+
echo " Run: agents add $AGENT@$VERSION" >&2
|
|
397
|
+
exit 1
|
|
398
|
+
;;
|
|
399
|
+
esac
|
|
400
|
+
else
|
|
401
|
+
echo " Run: agents add $AGENT@$VERSION" >&2
|
|
402
|
+
exit 1
|
|
403
|
+
fi
|
|
404
|
+
else
|
|
405
|
+
echo "agents: $AGENT@$VERSION not installed" >&2
|
|
406
|
+
echo " Run: agents add $AGENT@$VERSION" >&2
|
|
407
|
+
exit 1
|
|
408
|
+
fi
|
|
335
409
|
fi
|
|
336
410
|
fi
|
|
337
411
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands staleness — one `.md` file per command, first-wins across
|
|
3
|
+
* project > user > system > extras.
|
|
4
|
+
*/
|
|
5
|
+
import type { FileEntry } from '../types.js';
|
|
6
|
+
import type { TypedResourceChecker } from './types.js';
|
|
7
|
+
export declare const commandsChecker: TypedResourceChecker<FileEntry>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands staleness — one `.md` file per command, first-wins across
|
|
3
|
+
* project > user > system > extras.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { firstWinsLayers, listAcrossLayers, resolveByName } from '../layers.js';
|
|
8
|
+
import { fingerprintFile, isFileStale } from '../fingerprint.js';
|
|
9
|
+
export const commandsChecker = {
|
|
10
|
+
type: 'commands',
|
|
11
|
+
listNames(cwd) {
|
|
12
|
+
return listAcrossLayers(firstWinsLayers(cwd), 'commands', (name) => name.endsWith('.md')).map((n) => n.replace(/\.md$/, ''));
|
|
13
|
+
},
|
|
14
|
+
build(name, cwd) {
|
|
15
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('commands', `${name}.md`), (p) => fs.existsSync(p));
|
|
16
|
+
if (!resolved)
|
|
17
|
+
return null;
|
|
18
|
+
const fp = fingerprintFile(resolved.path);
|
|
19
|
+
return fp ? { source: fp } : null;
|
|
20
|
+
},
|
|
21
|
+
isFresh(name, stored, cwd) {
|
|
22
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('commands', `${name}.md`), (p) => fs.existsSync(p));
|
|
23
|
+
if (!resolved)
|
|
24
|
+
return false;
|
|
25
|
+
return !isFileStale(stored.source, resolved.path);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks staleness — one executable file per hook. Project layer is EXCLUDED
|
|
3
|
+
* by design: a cloned public repo with `.agents/hooks/foo` must not plant a
|
|
4
|
+
* hook that fires next time the user runs an agent inside it (see
|
|
5
|
+
* `src/lib/versions.ts:1832-1836`). Only user + system + extras count.
|
|
6
|
+
*
|
|
7
|
+
* Auxiliary files (README.md, promptcuts.yaml) live in hooks/ but are not
|
|
8
|
+
* hooks — the executable bit on the source distinguishes them. This matches
|
|
9
|
+
* the filter in `getAvailableResources`.
|
|
10
|
+
*/
|
|
11
|
+
import type { FileEntry } from '../types.js';
|
|
12
|
+
import type { TypedResourceChecker } from './types.js';
|
|
13
|
+
export declare const hooksChecker: TypedResourceChecker<FileEntry>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks staleness — one executable file per hook. Project layer is EXCLUDED
|
|
3
|
+
* by design: a cloned public repo with `.agents/hooks/foo` must not plant a
|
|
4
|
+
* hook that fires next time the user runs an agent inside it (see
|
|
5
|
+
* `src/lib/versions.ts:1832-1836`). Only user + system + extras count.
|
|
6
|
+
*
|
|
7
|
+
* Auxiliary files (README.md, promptcuts.yaml) live in hooks/ but are not
|
|
8
|
+
* hooks — the executable bit on the source distinguishes them. This matches
|
|
9
|
+
* the filter in `getAvailableResources`.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { hookLayers, listAcrossLayers, resolveByName } from '../layers.js';
|
|
14
|
+
import { fingerprintFile, isFileStale } from '../fingerprint.js';
|
|
15
|
+
/** Extensions that are NEVER hooks — docs, configuration, plain data. */
|
|
16
|
+
const NON_SCRIPT_EXTENSIONS = new Set([
|
|
17
|
+
'.md', '.markdown', '.rst', '.txt',
|
|
18
|
+
'.yaml', '.yml', '.json', '.toml', '.ini', '.conf',
|
|
19
|
+
]);
|
|
20
|
+
/** Extensions that explicitly mark a file as a script regardless of exec bit. */
|
|
21
|
+
const SCRIPT_EXTENSIONS = new Set([
|
|
22
|
+
'.sh', '.bash', '.zsh',
|
|
23
|
+
'.py', '.js', '.ts', '.mjs', '.cjs',
|
|
24
|
+
'.rb', '.pl', '.ps1',
|
|
25
|
+
]);
|
|
26
|
+
function isHookScript(full) {
|
|
27
|
+
try {
|
|
28
|
+
const stat = fs.statSync(full);
|
|
29
|
+
if (!stat.isFile())
|
|
30
|
+
return false;
|
|
31
|
+
const ext = path.extname(full).toLowerCase();
|
|
32
|
+
if (SCRIPT_EXTENSIONS.has(ext))
|
|
33
|
+
return true;
|
|
34
|
+
// Otherwise require exec bit AND a non-data extension. Older sync runs
|
|
35
|
+
// chmod 0o755'd everything including `promptcuts.yaml` / `README.md`,
|
|
36
|
+
// so exec bit alone can't be trusted.
|
|
37
|
+
if ((stat.mode & 0o111) === 0)
|
|
38
|
+
return false;
|
|
39
|
+
return !NON_SCRIPT_EXTENSIONS.has(ext);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const hooksChecker = {
|
|
46
|
+
type: 'hooks',
|
|
47
|
+
listNames(_cwd) {
|
|
48
|
+
return listAcrossLayers(hookLayers(), 'hooks', (_, full) => isHookScript(full));
|
|
49
|
+
},
|
|
50
|
+
build(name, _cwd) {
|
|
51
|
+
const resolved = resolveByName(hookLayers(), path.join('hooks', name), isHookScript);
|
|
52
|
+
if (!resolved)
|
|
53
|
+
return null;
|
|
54
|
+
const fp = fingerprintFile(resolved.path);
|
|
55
|
+
return fp ? { source: fp } : null;
|
|
56
|
+
},
|
|
57
|
+
isFresh(name, stored, _cwd) {
|
|
58
|
+
const resolved = resolveByName(hookLayers(), path.join('hooks', name), isHookScript);
|
|
59
|
+
if (!resolved)
|
|
60
|
+
return false;
|
|
61
|
+
return !isFileStale(stored.source, resolved.path);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server staleness — one `.yaml`/`.yml` file per server, first-wins
|
|
3
|
+
* across project > user > system > extras. Name is the `name:` field inside
|
|
4
|
+
* the YAML, NOT the filename (per `getAvailableResources`).
|
|
5
|
+
*
|
|
6
|
+
* We delegate name/path discovery to `listMcpServerConfigs(cwd)` which
|
|
7
|
+
* already handles parsing — keeps a single source of truth for "what counts
|
|
8
|
+
* as a discoverable MCP server."
|
|
9
|
+
*/
|
|
10
|
+
import type { FileEntry } from '../types.js';
|
|
11
|
+
import type { TypedResourceChecker } from './types.js';
|
|
12
|
+
export declare const mcpChecker: TypedResourceChecker<FileEntry>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server staleness — one `.yaml`/`.yml` file per server, first-wins
|
|
3
|
+
* across project > user > system > extras. Name is the `name:` field inside
|
|
4
|
+
* the YAML, NOT the filename (per `getAvailableResources`).
|
|
5
|
+
*
|
|
6
|
+
* We delegate name/path discovery to `listMcpServerConfigs(cwd)` which
|
|
7
|
+
* already handles parsing — keeps a single source of truth for "what counts
|
|
8
|
+
* as a discoverable MCP server."
|
|
9
|
+
*/
|
|
10
|
+
import { fingerprintFile, isFileStale } from '../fingerprint.js';
|
|
11
|
+
import { listMcpServerConfigs } from '../../mcp.js';
|
|
12
|
+
function indexByName(cwd) {
|
|
13
|
+
const map = new Map();
|
|
14
|
+
for (const cfg of listMcpServerConfigs(cwd)) {
|
|
15
|
+
if (!map.has(cfg.name))
|
|
16
|
+
map.set(cfg.name, cfg.path);
|
|
17
|
+
}
|
|
18
|
+
return map;
|
|
19
|
+
}
|
|
20
|
+
export const mcpChecker = {
|
|
21
|
+
type: 'mcp',
|
|
22
|
+
listNames(cwd) {
|
|
23
|
+
return Array.from(indexByName(cwd).keys());
|
|
24
|
+
},
|
|
25
|
+
build(name, cwd) {
|
|
26
|
+
const src = indexByName(cwd).get(name);
|
|
27
|
+
if (!src)
|
|
28
|
+
return null;
|
|
29
|
+
const fp = fingerprintFile(src);
|
|
30
|
+
return fp ? { source: fp } : null;
|
|
31
|
+
},
|
|
32
|
+
isFresh(name, stored, cwd) {
|
|
33
|
+
const src = indexByName(cwd).get(name);
|
|
34
|
+
if (!src)
|
|
35
|
+
return false;
|
|
36
|
+
return !isFileStale(stored.source, src);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissions staleness — every `groups/*.yaml` across user + system
|
|
3
|
+
* contributes to the merged permission set (project layer not consulted by
|
|
4
|
+
* the current sync writer). First-wins on name collision (user > system).
|
|
5
|
+
*
|
|
6
|
+
* The active preset env value (`AGENTS_PERMISSION_PRESET`) is part of the
|
|
7
|
+
* fingerprint too — preset selection changes which groups get applied to
|
|
8
|
+
* the agent config, so a preset switch without a content change still
|
|
9
|
+
* counts as stale.
|
|
10
|
+
*/
|
|
11
|
+
import type { PermEntry } from '../types.js';
|
|
12
|
+
/** Walk user + system permissions/groups/. First-wins user > system on names. */
|
|
13
|
+
export declare function collectPermissionGroupFiles(): Record<string, string>;
|
|
14
|
+
/** Build the permissions section of the manifest. */
|
|
15
|
+
export declare function buildPermissions(): PermEntry;
|
|
16
|
+
/** True when the stored permissions section no longer matches current state. */
|
|
17
|
+
export declare function isPermissionsStale(stored: PermEntry): boolean;
|