@phnx-labs/agents-cli 1.20.12 → 1.20.14
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 +30 -0
- package/README.md +3 -0
- package/dist/commands/computer-actions.d.ts +3 -0
- package/dist/commands/computer-actions.js +16 -0
- package/dist/commands/doctor.js +51 -7
- package/dist/commands/exec.js +25 -4
- package/dist/commands/import.js +17 -6
- package/dist/commands/inspect.d.ts +28 -1
- package/dist/commands/inspect.js +330 -47
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +69 -26
- package/dist/commands/prune.js +8 -5
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +1 -0
- package/dist/commands/trash.d.ts +11 -0
- package/dist/commands/trash.js +57 -41
- package/dist/commands/versions.js +68 -20
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +56 -12
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +199 -0
- package/dist/index.js +4 -1
- package/dist/lib/agents.js +70 -22
- package/dist/lib/browser/ipc.d.ts +7 -0
- package/dist/lib/browser/ipc.js +43 -27
- package/dist/lib/capabilities.js +7 -1
- package/dist/lib/command-skills.d.ts +1 -0
- package/dist/lib/command-skills.js +23 -7
- package/dist/lib/exec.d.ts +32 -1
- package/dist/lib/exec.js +79 -7
- package/dist/lib/hooks.d.ts +21 -1
- package/dist/lib/hooks.js +69 -7
- package/dist/lib/mcp.js +33 -0
- package/dist/lib/models.js +5 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +96 -6
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/winpath.d.ts +35 -0
- package/dist/lib/platform/winpath.js +86 -0
- package/dist/lib/plugins.d.ts +24 -0
- package/dist/lib/plugins.js +37 -2
- package/dist/lib/project-launch.js +110 -5
- package/dist/lib/registry.js +15 -2
- package/dist/lib/rotate.d.ts +7 -0
- package/dist/lib/rotate.js +17 -7
- package/dist/lib/runner.js +14 -0
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/settings-manifest.d.ts +39 -0
- package/dist/lib/settings-manifest.js +163 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +16 -31
- package/dist/lib/staleness/detectors/subagents.js +16 -0
- package/dist/lib/staleness/writers/subagents.js +11 -3
- package/dist/lib/subagents.d.ts +9 -0
- package/dist/lib/subagents.js +33 -0
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +6 -0
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.d.ts +15 -3
- package/dist/lib/versions.js +88 -19
- package/dist/lib/wallet/index.d.ts +78 -0
- package/dist/lib/wallet/index.js +253 -0
- package/package.json +3 -3
- package/scripts/postinstall.js +35 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subagents detector. Claude: flat .md files under `<agentDir>/agents/`.
|
|
3
|
+
* Droid: flat .md files under `<versionHome>/.factory/droids/`.
|
|
3
4
|
* OpenClaw: subdirectories containing AGENTS.md under `<versionHome>/.openclaw/`.
|
|
4
5
|
* Mirrors versions.ts:521-539.
|
|
5
6
|
*/
|
|
@@ -21,6 +22,20 @@ function buildClaudeDetector() {
|
|
|
21
22
|
},
|
|
22
23
|
};
|
|
23
24
|
}
|
|
25
|
+
function buildDroidDetector() {
|
|
26
|
+
return {
|
|
27
|
+
kind: 'subagents',
|
|
28
|
+
agent: 'droid',
|
|
29
|
+
list({ versionHome }) {
|
|
30
|
+
const droidsDir = path.join(versionHome, '.factory', 'droids');
|
|
31
|
+
if (!fs.existsSync(droidsDir))
|
|
32
|
+
return [];
|
|
33
|
+
return fs.readdirSync(droidsDir)
|
|
34
|
+
.filter(f => f.endsWith('.md'))
|
|
35
|
+
.map(f => f.replace('.md', ''));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
24
39
|
function buildOpenclawDetector() {
|
|
25
40
|
return {
|
|
26
41
|
kind: 'subagents',
|
|
@@ -37,6 +52,7 @@ function buildOpenclawDetector() {
|
|
|
37
52
|
}
|
|
38
53
|
const handlers = {
|
|
39
54
|
claude: buildClaudeDetector,
|
|
55
|
+
droid: buildDroidDetector,
|
|
40
56
|
openclaw: buildOpenclawDetector,
|
|
41
57
|
};
|
|
42
58
|
export const subagentsDetectors = lazyAgentMap(() => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subagents writer. Claude flattens each subagent into a single .md file
|
|
3
|
-
* under `<agentDir>/agents/`.
|
|
4
|
-
*
|
|
3
|
+
* under `<agentDir>/agents/`. Droid (Factory AI) flattens each into a custom
|
|
4
|
+
* droid .md under `<versionHome>/.factory/droids/`. OpenClaw copies the full
|
|
5
|
+
* subagent directory (with AGENT.md renamed to AGENTS.md) into
|
|
6
|
+
* `<versionHome>/.openclaw/<name>/`.
|
|
5
7
|
*
|
|
6
8
|
* Source-side discovery is `listInstalledSubagents` from lib/subagents.ts —
|
|
7
9
|
* it reads user + system layers only (project layer excluded for the same
|
|
@@ -10,7 +12,7 @@
|
|
|
10
12
|
import * as fs from 'fs';
|
|
11
13
|
import * as path from 'path';
|
|
12
14
|
import { capableAgents } from '../../capabilities.js';
|
|
13
|
-
import { listInstalledSubagents, transformSubagentForClaude, syncSubagentToOpenclaw } from '../../subagents.js';
|
|
15
|
+
import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForDroid, syncSubagentToOpenclaw } from '../../subagents.js';
|
|
14
16
|
import { safeJoin } from '../../paths.js';
|
|
15
17
|
import { lazyAgentMap } from './lazy-map.js';
|
|
16
18
|
function buildSubagentsWriter(agent) {
|
|
@@ -32,6 +34,12 @@ function buildSubagentsWriter(agent) {
|
|
|
32
34
|
fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForClaude(sub.path));
|
|
33
35
|
synced.push(sub.name);
|
|
34
36
|
}
|
|
37
|
+
else if (agent === 'droid') {
|
|
38
|
+
const droidsDir = path.join(versionHome, '.factory', 'droids');
|
|
39
|
+
fs.mkdirSync(droidsDir, { recursive: true });
|
|
40
|
+
fs.writeFileSync(safeJoin(droidsDir, `${sub.name}.md`), transformSubagentForDroid(sub.path));
|
|
41
|
+
synced.push(sub.name);
|
|
42
|
+
}
|
|
35
43
|
else if (agent === 'openclaw') {
|
|
36
44
|
const target = safeJoin(path.join(versionHome, '.openclaw'), sub.name);
|
|
37
45
|
const r = syncSubagentToOpenclaw(sub.path, target);
|
package/dist/lib/subagents.d.ts
CHANGED
|
@@ -47,6 +47,15 @@ export declare function removeSubagent(name: string): {
|
|
|
47
47
|
* Combines AGENT.md frontmatter + body with other files as sections
|
|
48
48
|
*/
|
|
49
49
|
export declare function transformSubagentForClaude(subagentDir: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Transform a subagent into a Factory AI Droid "custom droid" .md file.
|
|
52
|
+
*
|
|
53
|
+
* Mirrors transformSubagentForClaude (flatten frontmatter + body + appended
|
|
54
|
+
* .md sections), but emits only frontmatter keys Factory recognizes
|
|
55
|
+
* (name, description, model). Factory has no `color` field, so it is dropped.
|
|
56
|
+
* See https://docs.factory.ai/cli/configuration/custom-droids.
|
|
57
|
+
*/
|
|
58
|
+
export declare function transformSubagentForDroid(subagentDir: string): string;
|
|
50
59
|
/**
|
|
51
60
|
* Sync a subagent to an OpenClaw workspace
|
|
52
61
|
* Copies full directory, renames AGENT.md to AGENTS.md
|
package/dist/lib/subagents.js
CHANGED
|
@@ -229,6 +229,39 @@ export function transformSubagentForClaude(subagentDir) {
|
|
|
229
229
|
}
|
|
230
230
|
return result;
|
|
231
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Transform a subagent into a Factory AI Droid "custom droid" .md file.
|
|
234
|
+
*
|
|
235
|
+
* Mirrors transformSubagentForClaude (flatten frontmatter + body + appended
|
|
236
|
+
* .md sections), but emits only frontmatter keys Factory recognizes
|
|
237
|
+
* (name, description, model). Factory has no `color` field, so it is dropped.
|
|
238
|
+
* See https://docs.factory.ai/cli/configuration/custom-droids.
|
|
239
|
+
*/
|
|
240
|
+
export function transformSubagentForDroid(subagentDir) {
|
|
241
|
+
const agentMd = path.join(subagentDir, 'AGENT.md');
|
|
242
|
+
const frontmatter = parseSubagentFrontmatter(agentMd);
|
|
243
|
+
const body = getSubagentBody(agentMd);
|
|
244
|
+
if (!frontmatter) {
|
|
245
|
+
throw new Error(`Invalid AGENT.md in ${subagentDir}`);
|
|
246
|
+
}
|
|
247
|
+
const frontmatterYaml = yaml.stringify({
|
|
248
|
+
name: frontmatter.name,
|
|
249
|
+
description: frontmatter.description,
|
|
250
|
+
...(frontmatter.model && { model: frontmatter.model }),
|
|
251
|
+
}).trim();
|
|
252
|
+
let result = `---\n${frontmatterYaml}\n---\n\n${body}`;
|
|
253
|
+
const files = fs.readdirSync(subagentDir)
|
|
254
|
+
.filter(f => f.endsWith('.md') && f !== 'AGENT.md')
|
|
255
|
+
.sort();
|
|
256
|
+
for (const file of files) {
|
|
257
|
+
const filePath = path.join(subagentDir, file);
|
|
258
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
259
|
+
const sectionName = file.replace('.md', '');
|
|
260
|
+
const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
|
|
261
|
+
result += `\n\n## ${title}\n\n${content}`;
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
232
265
|
/**
|
|
233
266
|
* Sync a subagent to an OpenClaw workspace
|
|
234
267
|
* Copies full directory, renames AGENT.md to AGENTS.md
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -173,7 +173,7 @@ export function captureProcessStartTime(pid) {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
/** Agent types the team runner supports. */
|
|
176
|
-
const TEAM_AGENT_TYPES = ['codex', 'cursor', 'gemini', 'claude', 'opencode', 'grok', 'antigravity', 'kimi'];
|
|
176
|
+
const TEAM_AGENT_TYPES = ['codex', 'cursor', 'gemini', 'claude', 'opencode', 'grok', 'antigravity', 'kimi', 'droid'];
|
|
177
177
|
// Suffix appended to all prompts to ensure agents provide a summary
|
|
178
178
|
const PROMPT_SUFFIX = `
|
|
179
179
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Supported agent CLI types for team spawning. */
|
|
2
|
-
export type AgentType = 'codex' | 'gemini' | 'cursor' | 'claude' | 'opencode' | 'grok' | 'antigravity' | 'kimi';
|
|
2
|
+
export type AgentType = 'codex' | 'gemini' | 'cursor' | 'claude' | 'opencode' | 'grok' | 'antigravity' | 'kimi' | 'droid';
|
|
3
3
|
/** Normalize a raw JSON event from any agent type into an array of unified event objects. */
|
|
4
4
|
export declare function normalizeEvents(agentType: AgentType, raw: any): any[];
|
|
5
5
|
/** Normalize a raw JSON event, returning only the first unified event (convenience wrapper). */
|
|
@@ -31,6 +31,12 @@ export function normalizeEvents(agentType, raw) {
|
|
|
31
31
|
else if (agentType === 'antigravity') {
|
|
32
32
|
return normalizeAntigravity(raw);
|
|
33
33
|
}
|
|
34
|
+
// droid (Factory AI) intentionally falls through to the generic normalizer
|
|
35
|
+
// below: its `-o stream-json` JSONL event schema is not yet verified against
|
|
36
|
+
// a live run (the documented `debug` format differs from stream-json). Events
|
|
37
|
+
// still stream and render; structured tool/file categorization will be added
|
|
38
|
+
// once a real `droid exec -o stream-json` sample is captured. Do NOT guess the
|
|
39
|
+
// schema here — a wrong discriminator silently mislabels every event.
|
|
34
40
|
const timestamp = new Date().toISOString();
|
|
35
41
|
return [{
|
|
36
42
|
type: raw.type || 'unknown',
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* formats for each supported agent.
|
|
7
7
|
*/
|
|
8
8
|
/** Unique identifier for a supported AI coding agent. */
|
|
9
|
-
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'roo' | 'antigravity' | 'grok' | 'kimi';
|
|
9
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'roo' | 'antigravity' | 'grok' | 'kimi' | 'droid';
|
|
10
10
|
/** How `agents run <agent>` chooses an installed version when none is pinned. */
|
|
11
11
|
export type RunStrategy = 'pinned' | 'available' | 'balanced';
|
|
12
12
|
/** Per-agent run strategy config. */
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -126,6 +126,10 @@ export declare function isVersionInstalled(agent: AgentId, version: string): boo
|
|
|
126
126
|
* Get the latest available version from npm for an agent.
|
|
127
127
|
*/
|
|
128
128
|
export declare function getLatestNpmVersion(agent: AgentId): Promise<string | null>;
|
|
129
|
+
/**
|
|
130
|
+
* Get the oldest published version from npm for an agent.
|
|
131
|
+
*/
|
|
132
|
+
export declare function getOldestNpmVersion(agent: AgentId): Promise<string | null>;
|
|
129
133
|
/**
|
|
130
134
|
* Check if 'latest' version is already installed (by resolving to actual version).
|
|
131
135
|
*/
|
|
@@ -133,6 +137,13 @@ export declare function isLatestInstalled(agent: AgentId): Promise<{
|
|
|
133
137
|
installed: boolean;
|
|
134
138
|
version: string | null;
|
|
135
139
|
}>;
|
|
140
|
+
/**
|
|
141
|
+
* Check if 'oldest' published version is already installed (by resolving to actual version).
|
|
142
|
+
*/
|
|
143
|
+
export declare function isOldestInstalled(agent: AgentId): Promise<{
|
|
144
|
+
installed: boolean;
|
|
145
|
+
version: string | null;
|
|
146
|
+
}>;
|
|
136
147
|
/**
|
|
137
148
|
* List all installed versions for an agent.
|
|
138
149
|
*/
|
|
@@ -210,6 +221,7 @@ export declare function resolveVersion(agent: AgentId, projectPath?: string): st
|
|
|
210
221
|
*
|
|
211
222
|
* undefined / "" / "default" -> undefined (caller falls back to project pin or global default)
|
|
212
223
|
* "latest" -> highest installed version (process.exit if none installed)
|
|
224
|
+
* "oldest" -> lowest installed version (process.exit if none installed)
|
|
213
225
|
* "x.y.z" (installed) -> "x.y.z"
|
|
214
226
|
* "x.y.z" (not installed) -> process.exit with installed-list hint
|
|
215
227
|
*
|
|
@@ -221,9 +233,9 @@ export declare function resolveVersion(agent: AgentId, projectPath?: string): st
|
|
|
221
233
|
export declare function resolveVersionAlias(agent: AgentId, raw: string | undefined | null): string | undefined;
|
|
222
234
|
/**
|
|
223
235
|
* Loose variant of resolveVersionAlias for record-filter contexts (sessions,
|
|
224
|
-
* team history). Same `default`/`latest` semantics, but explicit
|
|
225
|
-
* pass through unchanged so historical records of uninstalled versions
|
|
226
|
-
* queryable.
|
|
236
|
+
* team history). Same `default`/`latest`/`oldest` semantics, but explicit
|
|
237
|
+
* versions pass through unchanged so historical records of uninstalled versions
|
|
238
|
+
* remain queryable.
|
|
227
239
|
*/
|
|
228
240
|
export declare function resolveVersionAliasLoose(agent: AgentId, raw: string | undefined | null): string | undefined;
|
|
229
241
|
/**
|
package/dist/lib/versions.js
CHANGED
|
@@ -35,7 +35,7 @@ import { discoverPlugins } from './plugins.js';
|
|
|
35
35
|
import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
|
|
36
36
|
import { emit } from './events.js';
|
|
37
37
|
import { safeJoin } from './paths.js';
|
|
38
|
-
import { listCommandSkillsInVersion, shouldInstallCommandAsSkill } from './command-skills.js';
|
|
38
|
+
import { listCommandSkillsInVersion, readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
|
|
39
39
|
import { getWriter, getDetector } from './staleness/registry.js';
|
|
40
40
|
/** Promisified exec for running shell commands. */
|
|
41
41
|
const execAsync = promisify(exec);
|
|
@@ -804,10 +804,7 @@ export function getVersionDir(agent, version) {
|
|
|
804
804
|
export function getBinaryPath(agent, version) {
|
|
805
805
|
const agentConfig = AGENTS[agent];
|
|
806
806
|
if (agent === 'grok') {
|
|
807
|
-
|
|
808
|
-
// We return a best-effort path (used for display / checks). Real resolution
|
|
809
|
-
// happens in agents.ts resolveGrokBinary + the generated shims.
|
|
810
|
-
const grokDownloads = path.join(os.homedir(), '.grok', 'downloads');
|
|
807
|
+
const grokDownloads = path.join(getVersionHomePath(agent, version), '.grok', 'downloads');
|
|
811
808
|
// Best effort: first matching file for this version
|
|
812
809
|
try {
|
|
813
810
|
const entries = fs.readdirSync(grokDownloads);
|
|
@@ -853,6 +850,26 @@ export async function getLatestNpmVersion(agent) {
|
|
|
853
850
|
return null;
|
|
854
851
|
}
|
|
855
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* Get the oldest published version from npm for an agent.
|
|
855
|
+
*/
|
|
856
|
+
export async function getOldestNpmVersion(agent) {
|
|
857
|
+
const agentConfig = AGENTS[agent];
|
|
858
|
+
if (!agentConfig.npmPackage)
|
|
859
|
+
return null;
|
|
860
|
+
try {
|
|
861
|
+
const { stdout } = await execFileAsync('npm', ['view', agentConfig.npmPackage, 'versions', '--json'], { shell: process.platform === 'win32' });
|
|
862
|
+
const parsed = JSON.parse(stdout.trim());
|
|
863
|
+
// `npm view ... versions --json` returns an array (multiple versions) or a
|
|
864
|
+
// bare string (single published version). Normalize to an array.
|
|
865
|
+
const versions = Array.isArray(parsed) ? parsed : [parsed];
|
|
866
|
+
const sorted = versions.filter((v) => VERSION_RE.test(v)).sort(compareVersions);
|
|
867
|
+
return sorted[0] ?? null;
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
856
873
|
/**
|
|
857
874
|
* Check if 'latest' version is already installed (by resolving to actual version).
|
|
858
875
|
*/
|
|
@@ -863,6 +880,16 @@ export async function isLatestInstalled(agent) {
|
|
|
863
880
|
}
|
|
864
881
|
return { installed: isVersionInstalled(agent, latestVersion), version: latestVersion };
|
|
865
882
|
}
|
|
883
|
+
/**
|
|
884
|
+
* Check if 'oldest' published version is already installed (by resolving to actual version).
|
|
885
|
+
*/
|
|
886
|
+
export async function isOldestInstalled(agent) {
|
|
887
|
+
const oldestVersion = await getOldestNpmVersion(agent);
|
|
888
|
+
if (!oldestVersion) {
|
|
889
|
+
return { installed: false, version: null };
|
|
890
|
+
}
|
|
891
|
+
return { installed: isVersionInstalled(agent, oldestVersion), version: oldestVersion };
|
|
892
|
+
}
|
|
866
893
|
/**
|
|
867
894
|
* List all installed versions for an agent.
|
|
868
895
|
*/
|
|
@@ -996,6 +1023,20 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
996
1023
|
emit('version.install', { agent, version: installedVersion });
|
|
997
1024
|
return { success: true, installedVersion };
|
|
998
1025
|
}
|
|
1026
|
+
// Resolve the `oldest` alias to a concrete npm version up front so the rest
|
|
1027
|
+
// of the install path treats it as an ordinary pinned install. (`latest`
|
|
1028
|
+
// keeps its bare-package-name + post-install-rename handling below.)
|
|
1029
|
+
if (version === 'oldest') {
|
|
1030
|
+
const oldest = await getOldestNpmVersion(agent);
|
|
1031
|
+
if (!oldest) {
|
|
1032
|
+
return {
|
|
1033
|
+
success: false,
|
|
1034
|
+
installedVersion: version,
|
|
1035
|
+
error: `Could not resolve the oldest published version for ${agentConfig.name} from npm.`,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
version = oldest;
|
|
1039
|
+
}
|
|
999
1040
|
ensureAgentsDir();
|
|
1000
1041
|
const versionDir = getVersionDir(agent, version);
|
|
1001
1042
|
// Create version directory and isolated home
|
|
@@ -1012,6 +1053,8 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1012
1053
|
const packageSpec = version === 'latest'
|
|
1013
1054
|
? agentConfig.npmPackage
|
|
1014
1055
|
: `${agentConfig.npmPackage}@${version}`;
|
|
1056
|
+
// The `${agentConfig.npmPackage}@` prefix is load-bearing: it ensures `version`
|
|
1057
|
+
// (which VERSION_RE permits to start with `-`) is never passed as a standalone npm CLI flag.
|
|
1015
1058
|
try {
|
|
1016
1059
|
// Check npm is available
|
|
1017
1060
|
const winShell = process.platform === 'win32';
|
|
@@ -1120,7 +1163,17 @@ export function softDeleteVersionDir(agent, version) {
|
|
|
1120
1163
|
const trashDest = path.join(trashAgentDir, stamp);
|
|
1121
1164
|
try {
|
|
1122
1165
|
fs.mkdirSync(trashAgentDir, { recursive: true, mode: 0o700 });
|
|
1123
|
-
|
|
1166
|
+
try {
|
|
1167
|
+
fs.renameSync(versionDir, trashDest);
|
|
1168
|
+
}
|
|
1169
|
+
catch (renameErr) {
|
|
1170
|
+
// On Windows, rename fails with EPERM/EACCES when any file in the tree
|
|
1171
|
+
// is locked by a running process. Fall back to recursive copy + delete.
|
|
1172
|
+
if (renameErr.code !== 'EPERM' && renameErr.code !== 'EACCES')
|
|
1173
|
+
throw renameErr;
|
|
1174
|
+
fs.cpSync(versionDir, trashDest, { recursive: true });
|
|
1175
|
+
fs.rmSync(versionDir, { recursive: true, force: true });
|
|
1176
|
+
}
|
|
1124
1177
|
return trashDest;
|
|
1125
1178
|
}
|
|
1126
1179
|
catch {
|
|
@@ -1182,10 +1235,10 @@ export function printTrashFooter(moved) {
|
|
|
1182
1235
|
console.log(chalk.gray('Sessions remain accessible via `agents sessions`.'));
|
|
1183
1236
|
if (moved.length === 1) {
|
|
1184
1237
|
const { agent, version } = moved[0];
|
|
1185
|
-
console.log(chalk.gray(`Restore with: agents
|
|
1238
|
+
console.log(chalk.gray(`Restore with: agents restore ${agent}@${version}`));
|
|
1186
1239
|
}
|
|
1187
1240
|
else {
|
|
1188
|
-
console.log(chalk.gray('Restore with: agents
|
|
1241
|
+
console.log(chalk.gray('Restore with: agents restore <agent>@<version> (run `agents trash list` to see)'));
|
|
1189
1242
|
}
|
|
1190
1243
|
}
|
|
1191
1244
|
/**
|
|
@@ -1223,6 +1276,7 @@ export function resolveVersion(agent, projectPath) {
|
|
|
1223
1276
|
*
|
|
1224
1277
|
* undefined / "" / "default" -> undefined (caller falls back to project pin or global default)
|
|
1225
1278
|
* "latest" -> highest installed version (process.exit if none installed)
|
|
1279
|
+
* "oldest" -> lowest installed version (process.exit if none installed)
|
|
1226
1280
|
* "x.y.z" (installed) -> "x.y.z"
|
|
1227
1281
|
* "x.y.z" (not installed) -> process.exit with installed-list hint
|
|
1228
1282
|
*
|
|
@@ -1234,14 +1288,14 @@ export function resolveVersion(agent, projectPath) {
|
|
|
1234
1288
|
export function resolveVersionAlias(agent, raw) {
|
|
1235
1289
|
if (!raw || raw === 'default')
|
|
1236
1290
|
return undefined;
|
|
1237
|
-
if (raw === 'latest') {
|
|
1291
|
+
if (raw === 'latest' || raw === 'oldest') {
|
|
1238
1292
|
const installed = listInstalledVersions(agent);
|
|
1239
1293
|
if (installed.length === 0) {
|
|
1240
1294
|
console.error(chalk.red(`No ${agent} versions installed.`));
|
|
1241
1295
|
console.error(chalk.gray(`Install one: agents versions install ${agent}`));
|
|
1242
1296
|
process.exit(1);
|
|
1243
1297
|
}
|
|
1244
|
-
return installed[installed.length - 1];
|
|
1298
|
+
return raw === 'oldest' ? installed[0] : installed[installed.length - 1];
|
|
1245
1299
|
}
|
|
1246
1300
|
if (!isVersionInstalled(agent, raw)) {
|
|
1247
1301
|
const installed = listInstalledVersions(agent);
|
|
@@ -1256,16 +1310,18 @@ export function resolveVersionAlias(agent, raw) {
|
|
|
1256
1310
|
}
|
|
1257
1311
|
/**
|
|
1258
1312
|
* Loose variant of resolveVersionAlias for record-filter contexts (sessions,
|
|
1259
|
-
* team history). Same `default`/`latest` semantics, but explicit
|
|
1260
|
-
* pass through unchanged so historical records of uninstalled versions
|
|
1261
|
-
* queryable.
|
|
1313
|
+
* team history). Same `default`/`latest`/`oldest` semantics, but explicit
|
|
1314
|
+
* versions pass through unchanged so historical records of uninstalled versions
|
|
1315
|
+
* remain queryable.
|
|
1262
1316
|
*/
|
|
1263
1317
|
export function resolveVersionAliasLoose(agent, raw) {
|
|
1264
1318
|
if (!raw || raw === 'default')
|
|
1265
1319
|
return undefined;
|
|
1266
|
-
if (raw === 'latest') {
|
|
1320
|
+
if (raw === 'latest' || raw === 'oldest') {
|
|
1267
1321
|
const installed = listInstalledVersions(agent);
|
|
1268
|
-
|
|
1322
|
+
if (installed.length === 0)
|
|
1323
|
+
return undefined;
|
|
1324
|
+
return raw === 'oldest' ? installed[0] : installed[installed.length - 1];
|
|
1269
1325
|
}
|
|
1270
1326
|
return raw;
|
|
1271
1327
|
}
|
|
@@ -1643,10 +1699,10 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1643
1699
|
const commandsToSync = selection
|
|
1644
1700
|
? resolveSelection(selection.commands, available.commands)
|
|
1645
1701
|
: available.commands; // No selection = sync all
|
|
1702
|
+
const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
|
|
1646
1703
|
if (commandsToSync.length > 0 && commandsWriter) {
|
|
1647
1704
|
const commandsTarget = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1648
|
-
|
|
1649
|
-
if (commandsAsSkills) {
|
|
1705
|
+
if (commandsAsSkills && agentConfig.commandsSubdir) {
|
|
1650
1706
|
removePath(commandsTarget);
|
|
1651
1707
|
}
|
|
1652
1708
|
const r = commandsWriter.write({ version, versionHome, selection: commandsToSync, cwd });
|
|
@@ -1680,9 +1736,22 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1680
1736
|
// ~/.agents/skills/ (Gemini) are not registered; we clear the version-home
|
|
1681
1737
|
// skills dir for them so a stale per-version copy never shadows central.
|
|
1682
1738
|
const skillsWriter = getWriter('skills', agent);
|
|
1683
|
-
|
|
1739
|
+
let skillsToSync = selection
|
|
1684
1740
|
? resolveSelection(selection.skills, available.skills)
|
|
1685
1741
|
: available.skills;
|
|
1742
|
+
if (commandsAsSkills && commandsToSync.length > 0 && skillsToSync.length > 0) {
|
|
1743
|
+
const commandNames = new Set(commandsToSync);
|
|
1744
|
+
const skillRoots = [
|
|
1745
|
+
path.join(getUserAgentsDir(), 'skills'),
|
|
1746
|
+
getSkillsDir(),
|
|
1747
|
+
...getEnabledExtraRepos().map((e) => path.join(e.dir, 'skills')),
|
|
1748
|
+
];
|
|
1749
|
+
skillsToSync = skillsToSync.filter((skill) => {
|
|
1750
|
+
if (!commandNames.has(skill))
|
|
1751
|
+
return true;
|
|
1752
|
+
return readSkillSourceCommandMarker(skill, skillRoots) !== skill;
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1686
1755
|
if (agentConfig.nativeAgentsSkillsDir) {
|
|
1687
1756
|
removePath(path.join(agentDir, 'skills'));
|
|
1688
1757
|
}
|
|
@@ -1745,7 +1814,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1745
1814
|
// CAPABLE_AGENTS list and silently skipped it). Project rules are NOT
|
|
1746
1815
|
// synced into the version home — they are composed into the workspace at
|
|
1747
1816
|
// agents-run time (see compileRulesForProject).
|
|
1748
|
-
const skipMemory = selection &&
|
|
1817
|
+
const skipMemory = selection && Array.isArray(selection.memory) && selection.memory.length === 0;
|
|
1749
1818
|
const rulesWriter = getWriter('rules', agent);
|
|
1750
1819
|
if (!skipMemory && rulesWriter) {
|
|
1751
1820
|
try {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet: device-local credit-card vault.
|
|
3
|
+
*
|
|
4
|
+
* Two-tier storage so listing the wallet doesn't pop Touch ID, but reading
|
|
5
|
+
* a card always does:
|
|
6
|
+
*
|
|
7
|
+
* Card metadata (id, nickname, brand, last4, expiry, created_at, kind)
|
|
8
|
+
* -> ~/.agents/wallet/cards.json, mode 0600
|
|
9
|
+
* -> Display-only data, equivalent of Apple Wallet's "card art" tier.
|
|
10
|
+
*
|
|
11
|
+
* Card secret (PAN, CVC, cardholder)
|
|
12
|
+
* -> Keychain item `agents-cli.secrets.wallet.<id>` (JSON-encoded)
|
|
13
|
+
* -> Routed through the signed helper, so the OS gates decryption with
|
|
14
|
+
* Touch ID + biometryCurrentSet. Re-enrolling Touch ID invalidates
|
|
15
|
+
* the item, matching Apple Pay's AR-value rotation behavior.
|
|
16
|
+
*
|
|
17
|
+
* Not Apple Pay: we store a real PAN, not a network DPAN, and we do not
|
|
18
|
+
* generate per-transaction cryptograms. Callers must surface this clearly.
|
|
19
|
+
*/
|
|
20
|
+
/** Test seam: override the index path. Returns the previous override (or null). */
|
|
21
|
+
export declare function _setIndexPathForTest(p: string | null): string | null;
|
|
22
|
+
export type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'diners' | 'jcb' | 'unionpay' | 'unknown';
|
|
23
|
+
/** Storage kind discriminator. Reserved for future kind: 'stripe_token'. */
|
|
24
|
+
export type CardKind = 'pan_encrypted';
|
|
25
|
+
/** Non-sensitive card metadata. Safe to display without biometric auth. */
|
|
26
|
+
export interface CardMetadata {
|
|
27
|
+
id: string;
|
|
28
|
+
nickname: string;
|
|
29
|
+
brand: CardBrand;
|
|
30
|
+
last4: string;
|
|
31
|
+
exp_month: string;
|
|
32
|
+
exp_year: string;
|
|
33
|
+
created_at: string;
|
|
34
|
+
kind: CardKind;
|
|
35
|
+
}
|
|
36
|
+
/** Sensitive card fields. Keychain-stored, Touch ID required to read. */
|
|
37
|
+
export interface CardSecret {
|
|
38
|
+
pan: string;
|
|
39
|
+
cvc: string;
|
|
40
|
+
cardholder: string;
|
|
41
|
+
}
|
|
42
|
+
/** Card metadata plus its secret payload. Returned by show() only. */
|
|
43
|
+
export interface CardFull extends CardMetadata, CardSecret {
|
|
44
|
+
}
|
|
45
|
+
/** Input shape for add(). */
|
|
46
|
+
export interface AddCardInput {
|
|
47
|
+
nickname: string;
|
|
48
|
+
pan: string;
|
|
49
|
+
cvc: string;
|
|
50
|
+
cardholder: string;
|
|
51
|
+
exp_month: string;
|
|
52
|
+
exp_year: string;
|
|
53
|
+
}
|
|
54
|
+
/** Compute the Luhn checksum and verify a candidate PAN. */
|
|
55
|
+
export declare function isValidLuhn(pan: string): boolean;
|
|
56
|
+
/** Detect card brand from the BIN (first 6 digits). */
|
|
57
|
+
export declare function detectBrand(pan: string): CardBrand;
|
|
58
|
+
/** Atomically read the index file. Returns [] when missing. */
|
|
59
|
+
export declare function readIndex(): CardMetadata[];
|
|
60
|
+
/** List all stored cards. Does NOT trigger biometric auth. */
|
|
61
|
+
export declare function listCards(): CardMetadata[];
|
|
62
|
+
/** Look up a card by id (or by case-insensitive nickname). Returns undefined when missing. */
|
|
63
|
+
export declare function findCard(idOrNickname: string): CardMetadata | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Add a new card. Validates Luhn + expiry. Returns the metadata for the
|
|
66
|
+
* stored card. The PAN/CVC/cardholder are written to Keychain in a single
|
|
67
|
+
* JSON blob; only metadata is mirrored to the index file.
|
|
68
|
+
*/
|
|
69
|
+
export declare function addCard(input: AddCardInput): CardMetadata;
|
|
70
|
+
/**
|
|
71
|
+
* Reveal a card by id. **Triggers Touch ID on macOS.** Throws if the card
|
|
72
|
+
* isn't in the index or if the keychain entry is missing/cancelled.
|
|
73
|
+
*/
|
|
74
|
+
export declare function showCard(idOrNickname: string): CardFull;
|
|
75
|
+
/** Delete a card from both the index and Keychain. Returns the removed metadata or undefined. */
|
|
76
|
+
export declare function removeCard(idOrNickname: string): CardMetadata | undefined;
|
|
77
|
+
/** Rename a card. Throws if the new nickname collides with an existing card. */
|
|
78
|
+
export declare function renameCard(idOrNickname: string, newNickname: string): CardMetadata;
|