@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
package/dist/lib/plugins.js
CHANGED
|
@@ -105,6 +105,29 @@ export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user
|
|
|
105
105
|
hasSettings: pluginHasNonPermissionSettings(pluginRoot),
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Ordered, non-empty resource groups a plugin packages. Single source of truth
|
|
110
|
+
* for the breakdown shown by the plugin picker, `agents inspect --plugins`, and
|
|
111
|
+
* its detail view. Empty categories are omitted; `settings` appears only when
|
|
112
|
+
* the plugin merges non-permission settings.
|
|
113
|
+
*/
|
|
114
|
+
export function pluginResourceGroups(plugin) {
|
|
115
|
+
const groups = [
|
|
116
|
+
{ label: 'skills', items: plugin.skills.map((s) => `/${plugin.name}:${s}`) },
|
|
117
|
+
{ label: 'commands', items: plugin.commands.map((c) => `/${plugin.name}:${c}`) },
|
|
118
|
+
{ label: 'subagents', items: plugin.agentDefs },
|
|
119
|
+
{ label: 'hooks', items: plugin.hooks },
|
|
120
|
+
{ label: 'mcp', items: plugin.mcpServers },
|
|
121
|
+
{ label: 'lsp', items: plugin.lspServers },
|
|
122
|
+
{ label: 'monitors', items: plugin.monitors },
|
|
123
|
+
{ label: 'bin', items: plugin.bin },
|
|
124
|
+
{ label: 'scripts', items: plugin.scripts },
|
|
125
|
+
];
|
|
126
|
+
const out = groups.filter((g) => g.items.length > 0);
|
|
127
|
+
if (plugin.hasSettings)
|
|
128
|
+
out.push({ label: 'settings', items: ['settings.json'] });
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
108
131
|
export function inspectPluginCapabilities(pluginRoot) {
|
|
109
132
|
const manifest = loadPluginManifest(pluginRoot);
|
|
110
133
|
const plugin = manifest ? buildDiscoveredPlugin(pluginRoot, manifest) : null;
|
|
@@ -190,13 +213,25 @@ function discoverPluginSkills(pluginRoot) {
|
|
|
190
213
|
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
191
214
|
.map(d => d.name);
|
|
192
215
|
}
|
|
193
|
-
|
|
216
|
+
/**
|
|
217
|
+
* The lifecycle events a plugin hooks into, read from hooks/hooks.json.
|
|
218
|
+
*
|
|
219
|
+
* The official plugin format wraps the event map under a `hooks` key
|
|
220
|
+
* (`{ description, hooks: { SessionStart: [...], PreToolUse: [...] } }`), so the
|
|
221
|
+
* meaningful keys are the events — NOT the top-level keys (`description`,
|
|
222
|
+
* `hooks`). Older/flat files put the event names at the top level directly; we
|
|
223
|
+
* read whichever object actually holds the event map.
|
|
224
|
+
*/
|
|
225
|
+
export function discoverPluginHooks(pluginRoot) {
|
|
194
226
|
const hooksFile = path.join(pluginRoot, 'hooks', 'hooks.json');
|
|
195
227
|
if (!fs.existsSync(hooksFile))
|
|
196
228
|
return [];
|
|
197
229
|
try {
|
|
198
230
|
const content = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
|
|
199
|
-
|
|
231
|
+
const eventMap = content.hooks && typeof content.hooks === 'object' && !Array.isArray(content.hooks)
|
|
232
|
+
? content.hooks
|
|
233
|
+
: content;
|
|
234
|
+
return Object.keys(eventMap);
|
|
200
235
|
}
|
|
201
236
|
catch {
|
|
202
237
|
return [];
|
|
@@ -47,6 +47,7 @@ import * as path from 'path';
|
|
|
47
47
|
import { supports } from './capabilities.js';
|
|
48
48
|
import { getEnabledExtraRepos, getExtraPluginsDir, getPluginsDir, getProjectAgentsDir, getProjectPluginsDir, getSystemPluginsDir, } from './state.js';
|
|
49
49
|
import { getVersionHomePath } from './versions.js';
|
|
50
|
+
import { transformSubagentForClaude } from './subagents.js';
|
|
50
51
|
import { compileRulesForProject } from './rules/compile.js';
|
|
51
52
|
import { discoverPluginsInDir, hasPluginExecSurfaces, inspectPluginCapabilities } from './plugins.js';
|
|
52
53
|
import { MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, addPluginToSettings, copyPluginToMarketplace, marketplaceNameFor, marketplaceRoot, pluginInstallDir, registerMarketplace, removePluginFromSettings, syncMarketplaceManifest, } from './plugin-marketplace.js';
|
|
@@ -114,10 +115,22 @@ function touchLaunchSentinel(agent, version, cwd) {
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
const CLAUDE_MIRROR_PLANS = [
|
|
117
|
-
{ srcSubdir: 'subagents', destSubdir: 'agents',
|
|
118
|
-
{ srcSubdir: 'commands', destSubdir: 'commands',
|
|
119
|
-
{ srcSubdir: 'skills', destSubdir: 'skills',
|
|
118
|
+
{ srcSubdir: 'subagents', destSubdir: 'agents', mode: 'subagent-write' },
|
|
119
|
+
{ srcSubdir: 'commands', destSubdir: 'commands', mode: 'file-symlink' },
|
|
120
|
+
{ srcSubdir: 'skills', destSubdir: 'skills', mode: 'dir-symlink' },
|
|
120
121
|
];
|
|
122
|
+
/**
|
|
123
|
+
* Marker prepended-as-trailing-comment to every subagent file WE generate.
|
|
124
|
+
* It's an HTML comment — invisible to the markdown the agent reads — placed on
|
|
125
|
+
* the last line so it never disturbs the leading `---` frontmatter block.
|
|
126
|
+
*
|
|
127
|
+
* Ownership rule (the one don't-clobber decision for written, non-symlink
|
|
128
|
+
* files): we only overwrite a `.claude/agents/<name>.md` whose content carries
|
|
129
|
+
* this marker. A user-authored file (no marker) or a symlink at the dest is
|
|
130
|
+
* left untouched. A marker beats an mtime/sidecar sentinel because it travels
|
|
131
|
+
* with the file across copies and git, and needs no out-of-band state.
|
|
132
|
+
*/
|
|
133
|
+
const GENERATED_SUBAGENT_MARKER = '<!-- agents-cli:generated-subagent';
|
|
121
134
|
function mirrorWorkspaceResources(cwd, agent) {
|
|
122
135
|
// v1: claude-only. Other agents have workspace conventions we haven't
|
|
123
136
|
// mapped (amp: ~/.config/amp; antigravity: ~/.gemini/antigravity-cli;
|
|
@@ -147,13 +160,20 @@ function mirrorWorkspaceResources(cwd, agent) {
|
|
|
147
160
|
continue;
|
|
148
161
|
const destDir = path.join(agentWorkspaceDir, plan.destSubdir);
|
|
149
162
|
fs.mkdirSync(destDir, { recursive: true });
|
|
163
|
+
// Subagents flatten N source files into one written .md — not a symlink.
|
|
164
|
+
if (plan.mode === 'subagent-write') {
|
|
165
|
+
const r = writeProjectSubagents(srcDir, destDir, cwd);
|
|
166
|
+
links += r.links;
|
|
167
|
+
skipped.push(...r.skipped);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
150
170
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
151
171
|
for (const entry of entries) {
|
|
152
172
|
if (entry.name.startsWith('.'))
|
|
153
173
|
continue;
|
|
154
|
-
if (plan.
|
|
174
|
+
if (plan.mode === 'dir-symlink' && !entry.isDirectory())
|
|
155
175
|
continue;
|
|
156
|
-
if (
|
|
176
|
+
if (plan.mode === 'file-symlink' && !entry.isFile() && !entry.isSymbolicLink())
|
|
157
177
|
continue;
|
|
158
178
|
const srcPath = path.join(srcDir, entry.name);
|
|
159
179
|
const destPath = path.join(destDir, entry.name);
|
|
@@ -167,6 +187,91 @@ function mirrorWorkspaceResources(cwd, agent) {
|
|
|
167
187
|
}
|
|
168
188
|
return { links, skipped };
|
|
169
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Mirror project subagents into `<cwd>/.claude/agents/`. The canonical source
|
|
192
|
+
* shape is a DIRECTORY containing AGENT.md (e.g. `.agents/subagents/probe/AGENT.md`)
|
|
193
|
+
* — confirmed by the detector (versions.ts) and lister (subagents.ts). Each
|
|
194
|
+
* such directory is flattened via transformSubagentForClaude (the exact writer
|
|
195
|
+
* the version-home sync uses) into a single `<name>.md`, then written under an
|
|
196
|
+
* ownership marker so a re-launch refreshes our file but never clobbers a
|
|
197
|
+
* user-authored one.
|
|
198
|
+
*
|
|
199
|
+
* Returns the same {links, skipped} shape the symlink path reports, so the
|
|
200
|
+
* caller's accounting is uniform across resource kinds.
|
|
201
|
+
*/
|
|
202
|
+
function writeProjectSubagents(srcDir, destDir, cwd) {
|
|
203
|
+
let links = 0;
|
|
204
|
+
const skipped = [];
|
|
205
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
206
|
+
if (entry.name.startsWith('.'))
|
|
207
|
+
continue;
|
|
208
|
+
if (!entry.isDirectory())
|
|
209
|
+
continue;
|
|
210
|
+
const subagentDir = path.join(srcDir, entry.name);
|
|
211
|
+
if (!fs.existsSync(path.join(subagentDir, 'AGENT.md')))
|
|
212
|
+
continue;
|
|
213
|
+
const destPath = path.join(destDir, `${entry.name}.md`);
|
|
214
|
+
if (writeSubagentIfOwned(subagentDir, destPath)) {
|
|
215
|
+
links += 1;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
skipped.push(path.relative(cwd, destPath));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { links, skipped };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Write a flattened subagent file at `destPath`, refusing to clobber user state.
|
|
225
|
+
*
|
|
226
|
+
* - dest missing → write fresh.
|
|
227
|
+
* - dest is our generation → overwrite (refresh; carries GENERATED_SUBAGENT_MARKER).
|
|
228
|
+
* - dest is a symlink / any
|
|
229
|
+
* non-regular file → SKIP (user state we don't own).
|
|
230
|
+
* - dest is a regular file
|
|
231
|
+
* without our marker → SKIP (hand-authored .claude/agents/<name>.md).
|
|
232
|
+
*
|
|
233
|
+
* Returns true when our file is present (written now or already current),
|
|
234
|
+
* false when we left a user-owned dest alone.
|
|
235
|
+
*/
|
|
236
|
+
function writeSubagentIfOwned(subagentDir, destPath) {
|
|
237
|
+
let existing = null;
|
|
238
|
+
let destLstat = null;
|
|
239
|
+
try {
|
|
240
|
+
destLstat = fs.lstatSync(destPath);
|
|
241
|
+
}
|
|
242
|
+
catch { /* missing — write fresh */ }
|
|
243
|
+
if (destLstat) {
|
|
244
|
+
if (!destLstat.isFile())
|
|
245
|
+
return false; // symlink/dir/etc. — user state
|
|
246
|
+
try {
|
|
247
|
+
existing = fs.readFileSync(destPath, 'utf-8');
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
if (!existing.includes(GENERATED_SUBAGENT_MARKER))
|
|
253
|
+
return false; // hand-authored
|
|
254
|
+
}
|
|
255
|
+
let body;
|
|
256
|
+
try {
|
|
257
|
+
body = transformSubagentForClaude(subagentDir);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return false; // malformed AGENT.md — don't write a broken file
|
|
261
|
+
}
|
|
262
|
+
const content = `${body}\n\n${GENERATED_SUBAGENT_MARKER} — edit .agents/subagents/${path.basename(subagentDir)}/ instead -->\n`;
|
|
263
|
+
// Skip-fast: identical content already on disk → no write (keeps mtime stable).
|
|
264
|
+
if (existing === content)
|
|
265
|
+
return true;
|
|
266
|
+
try {
|
|
267
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
268
|
+
fs.writeFileSync(destPath, content);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
170
275
|
/**
|
|
171
276
|
* Create or refresh a symlink at `destPath` pointing at `srcPath`. Returns
|
|
172
277
|
* true if we wrote (or already had) the link, false if we skipped because
|
package/dist/lib/registry.js
CHANGED
|
@@ -61,6 +61,13 @@ export function removeRegistry(type, name) {
|
|
|
61
61
|
}
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Cap every registry network call. Without this a slow or unreachable registry
|
|
66
|
+
* hangs the calling command indefinitely (`agents add`, `agents mcp`, package
|
|
67
|
+
* resolution) — and makes CI flake when the registry is unreachable. On timeout
|
|
68
|
+
* the fetch aborts, callers fall back to their git/no-match path.
|
|
69
|
+
*/
|
|
70
|
+
const REGISTRY_FETCH_TIMEOUT_MS = 8000;
|
|
64
71
|
async function fetchMcpRegistry(url, query, limit = 20, apiKey) {
|
|
65
72
|
const params = new URLSearchParams();
|
|
66
73
|
if (query)
|
|
@@ -73,7 +80,10 @@ async function fetchMcpRegistry(url, query, limit = 20, apiKey) {
|
|
|
73
80
|
if (apiKey) {
|
|
74
81
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
75
82
|
}
|
|
76
|
-
const response = await fetch(fullUrl, {
|
|
83
|
+
const response = await fetch(fullUrl, {
|
|
84
|
+
headers,
|
|
85
|
+
signal: AbortSignal.timeout(REGISTRY_FETCH_TIMEOUT_MS),
|
|
86
|
+
});
|
|
77
87
|
if (!response.ok) {
|
|
78
88
|
throw new Error(`Registry request failed: ${response.status} ${response.statusText}`);
|
|
79
89
|
}
|
|
@@ -191,7 +201,10 @@ async function fetchSkillIndex(url, apiKey) {
|
|
|
191
201
|
const headers = { Accept: 'application/json' };
|
|
192
202
|
if (apiKey)
|
|
193
203
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
194
|
-
const response = await fetch(url, {
|
|
204
|
+
const response = await fetch(url, {
|
|
205
|
+
headers,
|
|
206
|
+
signal: AbortSignal.timeout(REGISTRY_FETCH_TIMEOUT_MS),
|
|
207
|
+
});
|
|
195
208
|
if (!response.ok) {
|
|
196
209
|
throw new Error(`Registry request failed: ${response.status} ${response.statusText}`);
|
|
197
210
|
}
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface RotateCandidate {
|
|
|
11
11
|
agent: AgentId;
|
|
12
12
|
version: string;
|
|
13
13
|
email: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Per-org usage/quota key (e.g. `claude:org=<orgUuid>`) — the unit rate
|
|
16
|
+
* limits are actually measured in. Distinct orgs signed in under the same
|
|
17
|
+
* email have distinct keys, so this is the correct dedup boundary; null when
|
|
18
|
+
* no usage identity is available (then we fall back to email).
|
|
19
|
+
*/
|
|
20
|
+
usageKey: string | null;
|
|
14
21
|
usageStatus: AccountInfo['usageStatus'];
|
|
15
22
|
usageSnapshot: UsageSnapshot | null;
|
|
16
23
|
authValid: boolean;
|
package/dist/lib/rotate.js
CHANGED
|
@@ -105,19 +105,29 @@ function compareCandidates(a, b) {
|
|
|
105
105
|
return ta - tb;
|
|
106
106
|
return Math.random() - 0.5;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Identity a candidate dedups on. Quota is tracked per-org, so two versions
|
|
110
|
+
* that share an org are the same rate-limit bucket and must collapse — but two
|
|
111
|
+
* orgs under the same email (e.g. Enterprise + Personal on one Google identity)
|
|
112
|
+
* are genuinely separate buckets and must stay distinct. Prefer the org usage
|
|
113
|
+
* key; fall back to email only when no usage identity is available.
|
|
114
|
+
*/
|
|
115
|
+
function candidateIdentity(c) {
|
|
116
|
+
return c.usageKey ?? c.email;
|
|
117
|
+
}
|
|
108
118
|
function dedupeAndSortCandidates(candidates) {
|
|
109
|
-
const
|
|
119
|
+
const byIdentity = new Map();
|
|
110
120
|
for (const c of candidates) {
|
|
111
|
-
const
|
|
112
|
-
const existing =
|
|
121
|
+
const id = candidateIdentity(c);
|
|
122
|
+
const existing = byIdentity.get(id);
|
|
113
123
|
if (!existing) {
|
|
114
|
-
|
|
124
|
+
byIdentity.set(id, c);
|
|
115
125
|
continue;
|
|
116
126
|
}
|
|
117
127
|
if (compareCandidates(c, existing) < 0)
|
|
118
|
-
|
|
128
|
+
byIdentity.set(id, c);
|
|
119
129
|
}
|
|
120
|
-
return [...
|
|
130
|
+
return [...byIdentity.values()].sort(compareCandidates);
|
|
121
131
|
}
|
|
122
132
|
/**
|
|
123
133
|
* Pick a healthy candidate using weighted random by remaining capacity.
|
|
@@ -252,7 +262,7 @@ async function collectRunCandidates(agent) {
|
|
|
252
262
|
const usageSnapshot = usageKey
|
|
253
263
|
? usageByKey.get(usageKey)?.snapshot ?? null
|
|
254
264
|
: null;
|
|
255
|
-
return { ...candidate, usageSnapshot };
|
|
265
|
+
return { ...candidate, usageKey, usageSnapshot };
|
|
256
266
|
});
|
|
257
267
|
}
|
|
258
268
|
/**
|
package/dist/lib/runner.js
CHANGED
|
@@ -22,6 +22,7 @@ const AGENT_COMMANDS = {
|
|
|
22
22
|
codex: ['codex', 'exec', '--sandbox', 'workspace-write', '{prompt}', '--json'],
|
|
23
23
|
gemini: ['gemini', '{prompt}', '--output-format', 'stream-json'],
|
|
24
24
|
kimi: ['kimi', '--prompt', '{prompt}', '--output-format', 'stream-json'],
|
|
25
|
+
droid: ['droid', 'exec', '{prompt}', '-o', 'stream-json'],
|
|
25
26
|
};
|
|
26
27
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
27
28
|
export function buildJobCommand(config, resolvedPrompt) {
|
|
@@ -105,6 +106,19 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
105
106
|
}
|
|
106
107
|
appendModelAndReasoning(cmd, config);
|
|
107
108
|
}
|
|
109
|
+
if (config.agent === 'droid') {
|
|
110
|
+
// droid exec defaults to read-only (plan). Escalate autonomy per mode.
|
|
111
|
+
if (mode === 'edit') {
|
|
112
|
+
cmd.push('--auto', 'low');
|
|
113
|
+
}
|
|
114
|
+
else if (mode === 'auto') {
|
|
115
|
+
cmd.push('--auto', 'high');
|
|
116
|
+
}
|
|
117
|
+
else if (mode === 'skip') {
|
|
118
|
+
cmd.push('--skip-permissions-unsafe');
|
|
119
|
+
}
|
|
120
|
+
appendModelAndReasoning(cmd, config);
|
|
121
|
+
}
|
|
108
122
|
return cmd;
|
|
109
123
|
}
|
|
110
124
|
/**
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as fs from 'fs';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
13
|
-
import { getRoutinesDir } from './state.js';
|
|
13
|
+
import { getRoutinesDir, getUserAgentsDir } from './state.js';
|
|
14
14
|
function resolveRealHome() {
|
|
15
15
|
const home = os.homedir();
|
|
16
16
|
try {
|
|
@@ -58,7 +58,10 @@ function tomlString(value) {
|
|
|
58
58
|
}
|
|
59
59
|
/** Build a restricted environment for a sandboxed process, setting HOME to the overlay. */
|
|
60
60
|
export function buildSpawnEnv(overlayHome, extraEnv) {
|
|
61
|
-
const env = {
|
|
61
|
+
const env = {
|
|
62
|
+
HOME: overlayHome,
|
|
63
|
+
AGENTS_USER_DIR: getUserAgentsDir(),
|
|
64
|
+
};
|
|
62
65
|
for (const key of ENV_ALLOWLIST) {
|
|
63
66
|
if (process.env[key]) {
|
|
64
67
|
env[key] = process.env[key];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings carry-forward between version homes.
|
|
3
|
+
*
|
|
4
|
+
* Every installed version gets an isolated `home/`, so user-authored
|
|
5
|
+
* preferences (settings.json, config.toml, keybindings, auth) written while
|
|
6
|
+
* running one version do not exist in a freshly installed one. Resources
|
|
7
|
+
* managed in ~/.agents/ (commands, skills, hooks, rules, MCP YAML, plugins,
|
|
8
|
+
* subagents) are synced into every version home by syncResourcesToVersion and
|
|
9
|
+
* are deliberately NOT listed here — copying them would fight that sync.
|
|
10
|
+
*
|
|
11
|
+
* The manifest below classifies the remaining per-agent files, and
|
|
12
|
+
* carryForwardSettings() fills gaps in a target version home from a source
|
|
13
|
+
* version home. It never overwrites a value the target already has: scalars
|
|
14
|
+
* keep the target's value, objects merge recursively, arrays union. That makes
|
|
15
|
+
* the operation idempotent and safe to run on every `agents add` / `agents use`.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentId } from './types.js';
|
|
18
|
+
export interface CarryForwardResult {
|
|
19
|
+
/** Manifest rel paths that were created or updated in the target home. */
|
|
20
|
+
applied: string[];
|
|
21
|
+
/** Backup directory holding pre-merge copies of modified target files, if any. */
|
|
22
|
+
backupDir?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fill gaps in `target` from `source` without overwriting target values:
|
|
26
|
+
* missing keys are copied, plain objects recurse, and everything else —
|
|
27
|
+
* scalars AND arrays — keeps the target's value. Arrays deliberately do not
|
|
28
|
+
* union: other writers (factory sync, hooks registration) mutate array entries
|
|
29
|
+
* in place, so a union would keep re-appending stale pre-mutation copies from
|
|
30
|
+
* the source on every carry (e.g. a user hook duplicated after the system
|
|
31
|
+
* hooks were merged into it). Returns a new object.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fillGaps(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Carry user settings forward from one version home into another. Both paths
|
|
36
|
+
* are version-home roots (the directory containing `.claude/` / `.codex/`).
|
|
37
|
+
* Only fills gaps — never overwrites target values — so it is idempotent.
|
|
38
|
+
*/
|
|
39
|
+
export declare function carryForwardSettings(agent: AgentId, fromHome: string, toHome: string): CarryForwardResult;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings carry-forward between version homes.
|
|
3
|
+
*
|
|
4
|
+
* Every installed version gets an isolated `home/`, so user-authored
|
|
5
|
+
* preferences (settings.json, config.toml, keybindings, auth) written while
|
|
6
|
+
* running one version do not exist in a freshly installed one. Resources
|
|
7
|
+
* managed in ~/.agents/ (commands, skills, hooks, rules, MCP YAML, plugins,
|
|
8
|
+
* subagents) are synced into every version home by syncResourcesToVersion and
|
|
9
|
+
* are deliberately NOT listed here — copying them would fight that sync.
|
|
10
|
+
*
|
|
11
|
+
* The manifest below classifies the remaining per-agent files, and
|
|
12
|
+
* carryForwardSettings() fills gaps in a target version home from a source
|
|
13
|
+
* version home. It never overwrites a value the target already has: scalars
|
|
14
|
+
* keep the target's value, objects merge recursively, arrays union. That makes
|
|
15
|
+
* the operation idempotent and safe to run on every `agents add` / `agents use`.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as TOML from 'smol-toml';
|
|
20
|
+
import { getBackupsDir } from './state.js';
|
|
21
|
+
const SETTINGS_MANIFEST = {
|
|
22
|
+
claude: [
|
|
23
|
+
{ rel: '.claude/settings.json', strategy: 'json-merge' },
|
|
24
|
+
{ rel: '.claude/settings.local.json', strategy: 'copy-if-absent' },
|
|
25
|
+
{ rel: '.claude/keybindings.json', strategy: 'copy-if-absent' },
|
|
26
|
+
],
|
|
27
|
+
codex: [
|
|
28
|
+
{
|
|
29
|
+
rel: '.codex/config.toml',
|
|
30
|
+
strategy: 'toml-merge',
|
|
31
|
+
stateKeys: ['notice', 'windows_wsl_setup_acknowledged'],
|
|
32
|
+
},
|
|
33
|
+
{ rel: '.codex/auth.json', strategy: 'copy-if-absent', restrictMode: true },
|
|
34
|
+
{ rel: '.codex/instructions.md', strategy: 'copy-if-absent' },
|
|
35
|
+
{ rel: '.codex/hooks.json', strategy: 'copy-if-absent' },
|
|
36
|
+
{ rel: '.codex/prompts', strategy: 'dir-entries' },
|
|
37
|
+
{ rel: '.codex/rules', strategy: 'dir-entries' },
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
function isPlainObject(value) {
|
|
41
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fill gaps in `target` from `source` without overwriting target values:
|
|
45
|
+
* missing keys are copied, plain objects recurse, and everything else —
|
|
46
|
+
* scalars AND arrays — keeps the target's value. Arrays deliberately do not
|
|
47
|
+
* union: other writers (factory sync, hooks registration) mutate array entries
|
|
48
|
+
* in place, so a union would keep re-appending stale pre-mutation copies from
|
|
49
|
+
* the source on every carry (e.g. a user hook duplicated after the system
|
|
50
|
+
* hooks were merged into it). Returns a new object.
|
|
51
|
+
*/
|
|
52
|
+
export function fillGaps(target, source) {
|
|
53
|
+
const out = { ...target };
|
|
54
|
+
for (const [key, sourceValue] of Object.entries(source)) {
|
|
55
|
+
if (!(key in out)) {
|
|
56
|
+
out[key] = sourceValue;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const targetValue = out[key];
|
|
60
|
+
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
|
|
61
|
+
out[key] = fillGaps(targetValue, sourceValue);
|
|
62
|
+
}
|
|
63
|
+
// scalar, array, or type mismatch: target wins
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function stripStateKeys(obj, stateKeys) {
|
|
68
|
+
if (!stateKeys?.length)
|
|
69
|
+
return obj;
|
|
70
|
+
const out = { ...obj };
|
|
71
|
+
for (const key of stateKeys)
|
|
72
|
+
delete out[key];
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function backupFile(backupRoot, home, rel) {
|
|
76
|
+
const src = path.join(home, rel);
|
|
77
|
+
const dest = path.join(backupRoot, rel);
|
|
78
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
79
|
+
fs.copyFileSync(src, dest);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Carry user settings forward from one version home into another. Both paths
|
|
83
|
+
* are version-home roots (the directory containing `.claude/` / `.codex/`).
|
|
84
|
+
* Only fills gaps — never overwrites target values — so it is idempotent.
|
|
85
|
+
*/
|
|
86
|
+
export function carryForwardSettings(agent, fromHome, toHome) {
|
|
87
|
+
const manifest = SETTINGS_MANIFEST[agent];
|
|
88
|
+
const result = { applied: [] };
|
|
89
|
+
if (!manifest || !fs.existsSync(fromHome) || fromHome === toHome)
|
|
90
|
+
return result;
|
|
91
|
+
const backupRoot = path.join(getBackupsDir(), 'settings-carry', agent, new Date().toISOString().replace(/[:.]/g, '-'));
|
|
92
|
+
for (const entry of manifest) {
|
|
93
|
+
const sourcePath = path.join(fromHome, entry.rel);
|
|
94
|
+
const targetPath = path.join(toHome, entry.rel);
|
|
95
|
+
if (!fs.existsSync(sourcePath))
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
switch (entry.strategy) {
|
|
99
|
+
case 'copy-if-absent': {
|
|
100
|
+
if (fs.existsSync(targetPath))
|
|
101
|
+
break;
|
|
102
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
103
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
104
|
+
if (entry.restrictMode)
|
|
105
|
+
fs.chmodSync(targetPath, 0o600);
|
|
106
|
+
result.applied.push(entry.rel);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'dir-entries': {
|
|
110
|
+
if (!fs.statSync(sourcePath).isDirectory())
|
|
111
|
+
break;
|
|
112
|
+
let copied = false;
|
|
113
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
114
|
+
for (const name of fs.readdirSync(sourcePath)) {
|
|
115
|
+
const childTarget = path.join(targetPath, name);
|
|
116
|
+
if (fs.existsSync(childTarget))
|
|
117
|
+
continue;
|
|
118
|
+
fs.cpSync(path.join(sourcePath, name), childTarget, { recursive: true });
|
|
119
|
+
copied = true;
|
|
120
|
+
}
|
|
121
|
+
if (copied)
|
|
122
|
+
result.applied.push(entry.rel);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'json-merge':
|
|
126
|
+
case 'toml-merge': {
|
|
127
|
+
const parse = entry.strategy === 'json-merge'
|
|
128
|
+
? (text) => JSON.parse(text)
|
|
129
|
+
: (text) => TOML.parse(text);
|
|
130
|
+
const stringify = entry.strategy === 'json-merge'
|
|
131
|
+
? (obj) => JSON.stringify(obj, null, 2) + '\n'
|
|
132
|
+
: (obj) => TOML.stringify(obj) + '\n';
|
|
133
|
+
const source = stripStateKeys(parse(fs.readFileSync(sourcePath, 'utf-8')), entry.stateKeys);
|
|
134
|
+
if (!fs.existsSync(targetPath)) {
|
|
135
|
+
if (Object.keys(source).length === 0)
|
|
136
|
+
break;
|
|
137
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
138
|
+
fs.writeFileSync(targetPath, stringify(source), 'utf-8');
|
|
139
|
+
result.applied.push(entry.rel);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
const targetText = fs.readFileSync(targetPath, 'utf-8');
|
|
143
|
+
const targetObj = parse(targetText);
|
|
144
|
+
const merged = fillGaps(targetObj, source);
|
|
145
|
+
// Compare parsed content, not text: other writers format differently,
|
|
146
|
+
// and a semantic no-op must not trigger a rewrite/backup every switch.
|
|
147
|
+
if (JSON.stringify(merged) === JSON.stringify(targetObj))
|
|
148
|
+
break;
|
|
149
|
+
backupFile(backupRoot, toHome, entry.rel);
|
|
150
|
+
result.backupDir = backupRoot;
|
|
151
|
+
fs.writeFileSync(targetPath, stringify(merged), 'utf-8');
|
|
152
|
+
result.applied.push(entry.rel);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// A malformed source or target file must not break install/use.
|
|
159
|
+
// Leave the target untouched for this entry and move on.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface ConflictInfo {
|
|
|
77
77
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
78
78
|
* trigger auto-resync, run `agents sync` for that.
|
|
79
79
|
*/
|
|
80
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
80
|
+
export declare const SHIM_SCHEMA_VERSION = 18;
|
|
81
81
|
/**
|
|
82
82
|
* Generate the full bash shim script for the given agent. The returned string
|
|
83
83
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
package/dist/lib/shims.js
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import * as os from 'os';
|
|
14
|
-
import { execFileSync } from 'child_process';
|
|
15
14
|
import { fileURLToPath } from 'url';
|
|
16
15
|
import { confirm, select } from '@inquirer/prompts';
|
|
17
|
-
import { IS_WINDOWS } from './platform/index.js';
|
|
16
|
+
import { IS_WINDOWS, prependToWindowsUserPath } from './platform/index.js';
|
|
18
17
|
import { getShimsDir, getVersionsDir, getBackupsDir, ensureAgentsDir } from './state.js';
|
|
19
18
|
export { getShimsDir };
|
|
20
19
|
import { AGENTS } from './agents.js';
|
|
@@ -203,7 +202,7 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
203
202
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
204
203
|
* trigger auto-resync, run `agents sync` for that.
|
|
205
204
|
*/
|
|
206
|
-
export const SHIM_SCHEMA_VERSION =
|
|
205
|
+
export const SHIM_SCHEMA_VERSION = 18;
|
|
207
206
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
208
207
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
209
208
|
function shellQuote(value) {
|
|
@@ -273,7 +272,7 @@ export KIMI_CODE_HOME="$VERSION_DIR/home/${configDirName}"
|
|
|
273
272
|
# Shim for ${agentConfig.name}
|
|
274
273
|
# ${SHIM_VERSION_MARKER} ${SHIM_SCHEMA_VERSION}
|
|
275
274
|
|
|
276
|
-
AGENTS_USER_DIR="
|
|
275
|
+
AGENTS_USER_DIR="\${AGENTS_USER_DIR:-$HOME/.agents}"
|
|
277
276
|
AGENTS_BIN=${agentsBin}
|
|
278
277
|
AGENT="${agent}"
|
|
279
278
|
CLI_COMMAND="${cliCommand}"
|
|
@@ -1579,36 +1578,22 @@ export function addShimsToPath(overrides) {
|
|
|
1579
1578
|
* Register the shims dir on the Windows User PATH via the .NET environment API,
|
|
1580
1579
|
* which writes the registry AND broadcasts WM_SETTINGCHANGE — the correct analog
|
|
1581
1580
|
* of editing a shell rc file (no `setx` truncation, no manual step). Idempotent:
|
|
1582
|
-
* a no-op when the dir is already
|
|
1583
|
-
*
|
|
1581
|
+
* a no-op when the shims dir is already first in the User PATH. Moves it to the
|
|
1582
|
+
* front when it exists but is in the wrong position (e.g. appended by an old
|
|
1583
|
+
* install) so it overrides any npm/global installs that appear later. The shims
|
|
1584
|
+
* dir is passed via an env var so it is never interpolated into the script text.
|
|
1584
1585
|
*/
|
|
1585
1586
|
function addShimsToWindowsUserPath(shimsDir) {
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
"if ($null -eq $u) { $u = '' }",
|
|
1590
|
-
"$parts = @($u -split ';' | Where-Object { $_ -ne '' })",
|
|
1591
|
-
"if ($parts -contains $d) { 'present' } else {",
|
|
1592
|
-
" [Environment]::SetEnvironmentVariable('Path', (($parts + $d) -join ';'), 'User')",
|
|
1593
|
-
" 'added'",
|
|
1594
|
-
'}',
|
|
1595
|
-
].join('\n');
|
|
1596
|
-
try {
|
|
1597
|
-
const out = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1598
|
-
encoding: 'utf-8',
|
|
1599
|
-
env: { ...process.env, AGENTS_SHIMS_DIR: shimsDir },
|
|
1600
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1601
|
-
}).trim();
|
|
1602
|
-
return {
|
|
1603
|
-
success: true,
|
|
1604
|
-
alreadyPresent: out.includes('present'),
|
|
1605
|
-
location: 'your user PATH',
|
|
1606
|
-
reloadHint: 'Open a new terminal for the change to take effect.',
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
catch (err) {
|
|
1610
|
-
return { success: false, error: `Could not update the Windows user PATH: ${err.message}` };
|
|
1587
|
+
const r = prependToWindowsUserPath(shimsDir);
|
|
1588
|
+
if (!r.success) {
|
|
1589
|
+
return { success: false, error: r.error };
|
|
1611
1590
|
}
|
|
1591
|
+
return {
|
|
1592
|
+
success: true,
|
|
1593
|
+
alreadyPresent: r.alreadyPresent,
|
|
1594
|
+
location: 'your user PATH',
|
|
1595
|
+
reloadHint: 'Open a new terminal for the change to take effect.',
|
|
1596
|
+
};
|
|
1612
1597
|
}
|
|
1613
1598
|
export function listAgentsWithInstalledVersions() {
|
|
1614
1599
|
const versionsDir = getVersionsDir();
|