@phnx-labs/agents-cli 1.14.1 → 1.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -3
- package/dist/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +388 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/exec.js +17 -17
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +206 -12
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +162 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +46 -13
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +77 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +43 -26
- package/dist/lib/rotate.js +99 -44
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -76
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +8 -1
- package/dist/lib/teams/registry.js +8 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +13 -7
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +30 -2
- package/dist/lib/versions.js +127 -105
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- package/dist/lib/memory-compile.d.ts +0 -66
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare function isGitRepo(dir: string): Promise<boolean>;
|
|
2
|
+
export declare function getGitRoot(dir: string): Promise<string>;
|
|
3
|
+
/**
|
|
4
|
+
* Check if a worktree directory has uncommitted changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function hasUncommittedChanges(worktreePath: string): Promise<boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Create a new git worktree for a teammate.
|
|
9
|
+
*
|
|
10
|
+
* @param repoDir - Directory inside the git repository
|
|
11
|
+
* @param worktreeName - Name for the worktree (used in path and branch)
|
|
12
|
+
* @returns The absolute path to the created worktree
|
|
13
|
+
*/
|
|
14
|
+
export declare function createWorktree(repoDir: string, worktreeName: string): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Remove a git worktree and optionally its branch.
|
|
17
|
+
*
|
|
18
|
+
* @param repoDir - Directory inside the main git repository (not the worktree)
|
|
19
|
+
* @param worktreeName - Name of the worktree to remove
|
|
20
|
+
* @param deleteBranch - Whether to delete the associated branch
|
|
21
|
+
*/
|
|
22
|
+
export declare function removeWorktree(repoDir: string, worktreeName: string, deleteBranch?: boolean): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Get the worktree path for a given name.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getWorktreePath(gitRoot: string, worktreeName: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Get the branch name for a worktree.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getWorktreeBranch(worktreeName: string): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree utilities for isolated agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Creates/removes temporary worktrees so each teammate can work on
|
|
5
|
+
* its own branch without interfering with others or the main checkout.
|
|
6
|
+
*/
|
|
7
|
+
import { execFile } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import * as fs from 'fs/promises';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
export async function isGitRepo(dir) {
|
|
13
|
+
try {
|
|
14
|
+
await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: dir });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function getGitRoot(dir) {
|
|
22
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd: dir });
|
|
23
|
+
return stdout.trim();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if a worktree directory has uncommitted changes.
|
|
27
|
+
*/
|
|
28
|
+
export async function hasUncommittedChanges(worktreePath) {
|
|
29
|
+
try {
|
|
30
|
+
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], { cwd: worktreePath });
|
|
31
|
+
return stdout.trim().length > 0;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a new git worktree for a teammate.
|
|
39
|
+
*
|
|
40
|
+
* @param repoDir - Directory inside the git repository
|
|
41
|
+
* @param worktreeName - Name for the worktree (used in path and branch)
|
|
42
|
+
* @returns The absolute path to the created worktree
|
|
43
|
+
*/
|
|
44
|
+
export async function createWorktree(repoDir, worktreeName) {
|
|
45
|
+
const gitRoot = await getGitRoot(repoDir);
|
|
46
|
+
const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
|
|
47
|
+
const branchName = `agents/${worktreeName}`;
|
|
48
|
+
await fs.mkdir(path.dirname(worktreePath), { recursive: true });
|
|
49
|
+
await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, 'HEAD'], {
|
|
50
|
+
cwd: gitRoot,
|
|
51
|
+
});
|
|
52
|
+
return worktreePath;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Remove a git worktree and optionally its branch.
|
|
56
|
+
*
|
|
57
|
+
* @param repoDir - Directory inside the main git repository (not the worktree)
|
|
58
|
+
* @param worktreeName - Name of the worktree to remove
|
|
59
|
+
* @param deleteBranch - Whether to delete the associated branch
|
|
60
|
+
*/
|
|
61
|
+
export async function removeWorktree(repoDir, worktreeName, deleteBranch = true) {
|
|
62
|
+
const gitRoot = await getGitRoot(repoDir);
|
|
63
|
+
const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
|
|
64
|
+
const branchName = `agents/${worktreeName}`;
|
|
65
|
+
try {
|
|
66
|
+
await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: gitRoot });
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (err.message?.includes('is not a working tree')) {
|
|
70
|
+
await execFileAsync('git', ['worktree', 'prune'], { cwd: gitRoot });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (deleteBranch) {
|
|
77
|
+
try {
|
|
78
|
+
await execFileAsync('git', ['branch', '-D', branchName], { cwd: gitRoot });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Branch might not exist; ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the worktree path for a given name.
|
|
87
|
+
*/
|
|
88
|
+
export function getWorktreePath(gitRoot, worktreeName) {
|
|
89
|
+
return path.join(gitRoot, '.agents', 'worktrees', worktreeName);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the branch name for a worktree.
|
|
93
|
+
*/
|
|
94
|
+
export function getWorktreeBranch(worktreeName) {
|
|
95
|
+
return `agents/${worktreeName}`;
|
|
96
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/** Unique identifier for a supported AI coding agent. */
|
|
9
9
|
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'roo';
|
|
10
10
|
/** How `agents run <agent>` chooses an installed version when none is pinned. */
|
|
11
|
-
export type RunStrategy = 'pinned' | 'available' | '
|
|
11
|
+
export type RunStrategy = 'pinned' | 'available' | 'balanced';
|
|
12
12
|
/** Preview features that users can opt into via `agents beta`. */
|
|
13
13
|
export type BetaFeatureName = 'drive' | 'factory';
|
|
14
14
|
/** Subset of chalk color names used for agent-specific terminal output. */
|
|
@@ -41,10 +41,10 @@ export interface AgentConfig {
|
|
|
41
41
|
plugins: Capability;
|
|
42
42
|
/**
|
|
43
43
|
* Whether the agent natively resolves `@path/to/file` imports inside its
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* rules file at session start. If false, agents-cli must pre-compile the
|
|
45
|
+
* rules file (inline all @-imports) when syncing it into the version home.
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
rulesImports?: boolean;
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
@@ -186,9 +186,9 @@ export interface RepoInfo {
|
|
|
186
186
|
lastSync: string;
|
|
187
187
|
}
|
|
188
188
|
/** Canonical system repo cloned into ~/.agents-system/. */
|
|
189
|
-
export declare const DEFAULT_SYSTEM_REPO = "gh:
|
|
190
|
-
/**
|
|
191
|
-
export declare const
|
|
189
|
+
export declare const DEFAULT_SYSTEM_REPO = "gh:muqsitnawaz/.agents-system";
|
|
190
|
+
/** Mirror system repo (phnx-labs) — will become the default once fully released. */
|
|
191
|
+
export declare const MIRROR_SYSTEM_REPO = "gh:phnx-labs/.agents-system";
|
|
192
192
|
/** Strip the `gh:` prefix and `.git` suffix to get a GitHub `owner/repo` slug. */
|
|
193
193
|
export declare function systemRepoSlug(repo?: string): string;
|
|
194
194
|
/** Kind of package that can be searched and installed from a registry. */
|
|
@@ -299,6 +299,12 @@ export interface VersionResources {
|
|
|
299
299
|
permissions?: string[];
|
|
300
300
|
subagents?: string[];
|
|
301
301
|
plugins?: string[];
|
|
302
|
+
/**
|
|
303
|
+
* Active rule preset for this agent@version. The composer reads layered
|
|
304
|
+
* `rules.yaml` files and emits this preset's subrules as the agent's
|
|
305
|
+
* single instruction file. Absent/null means the literal "default" preset.
|
|
306
|
+
*/
|
|
307
|
+
rulesPreset?: string;
|
|
302
308
|
}
|
|
303
309
|
/** Manifest file (plugin.yaml) at the root of a plugin bundle. */
|
|
304
310
|
export interface PluginManifest {
|
package/dist/lib/types.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* formats for each supported agent.
|
|
7
7
|
*/
|
|
8
8
|
/** Canonical system repo cloned into ~/.agents-system/. */
|
|
9
|
-
export const DEFAULT_SYSTEM_REPO = 'gh:
|
|
10
|
-
/**
|
|
11
|
-
export const
|
|
9
|
+
export const DEFAULT_SYSTEM_REPO = 'gh:muqsitnawaz/.agents-system';
|
|
10
|
+
/** Mirror system repo (phnx-labs) — will become the default once fully released. */
|
|
11
|
+
export const MIRROR_SYSTEM_REPO = 'gh:phnx-labs/.agents-system';
|
|
12
12
|
/** Strip the `gh:` prefix and `.git` suffix to get a GitHub `owner/repo` slug. */
|
|
13
13
|
export function systemRepoSlug(repo = DEFAULT_SYSTEM_REPO) {
|
|
14
14
|
return repo.replace(/^gh:/, '').replace(/\.git$/, '');
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -111,6 +111,18 @@ export declare function isLatestInstalled(agent: AgentId): Promise<{
|
|
|
111
111
|
* List all installed versions for an agent.
|
|
112
112
|
*/
|
|
113
113
|
export declare function listInstalledVersions(agent: AgentId): string[];
|
|
114
|
+
/**
|
|
115
|
+
* List every version directory for an agent, including ones missing the
|
|
116
|
+
* binary (typically home-only leftovers from a prior `removeVersion`).
|
|
117
|
+
*
|
|
118
|
+
* Used by `agents prune` to surface stale installs that the regular
|
|
119
|
+
* `listInstalledVersions` filters out. Do NOT use elsewhere — every other
|
|
120
|
+
* call site assumes a working binary.
|
|
121
|
+
*/
|
|
122
|
+
export declare function listInstalledVersionDirs(agent: AgentId): Array<{
|
|
123
|
+
version: string;
|
|
124
|
+
hasBinary: boolean;
|
|
125
|
+
}>;
|
|
114
126
|
/**
|
|
115
127
|
* Get the global default version for an agent.
|
|
116
128
|
*/
|
|
@@ -128,8 +140,24 @@ export declare function installVersion(agent: AgentId, version: string, onProgre
|
|
|
128
140
|
error?: string;
|
|
129
141
|
}>;
|
|
130
142
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
143
|
+
* Soft-delete a version directory by moving it to ~/.agents-system/trash/versions/.
|
|
144
|
+
* Returns the trash path on success or null on failure / no source.
|
|
145
|
+
*
|
|
146
|
+
* Trash layout: ~/.agents-system/trash/versions/<agent>/<version>/<timestamp>/
|
|
147
|
+
* The timestamp suffix lets a user soft-delete the same version twice (after
|
|
148
|
+
* re-install) without collision and gives a chronological audit trail.
|
|
149
|
+
*
|
|
150
|
+
* The whole versionDir moves — including `home/` (transcripts, sessions). The
|
|
151
|
+
* user can recover everything via `agents trash restore <agent>@<version>`.
|
|
152
|
+
* Nothing is ever hard-deleted.
|
|
153
|
+
*/
|
|
154
|
+
export declare function softDeleteVersionDir(agent: AgentId, version: string): string | null;
|
|
155
|
+
/**
|
|
156
|
+
* Remove a specific version of an agent.
|
|
157
|
+
*
|
|
158
|
+
* Soft-delete only: moves the entire version directory (including `home/`)
|
|
159
|
+
* to ~/.agents-system/trash/versions/. Recoverable via `agents trash restore`.
|
|
160
|
+
* Nothing is hard-deleted.
|
|
133
161
|
*/
|
|
134
162
|
export declare function removeVersion(agent: AgentId, version: string): boolean;
|
|
135
163
|
/**
|
package/dist/lib/versions.js
CHANGED
|
@@ -23,7 +23,7 @@ import { promisify } from 'util';
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import * as TOML from 'smol-toml';
|
|
25
25
|
import { checkbox, select } from '@inquirer/prompts';
|
|
26
|
-
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, clearVersionResources, recordVersionResources, getProjectAgentsDir, getPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir } from './state.js';
|
|
26
|
+
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, clearVersionResources, recordVersionResources, getProjectAgentsDir, getPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
|
|
27
27
|
import { resolveResource } from './resources.js';
|
|
28
28
|
import { AGENTS, getAccountEmail, MCP_CAPABLE_AGENTS, COMMANDS_CAPABLE_AGENTS, getMcpConfigPathForHome, parseMcpConfig, resolveAgentName, formatAgentError } from './agents.js';
|
|
29
29
|
import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_AGENTS, discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME, getActivePermissionSetName, readPermissionSetRecipe, PERMISSION_SET_ENV_VAR } from './permissions.js';
|
|
@@ -34,7 +34,7 @@ import { listInstalledSubagents, transformSubagentForClaude, syncSubagentToOpenc
|
|
|
34
34
|
import { registerHooksToSettings } from './hooks.js';
|
|
35
35
|
import { supports, explainSkip } from './capabilities.js';
|
|
36
36
|
import { discoverPlugins, syncPluginToVersion, isPluginSynced, pluginSupportsAgent, cleanOrphanedPluginSkills } from './plugins.js';
|
|
37
|
-
import {
|
|
37
|
+
import { composeRulesFromState } from './rules/compose.js';
|
|
38
38
|
import { loadSyncManifest, saveSyncManifest, buildManifest, isSyncStale } from './sync-manifest.js';
|
|
39
39
|
import { PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
40
40
|
import { safeJoin } from './paths.js';
|
|
@@ -117,28 +117,34 @@ export function getAvailableResources(cwd = process.cwd()) {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
result.hooks = Array.from(hookNames);
|
|
120
|
-
// Rules (
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
120
|
+
// Rules — list available presets across layers (project > user > extras > system).
|
|
121
|
+
// The composer selects exactly one preset per sync; this list drives the
|
|
122
|
+
// resource-count display and `agents rules switch` picker. Routes through
|
|
123
|
+
// the rules-dir getters so test mocks work the same as production paths.
|
|
124
|
+
const presetNames = new Set();
|
|
125
|
+
const rulesDirs = [];
|
|
126
|
+
if (projectAgentsDir)
|
|
127
|
+
rulesDirs.push(path.join(projectAgentsDir, 'rules'));
|
|
128
|
+
rulesDirs.push(getUserRulesDir());
|
|
129
|
+
rulesDirs.push(getResolvedRulesDir());
|
|
130
|
+
for (const extra of getEnabledExtraRepos()) {
|
|
131
|
+
rulesDirs.push(path.join(extra.dir, 'rules'));
|
|
132
|
+
}
|
|
133
|
+
for (const rulesDir of rulesDirs) {
|
|
134
|
+
const rulesYamlPath = path.join(rulesDir, 'rules.yaml');
|
|
135
|
+
if (!fs.existsSync(rulesYamlPath))
|
|
136
|
+
continue;
|
|
137
|
+
try {
|
|
138
|
+
const parsed = yaml.parse(fs.readFileSync(rulesYamlPath, 'utf-8'));
|
|
139
|
+
for (const name of Object.keys(parsed?.presets || {})) {
|
|
140
|
+
presetNames.add(name);
|
|
138
141
|
}
|
|
139
142
|
}
|
|
143
|
+
catch {
|
|
144
|
+
// malformed rules.yaml — skip silently; the composer will surface the error.
|
|
145
|
+
}
|
|
140
146
|
}
|
|
141
|
-
result.memory = Array.from(
|
|
147
|
+
result.memory = Array.from(presetNames);
|
|
142
148
|
// MCP servers (*.yaml files)
|
|
143
149
|
const mcpNames = new Set();
|
|
144
150
|
for (const { base } of resourceBases) {
|
|
@@ -316,47 +322,13 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
316
322
|
}
|
|
317
323
|
}
|
|
318
324
|
}
|
|
319
|
-
// Rules
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
if (projectMemoryDir && fs.existsSync(projectMemoryDir)) {
|
|
328
|
-
fs.readdirSync(projectMemoryDir).filter(f => f.endsWith('.md') && f !== RULES_DOC_FILENAME).forEach(f => memoryFiles.add(f));
|
|
329
|
-
}
|
|
330
|
-
if (fs.existsSync(userMemoryDir)) {
|
|
331
|
-
fs.readdirSync(userMemoryDir).filter(f => f.endsWith('.md') && f !== RULES_DOC_FILENAME).forEach(f => memoryFiles.add(f));
|
|
332
|
-
}
|
|
333
|
-
for (const file of memoryFiles) {
|
|
334
|
-
const memName = file.replace(/\.md$/, '');
|
|
335
|
-
const targetName = file === 'AGENTS.md' ? agentConfig.instructionsFile : file;
|
|
336
|
-
const versionFile = path.join(configDir, targetName);
|
|
337
|
-
if (!fs.existsSync(versionFile))
|
|
338
|
-
continue;
|
|
339
|
-
const projectFile = projectMemoryDir ? path.join(projectMemoryDir, file) : null;
|
|
340
|
-
const centralFile = path.join(memoryDir, file);
|
|
341
|
-
const userFile = path.join(userMemoryDir, file);
|
|
342
|
-
const hasProject = projectFile ? fs.existsSync(projectFile) : false;
|
|
343
|
-
const hasUser = fs.existsSync(userFile);
|
|
344
|
-
const hasCentral = fs.existsSync(centralFile);
|
|
345
|
-
const sourceFile = hasProject ? projectFile : hasUser ? userFile : centralFile;
|
|
346
|
-
if (!hasProject && !hasCentral && !hasUser) {
|
|
347
|
-
result.memory.push(memName);
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
try {
|
|
351
|
-
const centralContent = fs.readFileSync(sourceFile, 'utf-8');
|
|
352
|
-
const versionContent = fs.readFileSync(versionFile, 'utf-8');
|
|
353
|
-
if (centralContent === versionContent) {
|
|
354
|
-
result.memory.push(memName);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
// Ignore
|
|
359
|
-
}
|
|
325
|
+
// Rules — single composed instruction file per agent. If the file exists in
|
|
326
|
+
// the version home, we consider the active preset synced. Available presets
|
|
327
|
+
// are surfaced from rules.yaml; this set is the subset that materialized.
|
|
328
|
+
const instrFile = path.join(configDir, agentConfig.instructionsFile);
|
|
329
|
+
if (fs.existsSync(instrFile)) {
|
|
330
|
+
const activePreset = getActiveRulesPreset(agent, version);
|
|
331
|
+
result.memory.push(activePreset);
|
|
360
332
|
}
|
|
361
333
|
// MCP - use canonical config path + parser per agent
|
|
362
334
|
if (MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
@@ -886,6 +858,31 @@ export function listInstalledVersions(agent) {
|
|
|
886
858
|
}
|
|
887
859
|
return versions.sort(compareVersions);
|
|
888
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* List every version directory for an agent, including ones missing the
|
|
863
|
+
* binary (typically home-only leftovers from a prior `removeVersion`).
|
|
864
|
+
*
|
|
865
|
+
* Used by `agents prune` to surface stale installs that the regular
|
|
866
|
+
* `listInstalledVersions` filters out. Do NOT use elsewhere — every other
|
|
867
|
+
* call site assumes a working binary.
|
|
868
|
+
*/
|
|
869
|
+
export function listInstalledVersionDirs(agent) {
|
|
870
|
+
const agentVersionsDir = path.join(getVersionsDir(), agent);
|
|
871
|
+
if (!fs.existsSync(agentVersionsDir)) {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
const entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
|
|
875
|
+
const out = [];
|
|
876
|
+
for (const entry of entries) {
|
|
877
|
+
if (!entry.isDirectory())
|
|
878
|
+
continue;
|
|
879
|
+
out.push({
|
|
880
|
+
version: entry.name,
|
|
881
|
+
hasBinary: fs.existsSync(getBinaryPath(agent, entry.name)),
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return out.sort((a, b) => compareVersions(a.version, b.version));
|
|
885
|
+
}
|
|
889
886
|
/**
|
|
890
887
|
* Get the global default version for an agent.
|
|
891
888
|
*/
|
|
@@ -1000,8 +997,9 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1000
997
|
/**
|
|
1001
998
|
* Remove install artifacts from a version directory, preserving `home/` which
|
|
1002
999
|
* contains the user's conversation history, sessions, history.jsonl, tasks,
|
|
1003
|
-
* todos, file-history, etc.
|
|
1004
|
-
*
|
|
1000
|
+
* todos, file-history, etc. Used by the install pipeline (NOT by removeVersion)
|
|
1001
|
+
* to clean up staging artifacts when a fresh install collides with an existing
|
|
1002
|
+
* dir. removeVersion uses soft-delete instead.
|
|
1005
1003
|
*/
|
|
1006
1004
|
function removeInstallArtifacts(versionDir) {
|
|
1007
1005
|
for (const entry of fs.readdirSync(versionDir)) {
|
|
@@ -1011,15 +1009,50 @@ function removeInstallArtifacts(versionDir) {
|
|
|
1011
1009
|
}
|
|
1012
1010
|
}
|
|
1013
1011
|
/**
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1012
|
+
* Soft-delete a version directory by moving it to ~/.agents-system/trash/versions/.
|
|
1013
|
+
* Returns the trash path on success or null on failure / no source.
|
|
1014
|
+
*
|
|
1015
|
+
* Trash layout: ~/.agents-system/trash/versions/<agent>/<version>/<timestamp>/
|
|
1016
|
+
* The timestamp suffix lets a user soft-delete the same version twice (after
|
|
1017
|
+
* re-install) without collision and gives a chronological audit trail.
|
|
1018
|
+
*
|
|
1019
|
+
* The whole versionDir moves — including `home/` (transcripts, sessions). The
|
|
1020
|
+
* user can recover everything via `agents trash restore <agent>@<version>`.
|
|
1021
|
+
* Nothing is ever hard-deleted.
|
|
1022
|
+
*/
|
|
1023
|
+
export function softDeleteVersionDir(agent, version) {
|
|
1024
|
+
const versionDir = getVersionDir(agent, version);
|
|
1025
|
+
if (!fs.existsSync(versionDir))
|
|
1026
|
+
return null;
|
|
1027
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1028
|
+
const trashRoot = getTrashVersionsDir();
|
|
1029
|
+
const trashAgentDir = path.join(trashRoot, agent, version);
|
|
1030
|
+
const trashDest = path.join(trashAgentDir, stamp);
|
|
1031
|
+
try {
|
|
1032
|
+
fs.mkdirSync(trashAgentDir, { recursive: true, mode: 0o700 });
|
|
1033
|
+
fs.renameSync(versionDir, trashDest);
|
|
1034
|
+
return trashDest;
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Remove a specific version of an agent.
|
|
1042
|
+
*
|
|
1043
|
+
* Soft-delete only: moves the entire version directory (including `home/`)
|
|
1044
|
+
* to ~/.agents-system/trash/versions/. Recoverable via `agents trash restore`.
|
|
1045
|
+
* Nothing is hard-deleted.
|
|
1016
1046
|
*/
|
|
1017
1047
|
export function removeVersion(agent, version) {
|
|
1018
1048
|
const versionDir = getVersionDir(agent, version);
|
|
1019
1049
|
if (!fs.existsSync(versionDir)) {
|
|
1020
1050
|
return false;
|
|
1021
1051
|
}
|
|
1022
|
-
|
|
1052
|
+
const trashPath = softDeleteVersionDir(agent, version);
|
|
1053
|
+
if (!trashPath) {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1023
1056
|
// Remove versioned alias (e.g., claude@2.0.65)
|
|
1024
1057
|
removeVersionedAlias(agent, version);
|
|
1025
1058
|
// Clear resource tracking for this version
|
|
@@ -1308,10 +1341,10 @@ export function getResourceDiff(agent, version) {
|
|
|
1308
1341
|
}
|
|
1309
1342
|
}
|
|
1310
1343
|
// Rules: check individual file symlinks
|
|
1311
|
-
const
|
|
1312
|
-
if (fs.existsSync(
|
|
1313
|
-
const
|
|
1314
|
-
for (const file of
|
|
1344
|
+
const systemRulesDir = getResolvedRulesDir();
|
|
1345
|
+
if (fs.existsSync(systemRulesDir)) {
|
|
1346
|
+
const ruleFiles = fs.readdirSync(systemRulesDir).filter(f => f.endsWith('.md') && f !== RULES_DOC_FILENAME);
|
|
1347
|
+
for (const file of ruleFiles) {
|
|
1315
1348
|
const targetName = file === 'AGENTS.md' ? agentConfig.instructionsFile : file;
|
|
1316
1349
|
const targetPath = path.join(agentDir, targetName);
|
|
1317
1350
|
const status = getSymlinkStatus(targetPath);
|
|
@@ -1555,44 +1588,33 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1555
1588
|
}
|
|
1556
1589
|
}
|
|
1557
1590
|
}
|
|
1558
|
-
// Sync
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'rules'), `${mem}.md`)),
|
|
1574
|
-
];
|
|
1575
|
-
const srcFile = candidates.find((p) => p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink()) || null;
|
|
1576
|
-
if (!srcFile)
|
|
1577
|
-
continue;
|
|
1578
|
-
const targetName = mem === 'AGENTS' ? agentConfig.instructionsFile : `${mem}.md`;
|
|
1591
|
+
// Sync rules — compose from layered subrules + active preset and write a
|
|
1592
|
+
// single inlined instruction file. No @-import expansion; no per-fragment
|
|
1593
|
+
// copies. Project rules are NOT synced into the version home — they are
|
|
1594
|
+
// composed into the workspace at agents-run time (see compileRulesForProject).
|
|
1595
|
+
const skipMemory = selection && (selection.memory === undefined || (Array.isArray(selection.memory) && selection.memory.length === 0));
|
|
1596
|
+
if (!skipMemory && COMMANDS_CAPABLE_AGENTS.includes(agent)) {
|
|
1597
|
+
try {
|
|
1598
|
+
// If selection.memory names a single preset, treat it as a one-shot
|
|
1599
|
+
// override; otherwise read the persisted active preset.
|
|
1600
|
+
const overridePreset = Array.isArray(selection?.memory) && selection.memory.length === 1 && selection.memory[0] !== 'AGENTS'
|
|
1601
|
+
? selection.memory[0]
|
|
1602
|
+
: null;
|
|
1603
|
+
const preset = overridePreset || getActiveRulesPreset(agent, version);
|
|
1604
|
+
const composed = composeRulesFromState({ preset });
|
|
1605
|
+
const targetName = agentConfig.instructionsFile;
|
|
1579
1606
|
const destFile = safeJoin(agentDir, targetName);
|
|
1607
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
1580
1608
|
removePath(destFile);
|
|
1581
|
-
|
|
1582
|
-
// resolve @-imports get a compiled (inlined) copy + sidecar manifest.
|
|
1583
|
-
// Everything else (secondary memory files, @-capable agents) gets a
|
|
1584
|
-
// straight copy.
|
|
1585
|
-
if (mem === 'AGENTS' && !agentSupportsImports) {
|
|
1586
|
-
compileMemoryForAgent(agent, version);
|
|
1587
|
-
}
|
|
1588
|
-
else {
|
|
1589
|
-
fs.copyFileSync(srcFile, destFile);
|
|
1590
|
-
}
|
|
1609
|
+
fs.writeFileSync(destFile, composed.content);
|
|
1591
1610
|
result.memory.push(targetName);
|
|
1592
|
-
|
|
1611
|
+
// Track which preset materialized — surfaces in `agents rules list`.
|
|
1612
|
+
recordVersionResources(agent, version, 'memory', [composed.preset]);
|
|
1593
1613
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1614
|
+
catch (err) {
|
|
1615
|
+
// No rules.yaml yet, or a typo'd preset name. Don't fail the whole sync —
|
|
1616
|
+
// just leave the agent without a synced rules file.
|
|
1617
|
+
console.warn(`Skipping rules sync for ${agent}@${version}: ${err.message}`);
|
|
1596
1618
|
}
|
|
1597
1619
|
}
|
|
1598
1620
|
// Apply permissions (if agent supports them).
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -48,6 +48,35 @@ function runMigrations() {
|
|
|
48
48
|
fs.unlinkSync(configSrc);
|
|
49
49
|
} catch { /* best-effort */ }
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
// 4. Move installed agent versions from ~/.agents/versions/ -> ~/.agents-system/versions/
|
|
53
|
+
// Pre-split layout put binaries under the user repo. Post-split, listInstalledVersions
|
|
54
|
+
// only scans the system root, so legacy installs become invisible without this move.
|
|
55
|
+
const userVersions = path.join(USER_DIR, 'versions');
|
|
56
|
+
const sysVersions = path.join(SYSTEM_DIR, 'versions');
|
|
57
|
+
if (fs.existsSync(userVersions)) {
|
|
58
|
+
try {
|
|
59
|
+
let moved = 0;
|
|
60
|
+
let skipped = 0;
|
|
61
|
+
for (const agent of fs.readdirSync(userVersions, { withFileTypes: true })) {
|
|
62
|
+
if (!agent.isDirectory()) continue;
|
|
63
|
+
const srcAgentDir = path.join(userVersions, agent.name);
|
|
64
|
+
const dstAgentDir = path.join(sysVersions, agent.name);
|
|
65
|
+
try { fs.mkdirSync(dstAgentDir, { recursive: true }); } catch {}
|
|
66
|
+
for (const ver of fs.readdirSync(srcAgentDir, { withFileTypes: true })) {
|
|
67
|
+
if (!ver.isDirectory()) continue;
|
|
68
|
+
const src = path.join(srcAgentDir, ver.name);
|
|
69
|
+
const dst = path.join(dstAgentDir, ver.name);
|
|
70
|
+
if (fs.existsSync(dst)) { skipped++; continue; }
|
|
71
|
+
try { fs.renameSync(src, dst); moved++; } catch {}
|
|
72
|
+
}
|
|
73
|
+
try { if (fs.readdirSync(srcAgentDir).length === 0) fs.rmdirSync(srcAgentDir); } catch {}
|
|
74
|
+
}
|
|
75
|
+
try { if (fs.readdirSync(userVersions).length === 0) fs.rmdirSync(userVersions); } catch {}
|
|
76
|
+
if (moved > 0) console.log(` Migrated ${moved} agent version dir(s) to ~/.agents-system/versions/`);
|
|
77
|
+
if (skipped > 0) console.log(` Kept ${skipped} legacy version dir(s) at ~/.agents/versions/ (already present in system root)`);
|
|
78
|
+
} catch { /* best-effort */ }
|
|
79
|
+
}
|
|
51
80
|
}
|
|
52
81
|
|
|
53
82
|
runMigrations();
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal memory refresh command.
|
|
3
|
-
*
|
|
4
|
-
* Registers the hidden `agents refresh-memory` command invoked by
|
|
5
|
-
* shims for agents that do not natively resolve @-imports in their
|
|
6
|
-
* memory file. Recompiles memory only when source files have changed.
|
|
7
|
-
*/
|
|
8
|
-
import { Command } from 'commander';
|
|
9
|
-
/**
|
|
10
|
-
* Hidden command invoked by shims for agents that don't natively resolve
|
|
11
|
-
* @-imports in their memory file. Fast-path check first (sha256 of tracked
|
|
12
|
-
* source files); only recompiles if a source has changed since the last
|
|
13
|
-
* sync. Typical cost: 10-20ms when memory is fresh.
|
|
14
|
-
*/
|
|
15
|
-
export declare function registerRefreshMemoryCommand(program: Command): void;
|