@phnx-labs/agents-cli 1.14.7 → 1.16.0
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 +78 -39
- package/README.md +74 -7
- package/dist/commands/alias.js +2 -2
- package/dist/commands/beta.js +6 -1
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +546 -75
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/exec.js +9 -2
- package/dist/commands/fork.js +2 -2
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +85 -43
- package/dist/commands/plugins.js +48 -15
- package/dist/commands/prune.d.ts +0 -20
- package/dist/commands/prune.js +291 -16
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +37 -1
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +32 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/teams.js +2 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/versions.js +8 -6
- package/dist/commands/view.js +61 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -20
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +29 -1
- package/dist/lib/browser/chrome.js +6 -3
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +9 -4
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +32 -4
- package/dist/lib/browser/ipc.js +145 -23
- package/dist/lib/browser/profiles.d.ts +5 -2
- package/dist/lib/browser/profiles.js +77 -37
- package/dist/lib/browser/service.d.ts +84 -13
- package/dist/lib/browser/service.js +806 -122
- package/dist/lib/browser/types.d.ts +81 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -0
- package/dist/lib/commands.js +6 -2
- package/dist/lib/daemon.js +6 -7
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.d.ts +94 -1
- package/dist/lib/events.js +264 -6
- package/dist/lib/exec.js +16 -10
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +125 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1178 -21
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +14 -11
- package/dist/lib/permissions.js +46 -42
- package/dist/lib/plugins.d.ts +30 -1
- package/dist/lib/plugins.js +75 -3
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/routines.d.ts +15 -0
- package/dist/lib/routines.js +68 -0
- package/dist/lib/runner.js +9 -5
- package/dist/lib/secrets/index.d.ts +14 -11
- package/dist/lib/secrets/index.js +49 -21
- package/dist/lib/secrets/linux.d.ts +27 -0
- package/dist/lib/secrets/linux.js +161 -0
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +34 -3
- package/dist/lib/session/discover.js +30 -15
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +6 -6
- package/dist/lib/skills.js +6 -2
- package/dist/lib/state.d.ts +86 -14
- package/dist/lib/state.js +150 -23
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +32 -3
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.d.ts +1 -1
- package/dist/lib/usage.js +15 -48
- package/dist/lib/versions.js +31 -21
- package/package.json +1 -1
- package/scripts/postinstall.js +37 -9
package/dist/lib/models.js
CHANGED
|
@@ -11,8 +11,8 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { execFileSync } from 'child_process';
|
|
13
13
|
import { getVersionDir } from './versions.js';
|
|
14
|
-
import {
|
|
15
|
-
const CACHE_PATH =
|
|
14
|
+
import { getModelsCachePath } from './state.js';
|
|
15
|
+
const CACHE_PATH = getModelsCachePath();
|
|
16
16
|
/**
|
|
17
17
|
* Bump when the extractor logic changes shape in an incompatible way so cached
|
|
18
18
|
* catalogs from older agents-cli builds are re-extracted.
|
|
@@ -42,26 +42,26 @@ export declare function discoverPermissionGroups(): PermissionGroupInfo[];
|
|
|
42
42
|
*/
|
|
43
43
|
export declare function getTotalPermissionRuleCount(): number;
|
|
44
44
|
/**
|
|
45
|
-
* A permission
|
|
46
|
-
* Lives at ~/.agents/permissions/
|
|
45
|
+
* A permission preset recipe — names a preset and lists which groups it composes.
|
|
46
|
+
* Lives at ~/.agents/permissions/presets/<name>.yaml.
|
|
47
47
|
*/
|
|
48
|
-
export interface
|
|
48
|
+
export interface PermissionPresetRecipe {
|
|
49
49
|
name: string;
|
|
50
50
|
description?: string;
|
|
51
51
|
includes: string[];
|
|
52
52
|
}
|
|
53
53
|
/** Env var that selects which set recipe to apply at sync time. */
|
|
54
|
-
export declare const
|
|
54
|
+
export declare const PERMISSION_PRESET_ENV_VAR = "AGENTS_PERMISSION_PRESET";
|
|
55
55
|
/**
|
|
56
|
-
* Read a permission
|
|
56
|
+
* Read a permission preset recipe by name from ~/.agents/permissions/presets/.
|
|
57
57
|
* Returns null if the recipe file is missing or malformed.
|
|
58
58
|
*/
|
|
59
|
-
export declare function
|
|
59
|
+
export declare function readPermissionPresetRecipe(name: string): PermissionPresetRecipe | null;
|
|
60
60
|
/**
|
|
61
|
-
* Return the active permission
|
|
61
|
+
* Return the active permission preset name from AGENTS_PERMISSION_PRESET env var,
|
|
62
62
|
* or null if unset. Caller decides the default behavior when null.
|
|
63
63
|
*/
|
|
64
|
-
export declare function
|
|
64
|
+
export declare function getActivePermissionPresetName(): string | null;
|
|
65
65
|
/**
|
|
66
66
|
* Build a PermissionSet from selected groups.
|
|
67
67
|
* Concatenates allow/deny rules from each group.
|
|
@@ -72,21 +72,24 @@ export declare function getActivePermissionSetName(): string | null;
|
|
|
72
72
|
export declare function buildPermissionsFromGroups(groupNames: string[]): PermissionSet;
|
|
73
73
|
/**
|
|
74
74
|
* List installed permission sets from central storage.
|
|
75
|
+
* User dir takes precedence; system entries are surfaced when user has no
|
|
76
|
+
* same-named override.
|
|
75
77
|
*/
|
|
76
78
|
export declare function listInstalledPermissions(): InstalledPermission[];
|
|
77
79
|
/**
|
|
78
|
-
* Get a specific permission set by name.
|
|
80
|
+
* Get a specific permission set by name. Searches user dir first, then system.
|
|
79
81
|
*/
|
|
80
82
|
export declare function getPermissionSet(name: string): InstalledPermission | null;
|
|
81
83
|
/**
|
|
82
|
-
* Install a permission set to central storage.
|
|
84
|
+
* Install a permission set to user-level central storage.
|
|
83
85
|
*/
|
|
84
86
|
export declare function installPermissionSet(sourcePath: string, name: string): {
|
|
85
87
|
success: boolean;
|
|
86
88
|
error?: string;
|
|
87
89
|
};
|
|
88
90
|
/**
|
|
89
|
-
* Remove a permission set from central storage.
|
|
91
|
+
* Remove a permission set from user-level central storage. System-shipped
|
|
92
|
+
* sets are intentionally not deletable from user commands.
|
|
90
93
|
*/
|
|
91
94
|
export declare function removePermissionSet(name: string): {
|
|
92
95
|
success: boolean;
|
package/dist/lib/permissions.js
CHANGED
|
@@ -159,15 +159,15 @@ export function getTotalPermissionRuleCount() {
|
|
|
159
159
|
return groups.reduce((sum, g) => sum + g.ruleCount, 0);
|
|
160
160
|
}
|
|
161
161
|
/** Env var that selects which set recipe to apply at sync time. */
|
|
162
|
-
export const
|
|
162
|
+
export const PERMISSION_PRESET_ENV_VAR = 'AGENTS_PERMISSION_PRESET';
|
|
163
163
|
/**
|
|
164
|
-
* Read a permission
|
|
164
|
+
* Read a permission preset recipe by name from ~/.agents/permissions/presets/.
|
|
165
165
|
* Returns null if the recipe file is missing or malformed.
|
|
166
166
|
*/
|
|
167
|
-
export function
|
|
168
|
-
const
|
|
167
|
+
export function readPermissionPresetRecipe(name) {
|
|
168
|
+
const presetsDir = path.join(getPermissionsDir(), 'presets');
|
|
169
169
|
for (const ext of ['.yaml', '.yml']) {
|
|
170
|
-
const filePath = safeJoin(
|
|
170
|
+
const filePath = safeJoin(presetsDir, name + ext);
|
|
171
171
|
if (!fs.existsSync(filePath))
|
|
172
172
|
continue;
|
|
173
173
|
try {
|
|
@@ -190,11 +190,11 @@ export function readPermissionSetRecipe(name) {
|
|
|
190
190
|
return null;
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
|
-
* Return the active permission
|
|
193
|
+
* Return the active permission preset name from AGENTS_PERMISSION_PRESET env var,
|
|
194
194
|
* or null if unset. Caller decides the default behavior when null.
|
|
195
195
|
*/
|
|
196
|
-
export function
|
|
197
|
-
const v = process.env[
|
|
196
|
+
export function getActivePermissionPresetName() {
|
|
197
|
+
const v = process.env[PERMISSION_PRESET_ENV_VAR];
|
|
198
198
|
return v && v.trim() ? v.trim() : null;
|
|
199
199
|
}
|
|
200
200
|
/**
|
|
@@ -259,55 +259,58 @@ export function buildPermissionsFromGroups(groupNames) {
|
|
|
259
259
|
}
|
|
260
260
|
/**
|
|
261
261
|
* List installed permission sets from central storage.
|
|
262
|
+
* User dir takes precedence; system entries are surfaced when user has no
|
|
263
|
+
* same-named override.
|
|
262
264
|
*/
|
|
263
265
|
export function listInstalledPermissions() {
|
|
264
266
|
ensureAgentsDir();
|
|
265
|
-
const
|
|
266
|
-
if (!fs.existsSync(dir)) {
|
|
267
|
-
return [];
|
|
268
|
-
}
|
|
267
|
+
const seen = new Set();
|
|
269
268
|
const results = [];
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
269
|
+
for (const dir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
270
|
+
if (!fs.existsSync(dir))
|
|
271
|
+
continue;
|
|
272
|
+
try {
|
|
273
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
if (!entry.isFile())
|
|
276
|
+
continue;
|
|
277
|
+
if (!entry.name.endsWith('.yml') && !entry.name.endsWith('.yaml'))
|
|
278
|
+
continue;
|
|
279
|
+
const filePath = path.join(dir, entry.name);
|
|
280
|
+
const set = parsePermissionSet(filePath);
|
|
281
|
+
if (!set)
|
|
282
|
+
continue;
|
|
283
|
+
if (seen.has(set.name))
|
|
284
|
+
continue;
|
|
285
|
+
seen.add(set.name);
|
|
286
|
+
results.push({ name: set.name, path: filePath, set });
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
catch {
|
|
290
|
+
// Skip inaccessible directory
|
|
291
|
+
}
|
|
290
292
|
}
|
|
291
293
|
return results;
|
|
292
294
|
}
|
|
293
295
|
/**
|
|
294
|
-
* Get a specific permission set by name.
|
|
296
|
+
* Get a specific permission set by name. Searches user dir first, then system.
|
|
295
297
|
*/
|
|
296
298
|
export function getPermissionSet(name) {
|
|
297
|
-
const dir
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
299
|
+
for (const dir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
300
|
+
for (const ext of ['.yml', '.yaml']) {
|
|
301
|
+
const filePath = safeJoin(dir, name + ext);
|
|
302
|
+
if (fs.existsSync(filePath)) {
|
|
303
|
+
const set = parsePermissionSet(filePath);
|
|
304
|
+
if (set) {
|
|
305
|
+
return { name: set.name, path: filePath, set };
|
|
306
|
+
}
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
309
|
}
|
|
307
310
|
return null;
|
|
308
311
|
}
|
|
309
312
|
/**
|
|
310
|
-
* Install a permission set to central storage.
|
|
313
|
+
* Install a permission set to user-level central storage.
|
|
311
314
|
*/
|
|
312
315
|
export function installPermissionSet(sourcePath, name) {
|
|
313
316
|
ensurePermissionsDir();
|
|
@@ -315,7 +318,7 @@ export function installPermissionSet(sourcePath, name) {
|
|
|
315
318
|
if (!set) {
|
|
316
319
|
return { success: false, error: 'Invalid permission file' };
|
|
317
320
|
}
|
|
318
|
-
const targetPath = safeJoin(
|
|
321
|
+
const targetPath = safeJoin(getUserPermissionsDir(), name + '.yml');
|
|
319
322
|
try {
|
|
320
323
|
fs.copyFileSync(sourcePath, targetPath);
|
|
321
324
|
return { success: true };
|
|
@@ -325,10 +328,11 @@ export function installPermissionSet(sourcePath, name) {
|
|
|
325
328
|
}
|
|
326
329
|
}
|
|
327
330
|
/**
|
|
328
|
-
* Remove a permission set from central storage.
|
|
331
|
+
* Remove a permission set from user-level central storage. System-shipped
|
|
332
|
+
* sets are intentionally not deletable from user commands.
|
|
329
333
|
*/
|
|
330
334
|
export function removePermissionSet(name) {
|
|
331
|
-
const dir =
|
|
335
|
+
const dir = getUserPermissionsDir();
|
|
332
336
|
for (const ext of ['.yml', '.yaml']) {
|
|
333
337
|
const filePath = safeJoin(dir, name + ext);
|
|
334
338
|
if (fs.existsSync(filePath)) {
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -73,7 +73,36 @@ export declare function removePluginFromVersion(pluginName: string, pluginRoot:
|
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
75
|
* Remove orphaned plugin skill directories from a version home.
|
|
76
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
76
77
|
* An orphan is a skill dir with the plugin prefix pattern (name--skill)
|
|
77
78
|
* where the plugin no longer exists in ~/.agents/plugins/.
|
|
78
79
|
*/
|
|
79
|
-
export declare function cleanOrphanedPluginSkills(agent: AgentId, versionHome: string, activePluginNames: Set<string
|
|
80
|
+
export declare function cleanOrphanedPluginSkills(agent: AgentId, versionHome: string, activePluginNames: Set<string>, version?: string): string[];
|
|
81
|
+
export interface VersionPluginDiff {
|
|
82
|
+
agent: AgentId;
|
|
83
|
+
version: string;
|
|
84
|
+
orphans: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compare a version home's plugin skills against discovered plugins.
|
|
88
|
+
* Returns orphan plugin skill names (pattern: pluginName--skillName).
|
|
89
|
+
*/
|
|
90
|
+
export declare function diffVersionPlugins(agent: AgentId, version: string): VersionPluginDiff;
|
|
91
|
+
/**
|
|
92
|
+
* Iterate all (agent, version) pairs that support plugins and are installed.
|
|
93
|
+
*/
|
|
94
|
+
export declare function iterPluginsCapableVersions(filter?: {
|
|
95
|
+
agent?: AgentId;
|
|
96
|
+
version?: string;
|
|
97
|
+
}): Array<{
|
|
98
|
+
agent: AgentId;
|
|
99
|
+
version: string;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Remove a single orphan plugin skill from a version home.
|
|
103
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
104
|
+
*/
|
|
105
|
+
export declare function removePluginSkillFromVersion(agent: AgentId, version: string, skillName: string): {
|
|
106
|
+
success: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
};
|
package/dist/lib/plugins.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
|
-
import { getPluginsDir } from './state.js';
|
|
10
|
+
import { getPluginsDir, getTrashPluginsDir } from './state.js';
|
|
11
|
+
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
11
12
|
import { AGENTS, PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
12
13
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
13
14
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
@@ -527,10 +528,11 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
527
528
|
}
|
|
528
529
|
/**
|
|
529
530
|
* Remove orphaned plugin skill directories from a version home.
|
|
531
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
530
532
|
* An orphan is a skill dir with the plugin prefix pattern (name--skill)
|
|
531
533
|
* where the plugin no longer exists in ~/.agents/plugins/.
|
|
532
534
|
*/
|
|
533
|
-
export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames) {
|
|
535
|
+
export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames, version) {
|
|
534
536
|
const removed = [];
|
|
535
537
|
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
536
538
|
if (!fs.existsSync(skillsDir))
|
|
@@ -546,7 +548,11 @@ export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames)
|
|
|
546
548
|
const pluginName = entry.name.slice(0, dashIdx);
|
|
547
549
|
if (!activePluginNames.has(pluginName)) {
|
|
548
550
|
try {
|
|
549
|
-
|
|
551
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
552
|
+
const trashDir = path.join(getTrashPluginsDir(), agent, version || 'unknown', entry.name);
|
|
553
|
+
const trashDest = path.join(trashDir, stamp);
|
|
554
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
555
|
+
fs.renameSync(path.join(skillsDir, entry.name), trashDest);
|
|
550
556
|
removed.push(entry.name);
|
|
551
557
|
}
|
|
552
558
|
catch {
|
|
@@ -556,3 +562,69 @@ export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames)
|
|
|
556
562
|
}
|
|
557
563
|
return removed;
|
|
558
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Compare a version home's plugin skills against discovered plugins.
|
|
567
|
+
* Returns orphan plugin skill names (pattern: pluginName--skillName).
|
|
568
|
+
*/
|
|
569
|
+
export function diffVersionPlugins(agent, version) {
|
|
570
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
571
|
+
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
572
|
+
const orphans = [];
|
|
573
|
+
if (!fs.existsSync(skillsDir)) {
|
|
574
|
+
return { agent, version, orphans };
|
|
575
|
+
}
|
|
576
|
+
const activePlugins = new Set(discoverPlugins().map(p => p.name));
|
|
577
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
578
|
+
for (const entry of entries) {
|
|
579
|
+
if (!entry.isDirectory())
|
|
580
|
+
continue;
|
|
581
|
+
const dashIdx = entry.name.indexOf('--');
|
|
582
|
+
if (dashIdx === -1)
|
|
583
|
+
continue;
|
|
584
|
+
const pluginName = entry.name.slice(0, dashIdx);
|
|
585
|
+
if (!activePlugins.has(pluginName)) {
|
|
586
|
+
orphans.push(entry.name);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return { agent, version, orphans: orphans.sort() };
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Iterate all (agent, version) pairs that support plugins and are installed.
|
|
593
|
+
*/
|
|
594
|
+
export function iterPluginsCapableVersions(filter) {
|
|
595
|
+
const pairs = [];
|
|
596
|
+
const agents = filter?.agent ? [filter.agent] : PLUGINS_CAPABLE_AGENTS;
|
|
597
|
+
for (const agent of agents) {
|
|
598
|
+
if (!PLUGINS_CAPABLE_AGENTS.includes(agent))
|
|
599
|
+
continue;
|
|
600
|
+
const versions = listInstalledVersions(agent);
|
|
601
|
+
for (const version of versions) {
|
|
602
|
+
if (filter?.version && filter.version !== version)
|
|
603
|
+
continue;
|
|
604
|
+
pairs.push({ agent, version });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return pairs;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Remove a single orphan plugin skill from a version home.
|
|
611
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
612
|
+
*/
|
|
613
|
+
export function removePluginSkillFromVersion(agent, version, skillName) {
|
|
614
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
615
|
+
const skillPath = path.join(versionHome, `.${agent}`, 'skills', skillName);
|
|
616
|
+
if (!fs.existsSync(skillPath)) {
|
|
617
|
+
return { success: true };
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
621
|
+
const trashDir = path.join(getTrashPluginsDir(), agent, version, skillName);
|
|
622
|
+
const trashDest = path.join(trashDir, stamp);
|
|
623
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
624
|
+
fs.renameSync(skillPath, trashDest);
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
return { success: false, error: err.message };
|
|
628
|
+
}
|
|
629
|
+
return { success: true };
|
|
630
|
+
}
|
package/dist/lib/pty-server.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as path from 'path';
|
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
15
|
import { execFileSync } from 'child_process';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
-
import {
|
|
17
|
+
import { getPtyDir as getPtyDirRoot } from './state.js';
|
|
18
18
|
/**
|
|
19
19
|
* Capture a stable identifier for a process at the moment it was started.
|
|
20
20
|
* Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
|
|
@@ -53,7 +53,6 @@ export function captureProcessStartTime(pid) {
|
|
|
53
53
|
}
|
|
54
54
|
// --- Constants ---
|
|
55
55
|
const SENTINEL = '__AGENTS_PTY_DONE__';
|
|
56
|
-
const PTY_DIR = 'helpers/pty';
|
|
57
56
|
const SOCKET_NAME = 'pty.sock';
|
|
58
57
|
const PID_FILE = 'pty.pid';
|
|
59
58
|
const LOG_FILE = 'logs.jsonl';
|
|
@@ -84,7 +83,7 @@ function buildPtyEnv() {
|
|
|
84
83
|
}
|
|
85
84
|
/** Get the PTY helper directory, creating it if needed. */
|
|
86
85
|
function getPtyDir() {
|
|
87
|
-
const dir =
|
|
86
|
+
const dir = getPtyDirRoot();
|
|
88
87
|
fs.mkdirSync(dir, { recursive: true });
|
|
89
88
|
return dir;
|
|
90
89
|
}
|
|
@@ -178,7 +177,7 @@ export async function runPtyServer() {
|
|
|
178
177
|
}
|
|
179
178
|
catch (err) {
|
|
180
179
|
console.error('node-pty is required for PTY support.');
|
|
181
|
-
console.error('Install: cd ' +
|
|
180
|
+
console.error('Install: cd ' + '~/agents-cli && bun add node-pty');
|
|
182
181
|
process.exit(1);
|
|
183
182
|
}
|
|
184
183
|
try {
|
|
@@ -188,7 +187,7 @@ export async function runPtyServer() {
|
|
|
188
187
|
}
|
|
189
188
|
catch {
|
|
190
189
|
console.error('@xterm/headless is required for PTY support.');
|
|
191
|
-
console.error('Install: cd ' +
|
|
190
|
+
console.error('Install: cd ' + '~/agents-cli && bun add @xterm/headless');
|
|
192
191
|
process.exit(1);
|
|
193
192
|
}
|
|
194
193
|
const sessions = new Map();
|
|
@@ -493,11 +492,11 @@ export async function runPtyServer() {
|
|
|
493
492
|
});
|
|
494
493
|
conn.on('error', () => { });
|
|
495
494
|
});
|
|
496
|
-
// Lock down
|
|
497
|
-
// user with execute on the parent dir could connect to the socket
|
|
498
|
-
// the listen()-to-chmod() window. macOS BSD AF_UNIX semantics make
|
|
499
|
-
// mode advisory only, so the parent dir is the real boundary.
|
|
500
|
-
const agentsDir =
|
|
495
|
+
// Lock down the PTY scratch dir before opening the socket — without this,
|
|
496
|
+
// any local user with execute on the parent dir could connect to the socket
|
|
497
|
+
// during the listen()-to-chmod() window. macOS BSD AF_UNIX semantics make
|
|
498
|
+
// socket mode advisory only, so the parent dir is the real boundary.
|
|
499
|
+
const agentsDir = getPtyDirRoot();
|
|
501
500
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
502
501
|
fs.chmodSync(agentsDir, 0o700);
|
|
503
502
|
// umask covers any inherited group/other bits while listen() is creating
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HooksHandler - ResourceHandler implementation for hooks.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Hook declarations live in:
|
|
5
|
+
* - System: ~/.agents-system/hooks.yaml (npm-shipped defaults)
|
|
6
|
+
* - User: `hooks:` section of ~/.agents/agents.yaml
|
|
7
|
+
* - Project: <project>/.agents/hooks.yaml
|
|
8
|
+
*
|
|
5
9
|
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
10
|
* Non-conflicting hooks from all layers are unioned together.
|
|
7
11
|
*/
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HooksHandler - ResourceHandler implementation for hooks.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Hook declarations live in:
|
|
5
|
+
* - System: ~/.agents-system/hooks.yaml (npm-shipped defaults)
|
|
6
|
+
* - User: `hooks:` section of ~/.agents/agents.yaml
|
|
7
|
+
* - Project: <project>/.agents/hooks.yaml
|
|
8
|
+
*
|
|
5
9
|
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
10
|
* Non-conflicting hooks from all layers are unioned together.
|
|
7
11
|
*/
|
|
@@ -10,13 +14,20 @@ import * as path from 'path';
|
|
|
10
14
|
import * as yaml from 'yaml';
|
|
11
15
|
import { getSystemAgentsDir, getUserAgentsDir, getProjectAgentsDir, } from '../state.js';
|
|
12
16
|
/**
|
|
13
|
-
* Get the
|
|
17
|
+
* Get the hook manifest path for a layer dir. The user layer reads from
|
|
18
|
+
* agents.yaml (hooks: section) since that's where user hooks now live.
|
|
19
|
+
* Other layers continue to use a standalone hooks.yaml.
|
|
14
20
|
*/
|
|
15
21
|
function getHooksYamlPath(layerDir) {
|
|
22
|
+
if (layerDir === getUserAgentsDir()) {
|
|
23
|
+
return path.join(layerDir, 'agents.yaml');
|
|
24
|
+
}
|
|
16
25
|
return path.join(layerDir, 'hooks.yaml');
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
|
-
* Parse hooks
|
|
28
|
+
* Parse hooks for a layer directory.
|
|
29
|
+
* - User layer: read `hooks:` section from agents.yaml.
|
|
30
|
+
* - System / project layer: read top-level map from hooks.yaml.
|
|
20
31
|
* Returns empty object if file doesn't exist or is invalid.
|
|
21
32
|
*/
|
|
22
33
|
function parseHooksYaml(dir) {
|
|
@@ -27,7 +38,13 @@ function parseHooksYaml(dir) {
|
|
|
27
38
|
try {
|
|
28
39
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
29
40
|
const parsed = yaml.parse(content);
|
|
30
|
-
|
|
41
|
+
if (!parsed)
|
|
42
|
+
return {};
|
|
43
|
+
if (dir === getUserAgentsDir()) {
|
|
44
|
+
const hooks = parsed.hooks;
|
|
45
|
+
return (hooks && typeof hooks === 'object') ? hooks : {};
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
31
48
|
}
|
|
32
49
|
catch {
|
|
33
50
|
return {};
|
package/dist/lib/rotate.js
CHANGED
|
@@ -8,12 +8,11 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import * as yaml from 'yaml';
|
|
10
10
|
import { getAccountInfo } from './agents.js';
|
|
11
|
-
import { readMeta, writeMeta,
|
|
11
|
+
import { readMeta, writeMeta, getHelpersDir, getUserAgentsDir } 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
14
|
function getRotateDir() {
|
|
16
|
-
const dir = path.join(
|
|
15
|
+
const dir = path.join(getHelpersDir(), 'rotate');
|
|
17
16
|
fs.mkdirSync(dir, { recursive: true });
|
|
18
17
|
return dir;
|
|
19
18
|
}
|
|
@@ -35,7 +34,7 @@ export function normalizeRunStrategy(value) {
|
|
|
35
34
|
/** Read project-local run strategy from the nearest agents.yaml, if present. */
|
|
36
35
|
export function getProjectRunStrategy(agent, startPath) {
|
|
37
36
|
let dir = path.resolve(startPath);
|
|
38
|
-
const userAgentsYaml = path.join(
|
|
37
|
+
const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
39
38
|
while (dir !== path.dirname(dir)) {
|
|
40
39
|
const manifestPath = path.join(dir, 'agents.yaml');
|
|
41
40
|
if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -96,3 +96,18 @@ export declare function installJobFromSource(sourcePath: string, name: string):
|
|
|
96
96
|
success: boolean;
|
|
97
97
|
error?: string;
|
|
98
98
|
};
|
|
99
|
+
/** List all job names that have run directories. */
|
|
100
|
+
export declare function listJobsWithRuns(): string[];
|
|
101
|
+
/** Count total runs across all jobs. */
|
|
102
|
+
export declare function countAllRuns(): number;
|
|
103
|
+
/** Preview runs that would be pruned (keeping only the most recent `keep` per job). */
|
|
104
|
+
export declare function previewRunsPrune(keep: number): Array<{
|
|
105
|
+
jobName: string;
|
|
106
|
+
runId: string;
|
|
107
|
+
startedAt: string;
|
|
108
|
+
}>;
|
|
109
|
+
/** Delete old runs, keeping only the most recent `keep` per job. Returns bytes freed and run count. */
|
|
110
|
+
export declare function pruneRuns(keep: number): {
|
|
111
|
+
deleted: number;
|
|
112
|
+
bytesFreed: number;
|
|
113
|
+
};
|
package/dist/lib/routines.js
CHANGED
|
@@ -350,3 +350,71 @@ export function installJobFromSource(sourcePath, name) {
|
|
|
350
350
|
return { success: false, error: err.message };
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
|
+
/** List all job names that have run directories. */
|
|
354
|
+
export function listJobsWithRuns() {
|
|
355
|
+
const runsDir = getRunsDir();
|
|
356
|
+
if (!fs.existsSync(runsDir))
|
|
357
|
+
return [];
|
|
358
|
+
return fs.readdirSync(runsDir, { withFileTypes: true })
|
|
359
|
+
.filter((e) => e.isDirectory())
|
|
360
|
+
.map((e) => e.name);
|
|
361
|
+
}
|
|
362
|
+
/** Count total runs across all jobs. */
|
|
363
|
+
export function countAllRuns() {
|
|
364
|
+
let total = 0;
|
|
365
|
+
for (const jobName of listJobsWithRuns()) {
|
|
366
|
+
total += listRuns(jobName).length;
|
|
367
|
+
}
|
|
368
|
+
return total;
|
|
369
|
+
}
|
|
370
|
+
/** Preview runs that would be pruned (keeping only the most recent `keep` per job). */
|
|
371
|
+
export function previewRunsPrune(keep) {
|
|
372
|
+
const toPrune = [];
|
|
373
|
+
for (const jobName of listJobsWithRuns()) {
|
|
374
|
+
const runs = listRuns(jobName);
|
|
375
|
+
if (runs.length > keep) {
|
|
376
|
+
const toRemove = runs.slice(0, runs.length - keep);
|
|
377
|
+
for (const run of toRemove) {
|
|
378
|
+
toPrune.push({ jobName, runId: run.runId, startedAt: run.startedAt });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return toPrune;
|
|
383
|
+
}
|
|
384
|
+
/** Delete old runs, keeping only the most recent `keep` per job. Returns bytes freed and run count. */
|
|
385
|
+
export function pruneRuns(keep) {
|
|
386
|
+
let deleted = 0;
|
|
387
|
+
let bytesFreed = 0;
|
|
388
|
+
for (const jobName of listJobsWithRuns()) {
|
|
389
|
+
const runs = listRuns(jobName);
|
|
390
|
+
if (runs.length <= keep)
|
|
391
|
+
continue;
|
|
392
|
+
const toRemove = runs.slice(0, runs.length - keep);
|
|
393
|
+
for (const run of toRemove) {
|
|
394
|
+
const runDir = getRunDir(jobName, run.runId);
|
|
395
|
+
bytesFreed += getDirSize(runDir);
|
|
396
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
397
|
+
deleted++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { deleted, bytesFreed };
|
|
401
|
+
}
|
|
402
|
+
function getDirSize(dirPath) {
|
|
403
|
+
if (!fs.existsSync(dirPath))
|
|
404
|
+
return 0;
|
|
405
|
+
let size = 0;
|
|
406
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
409
|
+
if (entry.isDirectory()) {
|
|
410
|
+
size += getDirSize(fullPath);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
try {
|
|
414
|
+
size += fs.statSync(fullPath).size;
|
|
415
|
+
}
|
|
416
|
+
catch { /* ignore */ }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return size;
|
|
420
|
+
}
|
package/dist/lib/runner.js
CHANGED
|
@@ -14,7 +14,7 @@ import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, } from './rout
|
|
|
14
14
|
import { getRunsDir } from './state.js';
|
|
15
15
|
import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
|
|
16
16
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
17
|
-
import {
|
|
17
|
+
import { createTimer, maybeRotate, truncate } from './events.js';
|
|
18
18
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
19
19
|
const AGENT_COMMANDS = {
|
|
20
20
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
@@ -109,11 +109,13 @@ function generateRunId() {
|
|
|
109
109
|
/** Execute a job synchronously (waits for completion or timeout before resolving). */
|
|
110
110
|
export async function executeJob(config) {
|
|
111
111
|
maybeRotate();
|
|
112
|
-
const
|
|
112
|
+
const timer = createTimer('agent.run', {
|
|
113
113
|
agent: config.agent,
|
|
114
114
|
version: config.version,
|
|
115
115
|
jobName: config.name,
|
|
116
116
|
mode: config.mode,
|
|
117
|
+
prompt: truncate(config.prompt, 200),
|
|
118
|
+
schedule: config.schedule,
|
|
117
119
|
});
|
|
118
120
|
const resolvedPrompt = resolveJobPrompt(config);
|
|
119
121
|
const cmd = buildJobCommand(config, resolvedPrompt);
|
|
@@ -146,6 +148,8 @@ export async function executeJob(config) {
|
|
|
146
148
|
detached: true,
|
|
147
149
|
env: spawnEnv,
|
|
148
150
|
});
|
|
151
|
+
// Mark startup time (time from function call to process spawn)
|
|
152
|
+
timer.mark('startup');
|
|
149
153
|
meta.pid = child.pid || null;
|
|
150
154
|
writeRunMeta(meta);
|
|
151
155
|
let settled = false;
|
|
@@ -168,7 +172,7 @@ export async function executeJob(config) {
|
|
|
168
172
|
meta.status = 'timeout';
|
|
169
173
|
meta.completedAt = new Date().toISOString();
|
|
170
174
|
writeRunMeta(meta);
|
|
171
|
-
|
|
175
|
+
timer.end({ status: 'timeout', runId });
|
|
172
176
|
const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
|
|
173
177
|
resolve({ meta, reportPath });
|
|
174
178
|
}, timeoutMs);
|
|
@@ -185,7 +189,7 @@ export async function executeJob(config) {
|
|
|
185
189
|
meta.status = code === 0 ? 'completed' : 'failed';
|
|
186
190
|
meta.completedAt = new Date().toISOString();
|
|
187
191
|
writeRunMeta(meta);
|
|
188
|
-
|
|
192
|
+
timer.end({ status: meta.status, exitCode: code ?? undefined, runId });
|
|
189
193
|
const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
|
|
190
194
|
resolve({ meta, reportPath });
|
|
191
195
|
});
|
|
@@ -201,7 +205,7 @@ export async function executeJob(config) {
|
|
|
201
205
|
meta.status = 'failed';
|
|
202
206
|
meta.completedAt = new Date().toISOString();
|
|
203
207
|
writeRunMeta(meta);
|
|
204
|
-
|
|
208
|
+
timer.end({ status: 'failed', error: err.message, runId });
|
|
205
209
|
resolve({ meta, reportPath: null });
|
|
206
210
|
});
|
|
207
211
|
child.unref();
|