@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/subagents.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
|
-
import { getSubagentsDir, getUserSubagentsDir } from './state.js';
|
|
12
|
+
import { getSubagentsDir, getUserSubagentsDir, getTrashSubagentsDir } from './state.js';
|
|
13
|
+
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
13
14
|
import { safeJoin } from './paths.js';
|
|
14
15
|
/**
|
|
15
16
|
* Parse AGENT.md frontmatter to extract subagent metadata
|
|
@@ -407,3 +408,99 @@ export function listSubagentsForAgent(agentId, home) {
|
|
|
407
408
|
}
|
|
408
409
|
return subagents;
|
|
409
410
|
}
|
|
411
|
+
// Agents that support subagents
|
|
412
|
+
const SUBAGENTS_CAPABLE_AGENTS = ['claude', 'openclaw'];
|
|
413
|
+
/**
|
|
414
|
+
* Compare a version home's subagents against discovered subagents.
|
|
415
|
+
* Returns orphan subagent names.
|
|
416
|
+
*/
|
|
417
|
+
export function diffVersionSubagents(agent, version) {
|
|
418
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
419
|
+
const orphans = [];
|
|
420
|
+
// Get all discovered subagent names
|
|
421
|
+
const discovered = new Set();
|
|
422
|
+
for (const dir of [getSubagentsDir(), getUserSubagentsDir()]) {
|
|
423
|
+
if (fs.existsSync(dir)) {
|
|
424
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
425
|
+
if (entry.isDirectory()) {
|
|
426
|
+
discovered.add(entry.name);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Check what's installed
|
|
432
|
+
if (agent === 'claude') {
|
|
433
|
+
const agentsDir = path.join(versionHome, '.claude', 'agents');
|
|
434
|
+
if (fs.existsSync(agentsDir)) {
|
|
435
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
436
|
+
if (!file.endsWith('.md'))
|
|
437
|
+
continue;
|
|
438
|
+
const name = path.basename(file, '.md');
|
|
439
|
+
if (!discovered.has(name)) {
|
|
440
|
+
orphans.push(name);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (agent === 'openclaw') {
|
|
446
|
+
const openclawDir = path.join(versionHome, '.openclaw');
|
|
447
|
+
if (fs.existsSync(openclawDir)) {
|
|
448
|
+
for (const entry of fs.readdirSync(openclawDir, { withFileTypes: true })) {
|
|
449
|
+
if (!entry.isDirectory())
|
|
450
|
+
continue;
|
|
451
|
+
if (!discovered.has(entry.name)) {
|
|
452
|
+
orphans.push(entry.name);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return { agent, version, orphans: orphans.sort() };
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Iterate all (agent, version) pairs that support subagents and are installed.
|
|
461
|
+
*/
|
|
462
|
+
export function iterSubagentsCapableVersions(filter) {
|
|
463
|
+
const pairs = [];
|
|
464
|
+
const agents = filter?.agent ? [filter.agent] : SUBAGENTS_CAPABLE_AGENTS;
|
|
465
|
+
for (const agent of agents) {
|
|
466
|
+
if (!SUBAGENTS_CAPABLE_AGENTS.includes(agent))
|
|
467
|
+
continue;
|
|
468
|
+
const versions = listInstalledVersions(agent);
|
|
469
|
+
for (const version of versions) {
|
|
470
|
+
if (filter?.version && filter.version !== version)
|
|
471
|
+
continue;
|
|
472
|
+
pairs.push({ agent, version });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return pairs;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Remove a single subagent from a specific version home.
|
|
479
|
+
* Soft-deletes to ~/.agents/.trash/subagents/.
|
|
480
|
+
*/
|
|
481
|
+
export function removeSubagentFromVersion(agent, version, subagentName) {
|
|
482
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
483
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
484
|
+
const trashDir = path.join(getTrashSubagentsDir(), agent, version, subagentName);
|
|
485
|
+
try {
|
|
486
|
+
if (agent === 'claude') {
|
|
487
|
+
const targetPath = path.join(versionHome, '.claude', 'agents', `${subagentName}.md`);
|
|
488
|
+
if (fs.existsSync(targetPath)) {
|
|
489
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
490
|
+
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (agent === 'openclaw') {
|
|
494
|
+
const targetDir = path.join(versionHome, '.openclaw', subagentName);
|
|
495
|
+
if (fs.existsSync(targetDir)) {
|
|
496
|
+
const trashDest = path.join(trashDir, stamp);
|
|
497
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
498
|
+
fs.renameSync(targetDir, trashDest);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { success: true };
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
return { success: false, error: err.message };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
@@ -47,7 +47,7 @@ interface RulesEntry {
|
|
|
47
47
|
/** Permissions: all group files across all scopes (merged). */
|
|
48
48
|
interface PermEntry {
|
|
49
49
|
groups: Record<string, FileEntry>;
|
|
50
|
-
|
|
50
|
+
permissionPreset: string | null;
|
|
51
51
|
}
|
|
52
52
|
export interface SyncManifest {
|
|
53
53
|
v: typeof MANIFEST_VERSION;
|
|
@@ -28,7 +28,7 @@ import { getVersionsDir, getProjectAgentsDir, getUserAgentsDir, getSkillsDir, ge
|
|
|
28
28
|
import { resolveResource } from './resources.js';
|
|
29
29
|
import { listMcpServerConfigs } from './mcp.js';
|
|
30
30
|
import { isRulesStale } from './rules/compile.js';
|
|
31
|
-
import {
|
|
31
|
+
import { getActivePermissionPresetName } from './permissions.js';
|
|
32
32
|
import { listInstalledSubagents } from './subagents.js';
|
|
33
33
|
import { safeJoin } from './paths.js';
|
|
34
34
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -341,7 +341,7 @@ export function buildManifest(agent, version, available, cwd) {
|
|
|
341
341
|
mcp,
|
|
342
342
|
permissions: {
|
|
343
343
|
groups: permGroups,
|
|
344
|
-
|
|
344
|
+
permissionPreset: getActivePermissionPresetName(),
|
|
345
345
|
},
|
|
346
346
|
subagents,
|
|
347
347
|
};
|
|
@@ -420,7 +420,7 @@ export function isSyncStale(manifest, available, agent, version, cwd) {
|
|
|
420
420
|
return true;
|
|
421
421
|
}
|
|
422
422
|
// ── Permissions ───────────────────────────────────────────────────────────
|
|
423
|
-
if (manifest.permissions.
|
|
423
|
+
if (manifest.permissions.permissionPreset !== getActivePermissionPresetName())
|
|
424
424
|
return true;
|
|
425
425
|
const currentGroups = collectPermissionGroupFiles();
|
|
426
426
|
if (nameSetDiffers(Object.keys(manifest.permissions.groups), Object.keys(currentGroups)))
|
|
@@ -13,7 +13,7 @@ import { homedir, tmpdir } from 'os';
|
|
|
13
13
|
import { constants as fsConstants } from 'fs';
|
|
14
14
|
import { randomBytes } from 'crypto';
|
|
15
15
|
import lockfile from 'proper-lockfile';
|
|
16
|
-
import {
|
|
16
|
+
import { getTeamsDir, getTeamsAgentsDir } from '../state.js';
|
|
17
17
|
/**
|
|
18
18
|
* Atomic JSON write: writes to a sibling tmp file then renames over the
|
|
19
19
|
* target. rename(2) is atomic on POSIX, so a crashed/interrupted write
|
|
@@ -62,8 +62,9 @@ async function withConfigLock(p, fn) {
|
|
|
62
62
|
}
|
|
63
63
|
// All supported teammate agent types
|
|
64
64
|
const ALL_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode'];
|
|
65
|
-
// Teams
|
|
66
|
-
|
|
65
|
+
// Teams config + registry live under ~/.agents/teams/ (definitions);
|
|
66
|
+
// per-run agent execution dirs live under ~/.agents/.history/teams/agents/.
|
|
67
|
+
const TEAMS_DIR = getTeamsDir();
|
|
67
68
|
// Legacy paths (for migration)
|
|
68
69
|
const LEGACY_CONFIG_DIR = path.join(homedir(), '.agents');
|
|
69
70
|
// Legacy migration from pre-OSS brand; safe to remove after 2026-07
|
|
@@ -100,8 +101,17 @@ export async function resolveBaseDir() {
|
|
|
100
101
|
throw new Error('Unable to determine a writable data directory for teams');
|
|
101
102
|
}
|
|
102
103
|
async function resolveAgentsPath() {
|
|
103
|
-
const
|
|
104
|
-
|
|
104
|
+
const historyAgents = getTeamsAgentsDir();
|
|
105
|
+
if (await ensureWritableDir(historyAgents)) {
|
|
106
|
+
return historyAgents;
|
|
107
|
+
}
|
|
108
|
+
// Last-resort temp fallback so dispatch keeps working when ~/.agents is unwritable.
|
|
109
|
+
const tmpAgents = path.join(TMP_FALLBACK_DIR, 'agents');
|
|
110
|
+
if (await ensureWritableDir(tmpAgents)) {
|
|
111
|
+
console.warn(`[agents teams] Falling back to temp agents dir at ${tmpAgents}`);
|
|
112
|
+
return tmpAgents;
|
|
113
|
+
}
|
|
114
|
+
throw new Error('Unable to determine a writable agents directory');
|
|
105
115
|
}
|
|
106
116
|
async function resolveConfigPath() {
|
|
107
117
|
await fs.mkdir(TEAMS_DIR, { recursive: true });
|
|
@@ -10,9 +10,9 @@ import * as fsSync from 'fs';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { randomBytes } from 'crypto';
|
|
12
12
|
import lockfile from 'proper-lockfile';
|
|
13
|
-
import {
|
|
13
|
+
import { getTeamsDir } from '../state.js';
|
|
14
14
|
async function registryPath() {
|
|
15
|
-
return path.join(
|
|
15
|
+
return path.join(getTeamsDir(), 'registry.json');
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Atomic JSON write: writes to a unique sibling tmp file then renames over
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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 MIRROR_SYSTEM_REPO = "gh:
|
|
189
|
+
export declare const DEFAULT_SYSTEM_REPO = "gh:phnx-labs/.agents-system";
|
|
190
|
+
/** Legacy system repo — kept so existing installs still recognize their origin. */
|
|
191
|
+
export declare const MIRROR_SYSTEM_REPO = "gh:muqsitnawaz/.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. */
|
|
@@ -378,6 +378,35 @@ export interface Meta {
|
|
|
378
378
|
* once. Tracked so a user `registry remove` won't silently re-seed.
|
|
379
379
|
*/
|
|
380
380
|
seededPresets?: string[];
|
|
381
|
+
/**
|
|
382
|
+
* Hook manifest entries keyed by hook name. Folded into agents.yaml so the
|
|
383
|
+
* user has a single file to sync. Each entry shape matches ManifestHook
|
|
384
|
+
* (script, events, timeout, matches, enabled).
|
|
385
|
+
*/
|
|
386
|
+
hooks?: Record<string, ManifestHook>;
|
|
387
|
+
/**
|
|
388
|
+
* Browser profile definitions keyed by profile name. Portable user config
|
|
389
|
+
* that syncs with `agents repo push/pull`. Runtime state (chrome-data, pids)
|
|
390
|
+
* lives separately in ~/.agents/.cache/browser/<profile>/.
|
|
391
|
+
*/
|
|
392
|
+
browser?: Record<string, BrowserProfileConfig>;
|
|
393
|
+
}
|
|
394
|
+
/** Browser profile definition stored in agents.yaml. */
|
|
395
|
+
export interface BrowserProfileConfig {
|
|
396
|
+
description?: string;
|
|
397
|
+
browser: 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'custom';
|
|
398
|
+
binary?: string;
|
|
399
|
+
electron?: boolean;
|
|
400
|
+
endpoints: string[];
|
|
401
|
+
chrome?: {
|
|
402
|
+
headless?: boolean;
|
|
403
|
+
args?: string[];
|
|
404
|
+
};
|
|
405
|
+
secrets?: string;
|
|
406
|
+
viewport?: {
|
|
407
|
+
width: number;
|
|
408
|
+
height: number;
|
|
409
|
+
};
|
|
381
410
|
}
|
|
382
411
|
/** Options controlling which agents and resources are synced during `agents pull` / `agents use`. */
|
|
383
412
|
export interface SyncOptions {
|
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 MIRROR_SYSTEM_REPO = 'gh:
|
|
9
|
+
export const DEFAULT_SYSTEM_REPO = 'gh:phnx-labs/.agents-system';
|
|
10
|
+
/** Legacy system repo — kept so existing installs still recognize their origin. */
|
|
11
|
+
export const MIRROR_SYSTEM_REPO = 'gh:muqsitnawaz/.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/usage.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ export declare function getUsageInfoForIdentity(input: UsageIdentityInput): Prom
|
|
|
79
79
|
export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number): string;
|
|
80
80
|
/** Format a multi-line usage section for detailed agent views. */
|
|
81
81
|
export declare function formatUsageSection(usage: UsageInfo): string[];
|
|
82
|
-
/** Load Claude OAuth credentials from the
|
|
82
|
+
/** Load Claude OAuth credentials from the system keychain/keyring. */
|
|
83
83
|
export declare function loadClaudeOauth(home?: string): Promise<ClaudeOauthCredentials | null>;
|
|
84
84
|
/**
|
|
85
85
|
* Derive the Keychain service name for a Claude home directory.
|
package/dist/lib/usage.js
CHANGED
|
@@ -15,7 +15,8 @@ import * as readline from 'readline';
|
|
|
15
15
|
import { promisify } from 'util';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import { walkForFiles } from './fs-walk.js';
|
|
18
|
-
import {
|
|
18
|
+
import { getKeychainToken, setKeychainToken, deleteKeychainToken, } from './secrets/index.js';
|
|
19
|
+
import { getCacheDir } from './state.js';
|
|
19
20
|
const execFileAsync = promisify(execFile);
|
|
20
21
|
const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
21
22
|
const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
@@ -30,7 +31,7 @@ const CLAUDE_SCOPES = [
|
|
|
30
31
|
'user:file_upload',
|
|
31
32
|
];
|
|
32
33
|
const CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
33
|
-
const getClaudeUsageCachePath = () => path.join(
|
|
34
|
+
const getClaudeUsageCachePath = () => path.join(getCacheDir(), 'claude-usage.json');
|
|
34
35
|
const CACHED_CLAUDE_USAGE_SOURCE_LABEL = 'last seen live account data';
|
|
35
36
|
const COMPACT_BAR_LEN = 5;
|
|
36
37
|
const USAGE_BAR_LEN = 10;
|
|
@@ -343,27 +344,15 @@ function normalizeClaudeWindow(window, key, label, shortLabel) {
|
|
|
343
344
|
windowMinutes: inferWindowMinutes(key),
|
|
344
345
|
};
|
|
345
346
|
}
|
|
346
|
-
|
|
347
|
-
/** Load Claude OAuth credentials from the macOS Keychain. */
|
|
347
|
+
/** Load Claude OAuth credentials from the system keychain/keyring. */
|
|
348
348
|
export async function loadClaudeOauth(home) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
process.stderr.write('[agents] Usage tracking requires macOS Keychain. Skipped on this platform.\n');
|
|
352
|
-
warnedNonDarwin = true;
|
|
353
|
-
}
|
|
349
|
+
// Windows not yet supported
|
|
350
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
354
351
|
return null;
|
|
355
352
|
}
|
|
356
353
|
try {
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
'find-generic-password',
|
|
360
|
-
'-a',
|
|
361
|
-
account,
|
|
362
|
-
'-s',
|
|
363
|
-
// Managed Claude homes must stay pinned to their own service name.
|
|
364
|
-
getClaudeKeychainService(home),
|
|
365
|
-
'-w',
|
|
366
|
-
]);
|
|
354
|
+
const service = getClaudeKeychainService(home);
|
|
355
|
+
const stdout = getKeychainToken(service);
|
|
367
356
|
const payload = JSON.parse(stdout.trim());
|
|
368
357
|
if (typeof payload?.claudeAiOauth?.accessToken !== 'string')
|
|
369
358
|
return null;
|
|
@@ -380,27 +369,20 @@ export async function loadClaudeOauth(home) {
|
|
|
380
369
|
}
|
|
381
370
|
}
|
|
382
371
|
/**
|
|
383
|
-
* Save Claude OAuth credentials to the
|
|
372
|
+
* Save Claude OAuth credentials to the system keychain/keyring.
|
|
384
373
|
* Reads the existing payload, merges the new OAuth fields, and writes back.
|
|
385
374
|
*/
|
|
386
375
|
async function saveClaudeOauth(home, credentials) {
|
|
387
|
-
|
|
376
|
+
// Windows not yet supported
|
|
377
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
388
378
|
return false;
|
|
389
379
|
}
|
|
390
380
|
try {
|
|
391
|
-
const account = os.userInfo().username;
|
|
392
381
|
const service = getClaudeKeychainService(home);
|
|
393
382
|
// Read existing payload to preserve other fields
|
|
394
383
|
let existingPayload = {};
|
|
395
384
|
try {
|
|
396
|
-
const
|
|
397
|
-
'find-generic-password',
|
|
398
|
-
'-a',
|
|
399
|
-
account,
|
|
400
|
-
'-s',
|
|
401
|
-
service,
|
|
402
|
-
'-w',
|
|
403
|
-
]);
|
|
385
|
+
const stdout = getKeychainToken(service);
|
|
404
386
|
existingPayload = JSON.parse(stdout.trim());
|
|
405
387
|
}
|
|
406
388
|
catch {
|
|
@@ -418,29 +400,14 @@ async function saveClaudeOauth(home, credentials) {
|
|
|
418
400
|
},
|
|
419
401
|
};
|
|
420
402
|
const payloadJson = JSON.stringify(newPayload);
|
|
421
|
-
// Delete existing entry first
|
|
403
|
+
// Delete existing entry first, then add updated entry
|
|
422
404
|
try {
|
|
423
|
-
|
|
424
|
-
'delete-generic-password',
|
|
425
|
-
'-a',
|
|
426
|
-
account,
|
|
427
|
-
'-s',
|
|
428
|
-
service,
|
|
429
|
-
]);
|
|
405
|
+
deleteKeychainToken(service);
|
|
430
406
|
}
|
|
431
407
|
catch {
|
|
432
408
|
// Entry might not exist, ignore
|
|
433
409
|
}
|
|
434
|
-
|
|
435
|
-
await execFileAsync('security', [
|
|
436
|
-
'add-generic-password',
|
|
437
|
-
'-a',
|
|
438
|
-
account,
|
|
439
|
-
'-s',
|
|
440
|
-
service,
|
|
441
|
-
'-w',
|
|
442
|
-
payloadJson,
|
|
443
|
-
]);
|
|
410
|
+
setKeychainToken(service, payloadJson);
|
|
444
411
|
return true;
|
|
445
412
|
}
|
|
446
413
|
catch {
|
package/dist/lib/versions.js
CHANGED
|
@@ -23,10 +23,10 @@ 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, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
|
|
26
|
+
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, clearVersionResources, recordVersionResources, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, 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
|
-
import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_AGENTS, discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME,
|
|
29
|
+
import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_AGENTS, discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
30
30
|
import { installMcpServers, parseMcpServerConfig } from './mcp.js';
|
|
31
31
|
import { markdownToToml } from './convert.js';
|
|
32
32
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
@@ -192,10 +192,9 @@ export function getAvailableResources(cwd = process.cwd()) {
|
|
|
192
192
|
// Plugins (directories with .claude-plugin/plugin.json)
|
|
193
193
|
const allPlugins = discoverPlugins();
|
|
194
194
|
result.plugins = allPlugins.map(p => p.name);
|
|
195
|
-
// Promptcuts —
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
result.promptcuts = fs.existsSync(getPromptcutsPath());
|
|
195
|
+
// Promptcuts — present if either layer exists. Reads merge user + system
|
|
196
|
+
// with user precedence (see readMergedPromptcuts); writes always go to user.
|
|
197
|
+
result.promptcuts = fs.existsSync(getUserPromptcutsPath()) || fs.existsSync(getPromptcutsPath());
|
|
199
198
|
return result;
|
|
200
199
|
}
|
|
201
200
|
// Files/dirs that are never synced into a version home (OS metadata, local tooling).
|
|
@@ -676,14 +675,14 @@ export async function promptResourceSelection(agent) {
|
|
|
676
675
|
];
|
|
677
676
|
const availableCategories = categories.filter(c => c.available);
|
|
678
677
|
if (availableCategories.length === 0) {
|
|
679
|
-
console.log(chalk.gray('No resources available
|
|
678
|
+
console.log(chalk.gray('No resources available to sync.'));
|
|
680
679
|
return {};
|
|
681
680
|
}
|
|
682
681
|
// Step 1: Select categories (with "Select All" shortcut at the top)
|
|
683
682
|
console.log();
|
|
684
683
|
const SELECT_ALL_KEY = '__select_all__';
|
|
685
684
|
const selectedCategories = await checkbox({
|
|
686
|
-
message: 'Which resources
|
|
685
|
+
message: 'Which resources would you like to sync?',
|
|
687
686
|
choices: [
|
|
688
687
|
{ name: chalk.bold('Select All (sync everything)'), value: SELECT_ALL_KEY, checked: false },
|
|
689
688
|
...availableCategories.map(c => ({
|
|
@@ -945,6 +944,17 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
945
944
|
throw new Error(`Invalid version: ${JSON.stringify(version)}`);
|
|
946
945
|
}
|
|
947
946
|
try {
|
|
947
|
+
// Check npm is available
|
|
948
|
+
try {
|
|
949
|
+
await execFileAsync('which', ['npm']);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return {
|
|
953
|
+
success: false,
|
|
954
|
+
installedVersion: version,
|
|
955
|
+
error: 'npm is not installed. Install Node.js and npm first: https://nodejs.org/',
|
|
956
|
+
};
|
|
957
|
+
}
|
|
948
958
|
onProgress?.(`Installing ${packageSpec}...`);
|
|
949
959
|
const { stdout } = await execFileAsync('npm', ['install', packageSpec], { cwd: versionDir });
|
|
950
960
|
// Determine the actual installed version
|
|
@@ -1178,7 +1188,7 @@ export function resolveVersionAliasLoose(agent, raw) {
|
|
|
1178
1188
|
* Get version specified in a project-root agents.yaml (not the user ~/.agents-system/agents.yaml).
|
|
1179
1189
|
*/
|
|
1180
1190
|
export function getProjectVersion(agent, startPath) {
|
|
1181
|
-
const userAgentsYaml = path.join(
|
|
1191
|
+
const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
1182
1192
|
let dir = path.resolve(startPath);
|
|
1183
1193
|
while (dir !== path.dirname(dir)) {
|
|
1184
1194
|
const manifestPath = path.join(dir, 'agents.yaml');
|
|
@@ -1630,39 +1640,39 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1630
1640
|
}
|
|
1631
1641
|
// Apply permissions (if agent supports them).
|
|
1632
1642
|
// Groups live in ~/.agents/permissions/groups/. Optional recipes in
|
|
1633
|
-
// ~/.agents/permissions/
|
|
1634
|
-
// If
|
|
1643
|
+
// ~/.agents/permissions/presets/<name>.yaml pick a subset via `includes:`.
|
|
1644
|
+
// If AGENTS_PERMISSION_PRESET is set, we resolve that recipe and use its
|
|
1635
1645
|
// includes list as the group filter (intersected with groups on disk).
|
|
1636
1646
|
const permissionGroups = discoverPermissionGroups();
|
|
1637
1647
|
const allGroupNames = permissionGroups.map(g => g.name);
|
|
1638
|
-
const
|
|
1639
|
-
let
|
|
1640
|
-
if (
|
|
1641
|
-
const recipe =
|
|
1648
|
+
const activePresetName = getActivePermissionPresetName();
|
|
1649
|
+
let presetFilteredGroups = null;
|
|
1650
|
+
if (activePresetName) {
|
|
1651
|
+
const recipe = readPermissionPresetRecipe(activePresetName);
|
|
1642
1652
|
if (recipe) {
|
|
1643
1653
|
const available = new Set(allGroupNames);
|
|
1644
|
-
|
|
1654
|
+
presetFilteredGroups = recipe.includes.filter(g => available.has(g));
|
|
1645
1655
|
}
|
|
1646
1656
|
else {
|
|
1647
|
-
console.warn(`${
|
|
1657
|
+
console.warn(`${PERMISSION_PRESET_ENV_VAR}=${activePresetName} but no recipe at ~/.agents/permissions/presets/${activePresetName}.yaml — falling back to all groups`);
|
|
1648
1658
|
}
|
|
1649
1659
|
}
|
|
1650
1660
|
let permsToSync;
|
|
1651
1661
|
if (selection) {
|
|
1652
1662
|
permsToSync = resolveSelection(selection.permissions, allGroupNames);
|
|
1653
|
-
// If a
|
|
1663
|
+
// If a preset recipe is active, the recipe's includes list always wins —
|
|
1654
1664
|
// even when the caller passed an explicit array via selection. Without
|
|
1655
1665
|
// this intersection, `agents add`'s buildAutomaticSelection would pass
|
|
1656
1666
|
// every group name discovered on disk (including 99-deny), bypassing
|
|
1657
1667
|
// the sandbox filter.
|
|
1658
|
-
if (
|
|
1659
|
-
const filterSet = new Set(
|
|
1668
|
+
if (presetFilteredGroups) {
|
|
1669
|
+
const filterSet = new Set(presetFilteredGroups);
|
|
1660
1670
|
permsToSync = permsToSync.filter(g => filterSet.has(g));
|
|
1661
1671
|
}
|
|
1662
1672
|
}
|
|
1663
1673
|
else {
|
|
1664
1674
|
permsToSync = PERMISSIONS_CAPABLE_AGENTS.includes(agent)
|
|
1665
|
-
? (
|
|
1675
|
+
? (presetFilteredGroups ?? allGroupNames)
|
|
1666
1676
|
: [];
|
|
1667
1677
|
}
|
|
1668
1678
|
if (permsToSync.length > 0 && PERMISSIONS_CAPABLE_AGENTS.includes(agent)) {
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -13,8 +13,15 @@ const SHIMS_DIR = path.join(HOME, '.agents-system', 'shims');
|
|
|
13
13
|
const SYSTEM_DIR = path.join(HOME, '.agents-system');
|
|
14
14
|
const USER_DIR = path.join(HOME, '.agents');
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
|
|
16
|
+
// For local installs, create directories and show a message
|
|
17
|
+
const isGlobalInstall = process.env.npm_config_global || process.argv.includes('-g');
|
|
18
|
+
if (!isGlobalInstall) {
|
|
19
|
+
// Still create user directories for local installs
|
|
20
|
+
fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
|
|
21
|
+
console.log(`
|
|
22
|
+
agents-cli installed locally.
|
|
23
|
+
To complete setup, run: npx agents setup
|
|
24
|
+
`);
|
|
18
25
|
process.exit(0);
|
|
19
26
|
}
|
|
20
27
|
|
|
@@ -165,17 +172,38 @@ async function main() {
|
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
// Default:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
// Default: offer to auto-add shims to PATH (like homebrew does)
|
|
176
|
+
const rcFile = getShellRc();
|
|
177
|
+
let alreadyConfigured = false;
|
|
178
|
+
if (fs.existsSync(rcFile)) {
|
|
179
|
+
const content = fs.readFileSync(rcFile, 'utf-8');
|
|
180
|
+
alreadyConfigured = content.includes('.agents-system/shims');
|
|
181
|
+
}
|
|
172
182
|
|
|
173
|
-
|
|
183
|
+
console.log(`\nagents-cli installed.`);
|
|
174
184
|
|
|
175
|
-
(
|
|
185
|
+
if (!alreadyConfigured && process.stdin.isTTY && process.stdout.isTTY) {
|
|
186
|
+
const answer = await ask(`\nAdd shims to PATH in ~/${path.basename(rcFile)}? [Y/n] `);
|
|
187
|
+
if (answer === '' || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
188
|
+
const addition = `\n# agents-cli: version switching for AI coding agents\n${exportLine}\n`;
|
|
189
|
+
fs.mkdirSync(path.dirname(rcFile), { recursive: true });
|
|
190
|
+
fs.appendFileSync(rcFile, addition);
|
|
191
|
+
console.log(`\n Added ${SHIMS_DIR} to PATH in ${path.basename(rcFile)}`);
|
|
192
|
+
console.log(` Restart your shell or run: source ~/${path.basename(rcFile)}\n`);
|
|
193
|
+
} else {
|
|
194
|
+
console.log(`
|
|
195
|
+
To enable version-aware shims, add this to your shell config:
|
|
176
196
|
|
|
177
|
-
|
|
197
|
+
${exportLine}
|
|
178
198
|
`);
|
|
199
|
+
}
|
|
200
|
+
} else if (!alreadyConfigured) {
|
|
201
|
+
console.log(`
|
|
202
|
+
To enable version-aware shims, add this to your shell config:
|
|
203
|
+
|
|
204
|
+
${exportLine}
|
|
205
|
+
`);
|
|
206
|
+
}
|
|
179
207
|
|
|
180
208
|
const choice = await promptForAliases();
|
|
181
209
|
if (choice === 'install') {
|