@phnx-labs/agents-cli 1.20.34 → 1.20.35
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 +22 -0
- package/README.md +5 -1
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +59 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/secrets.js +40 -16
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +58 -31
- package/dist/lib/agents.js +30 -9
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +76 -16
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.js +22 -0
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +45 -4
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +11 -3
- package/dist/lib/shims.js +54 -30
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +18 -0
- package/dist/lib/terminal/backends/index.js +29 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/package.json +1 -1
package/dist/lib/picker.js
CHANGED
|
@@ -197,3 +197,127 @@ export function itemPicker(config) {
|
|
|
197
197
|
});
|
|
198
198
|
return prompt(config);
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Multi-select variant of {@link itemPicker}. Same searchable, paginated list
|
|
202
|
+
* and preview pane, but `space` toggles a checkbox on the active row instead of
|
|
203
|
+
* the preview (preview moves to `tab`), and `enter` confirms every checked row.
|
|
204
|
+
*
|
|
205
|
+
* Returns the selected items (in the config's `items` order) or `null` on
|
|
206
|
+
* cancel. Pressing `enter` with nothing checked confirms just the highlighted
|
|
207
|
+
* row, so a quick single-pick still works.
|
|
208
|
+
*/
|
|
209
|
+
export function multiItemPicker(config) {
|
|
210
|
+
const prompt = createPrompt((cfg, done) => {
|
|
211
|
+
const theme = makeTheme({});
|
|
212
|
+
const [status, setStatus] = useState('idle');
|
|
213
|
+
const [searchTerm, setSearchTerm] = useState(cfg.initialSearch ?? '');
|
|
214
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
215
|
+
const [selectedKeys, setSelectedKeys] = useState(new Set());
|
|
216
|
+
const [active, setActive] = useState(0);
|
|
217
|
+
const prefix = usePrefix({ status, theme });
|
|
218
|
+
const results = useMemo(() => {
|
|
219
|
+
const filtered = cfg.filter(searchTerm).slice(0, 200);
|
|
220
|
+
return filtered.map((item) => ({
|
|
221
|
+
value: item,
|
|
222
|
+
label: cfg.labelFor(item, searchTerm),
|
|
223
|
+
}));
|
|
224
|
+
}, [searchTerm]);
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (active >= results.length)
|
|
227
|
+
setActive(0);
|
|
228
|
+
}, [results]);
|
|
229
|
+
const selected = results[active];
|
|
230
|
+
// Selected items resolved in the original list order for deterministic fan-out.
|
|
231
|
+
const collectSelected = () => cfg.items.filter((it) => selectedKeys.has(cfg.keyFor(it)));
|
|
232
|
+
useKeypress((key, rl) => {
|
|
233
|
+
if (isEnterKey(key)) {
|
|
234
|
+
const chosen = selectedKeys.size > 0 ? collectSelected() : selected ? [selected.value] : [];
|
|
235
|
+
if (chosen.length === 0)
|
|
236
|
+
return;
|
|
237
|
+
setStatus('done');
|
|
238
|
+
done(chosen);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// space toggles the active row's checkbox; strip the space from the buffer
|
|
242
|
+
// so it never leaks into the filter.
|
|
243
|
+
if (isSpaceKey(key)) {
|
|
244
|
+
rl.clearLine(0);
|
|
245
|
+
if (selected) {
|
|
246
|
+
const k = cfg.keyFor(selected.value);
|
|
247
|
+
const next = new Set(selectedKeys);
|
|
248
|
+
if (next.has(k))
|
|
249
|
+
next.delete(k);
|
|
250
|
+
else
|
|
251
|
+
next.add(k);
|
|
252
|
+
setSelectedKeys(next);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (key.name === 'tab' && cfg.buildPreview) {
|
|
257
|
+
rl.clearLine(0);
|
|
258
|
+
setPreviewOpen(!previewOpen);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (isUpKey(key)) {
|
|
262
|
+
rl.clearLine(0);
|
|
263
|
+
if (results.length > 0)
|
|
264
|
+
setActive((active - 1 + results.length) % results.length);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (isDownKey(key)) {
|
|
268
|
+
rl.clearLine(0);
|
|
269
|
+
if (results.length > 0)
|
|
270
|
+
setActive((active + 1) % results.length);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
setSearchTerm(rl.line);
|
|
274
|
+
});
|
|
275
|
+
const message = theme.style.message(cfg.message, status);
|
|
276
|
+
const count = selectedKeys.size;
|
|
277
|
+
if (status === 'done') {
|
|
278
|
+
return `${prefix} ${message} ${chalk.cyan(`${count || 1} session${(count || 1) === 1 ? '' : 's'}`)}`;
|
|
279
|
+
}
|
|
280
|
+
const placeholder = '(type to filter · space to toggle · enter to resume)';
|
|
281
|
+
const searchStr = searchTerm ? chalk.cyan(searchTerm) : chalk.gray(placeholder);
|
|
282
|
+
const header = [prefix, message, searchStr].filter(Boolean).join(' ');
|
|
283
|
+
const page = usePagination({
|
|
284
|
+
items: results,
|
|
285
|
+
active,
|
|
286
|
+
renderItem({ item, isActive }) {
|
|
287
|
+
if (Separator.isSeparator(item))
|
|
288
|
+
return ` ${item.separator}`;
|
|
289
|
+
const checked = selectedKeys.has(cfg.keyFor(item.value));
|
|
290
|
+
const box = checked ? chalk.green('[x]') : chalk.gray('[ ]');
|
|
291
|
+
const cursor = isActive ? chalk.cyan('>') : ' ';
|
|
292
|
+
const row = isActive ? chalk.bold(item.label) : item.label;
|
|
293
|
+
return `${cursor} ${box} ${row}`;
|
|
294
|
+
},
|
|
295
|
+
pageSize: cfg.pageSize ?? 10,
|
|
296
|
+
loop: false,
|
|
297
|
+
});
|
|
298
|
+
const enter = cfg.enterHint ?? 'resume';
|
|
299
|
+
const countStr = count > 0 ? chalk.green(`${count} selected`) : chalk.gray('0 selected');
|
|
300
|
+
const help = chalk.gray(`${countStr}${chalk.gray(' · ↑↓ navigate · space toggle')}${cfg.buildPreview ? chalk.gray(' · tab preview') : ''}${chalk.gray(` · ⏎ ${enter} · esc cancel`)}`);
|
|
301
|
+
const parts = [header, page];
|
|
302
|
+
if (results.length === 0) {
|
|
303
|
+
parts.push(chalk.gray(` ${cfg.emptyMessage ?? 'No matches.'}`));
|
|
304
|
+
}
|
|
305
|
+
if (previewOpen && selected && cfg.buildPreview) {
|
|
306
|
+
const width = terminalWidth();
|
|
307
|
+
const separator = chalk.gray('─'.repeat(Math.min(width, 80)));
|
|
308
|
+
const fixedRows = renderedRows(header, width) +
|
|
309
|
+
renderedRows(parts.slice(1).join('\n'), width) +
|
|
310
|
+
renderedRows(separator, width) +
|
|
311
|
+
renderedRows(help, width);
|
|
312
|
+
const availablePreviewRows = terminalRows() - fixedRows;
|
|
313
|
+
const preview = limitPreviewHeight(cfg.buildPreview(selected.value), availablePreviewRows, width);
|
|
314
|
+
if (preview) {
|
|
315
|
+
parts.push(separator);
|
|
316
|
+
parts.push(preview);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
parts.push(help);
|
|
320
|
+
return [header, parts.slice(1).join('\n')];
|
|
321
|
+
});
|
|
322
|
+
return prompt(config);
|
|
323
|
+
}
|
|
@@ -144,6 +144,36 @@ export declare function syncMarketplaceManifest(spec: MarketplaceSpec, agent: Ag
|
|
|
144
144
|
* marketplaces' entries are preserved untouched.
|
|
145
145
|
*/
|
|
146
146
|
export declare function registerMarketplace(spec: MarketplaceSpec, agent: AgentId, versionHome: string): void;
|
|
147
|
+
/**
|
|
148
|
+
* Record a plugin in Droid's installed_plugins.json (user scope), pointing
|
|
149
|
+
* installPath at the marketplace install dir so no second copy is needed. This
|
|
150
|
+
* is what makes `droid plugin list` show the plugin (Active once enabledPlugins
|
|
151
|
+
* is set). Idempotent: re-running refreshes lastUpdated and preserves the
|
|
152
|
+
* original installedAt plus any non-user scope entries.
|
|
153
|
+
*/
|
|
154
|
+
export declare function registerDroidInstalledPlugin(pluginName: string, marketplaceName: string, installDir: string, version: string, agent: AgentId, versionHome: string): void;
|
|
155
|
+
/** True when Droid's installed_plugins.json carries a user-scope entry for the plugin. */
|
|
156
|
+
export declare function isDroidPluginInstalled(pluginName: string, marketplaceName: string, agent: AgentId, versionHome: string): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Remove a plugin's user-scope entry from Droid's installed_plugins.json.
|
|
159
|
+
* Inverse of registerDroidInstalledPlugin. Drops the key when no scopes remain
|
|
160
|
+
* and deletes the file when the registry is empty.
|
|
161
|
+
*/
|
|
162
|
+
export declare function unregisterDroidInstalledPlugin(pluginName: string, marketplaceName: string, agent: AgentId, versionHome: string): void;
|
|
163
|
+
/**
|
|
164
|
+
* Record a plugin in Copilot's config.json#installedPlugins (the registry that
|
|
165
|
+
* makes `copilot plugin list` see it). `cache_path` points at the marketplace
|
|
166
|
+
* install dir — no separate copy. Idempotent: re-running refreshes the entry and
|
|
167
|
+
* preserves the original installed_at plus every other config key.
|
|
168
|
+
*/
|
|
169
|
+
export declare function registerCopilotInstalledPlugin(pluginName: string, marketplaceName: string, installDir: string, version: string, enabled: boolean, agent: AgentId, versionHome: string): void;
|
|
170
|
+
/**
|
|
171
|
+
* Remove a plugin's entry from Copilot's config.json#installedPlugins. Inverse
|
|
172
|
+
* of registerCopilotInstalledPlugin. Leaves the file (with an empty
|
|
173
|
+
* installedPlugins) rather than deleting it, since config.json may hold other
|
|
174
|
+
* Copilot-managed keys.
|
|
175
|
+
*/
|
|
176
|
+
export declare function unregisterCopilotInstalledPlugin(pluginName: string, marketplaceName: string, agent: AgentId, versionHome: string): void;
|
|
147
177
|
/**
|
|
148
178
|
* Drop a marketplace entry from known_marketplaces.json. Called when the last
|
|
149
179
|
* plugin under it is removed. Removes only its own entry; deletes the file only
|
|
@@ -127,7 +127,14 @@ export function marketplaceRoot(specOrName, agent, versionHome) {
|
|
|
127
127
|
return path.join(pluginsRootForVersion(agent, versionHome), 'marketplaces', nameOf(specOrName));
|
|
128
128
|
}
|
|
129
129
|
export function marketplaceManifestPath(specOrName, agent, versionHome) {
|
|
130
|
-
|
|
130
|
+
const root = marketplaceRoot(specOrName, agent, versionHome);
|
|
131
|
+
// Copilot resolves a marketplace's catalog from `marketplace.json` at the
|
|
132
|
+
// marketplace ROOT (also `.plugin/marketplace.json`, `.github/plugin/
|
|
133
|
+
// marketplace.json`) — NOT `.claude-plugin/marketplace.json`. Verified against
|
|
134
|
+
// GitHub Copilot CLI 1.0.56 (`plugin marketplace add`).
|
|
135
|
+
if (agent === 'copilot')
|
|
136
|
+
return path.join(root, 'marketplace.json');
|
|
137
|
+
return path.join(root, '.claude-plugin', 'marketplace.json');
|
|
131
138
|
}
|
|
132
139
|
export function pluginInstallDir(plugin, specOrName, agent, versionHome) {
|
|
133
140
|
return path.join(marketplaceRoot(specOrName, agent, versionHome), 'plugins', plugin.name);
|
|
@@ -375,6 +382,14 @@ export function syncMarketplaceManifest(spec, agent, versionHome) {
|
|
|
375
382
|
export function registerMarketplace(spec, agent, versionHome) {
|
|
376
383
|
const name = marketplaceNameFor(spec);
|
|
377
384
|
const root = marketplaceRoot(spec, agent, versionHome);
|
|
385
|
+
// Copilot diverges from Claude's known_marketplaces.json: it reads registered
|
|
386
|
+
// marketplaces from settings.json#extraKnownMarketplaces. Verified against
|
|
387
|
+
// GitHub Copilot CLI 1.0.56 (`plugin marketplace add <dir>` writes exactly
|
|
388
|
+
// this shape). A "directory"-source entry points at the on-disk catalog root.
|
|
389
|
+
if (agent === 'copilot') {
|
|
390
|
+
registerCopilotMarketplace(name, root, agent, versionHome);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
378
393
|
const knownPath = knownMarketplacesPath(agent, versionHome);
|
|
379
394
|
let known = {};
|
|
380
395
|
if (fs.existsSync(knownPath)) {
|
|
@@ -385,14 +400,206 @@ export function registerMarketplace(spec, agent, versionHome) {
|
|
|
385
400
|
known = {};
|
|
386
401
|
}
|
|
387
402
|
}
|
|
403
|
+
// Droid names the on-disk source type "local" (Claude/OpenClaw use
|
|
404
|
+
// "directory") and stamps autoUpdate — verified against `droid plugin
|
|
405
|
+
// marketplace add`. A "directory" entry is silently ignored by the Factory
|
|
406
|
+
// CLI, so the plugin never resolves.
|
|
407
|
+
const isDroid = agent === 'droid';
|
|
388
408
|
known[name] = {
|
|
389
|
-
source: { source: 'directory', path: root },
|
|
409
|
+
source: { source: isDroid ? 'local' : 'directory', path: root },
|
|
390
410
|
installLocation: root,
|
|
391
411
|
lastUpdated: new Date().toISOString(),
|
|
412
|
+
...(isDroid ? { autoUpdate: true } : {}),
|
|
392
413
|
};
|
|
393
414
|
fs.mkdirSync(path.dirname(knownPath), { recursive: true });
|
|
394
415
|
fs.writeFileSync(knownPath, JSON.stringify(known, null, 2) + '\n', 'utf-8');
|
|
395
416
|
}
|
|
417
|
+
// ─── Droid installed-plugins registry ─────────────────────────────────────────
|
|
418
|
+
function installedPluginsPath(agent, versionHome) {
|
|
419
|
+
return path.join(pluginsRootForVersion(agent, versionHome), 'installed_plugins.json');
|
|
420
|
+
}
|
|
421
|
+
function readInstalledPlugins(agent, versionHome) {
|
|
422
|
+
const p = installedPluginsPath(agent, versionHome);
|
|
423
|
+
if (fs.existsSync(p)) {
|
|
424
|
+
try {
|
|
425
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
426
|
+
if (parsed && typeof parsed === 'object' && parsed.plugins && typeof parsed.plugins === 'object') {
|
|
427
|
+
return { schemaVersion: parsed.schemaVersion ?? 1, plugins: parsed.plugins };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch { /* fall through to fresh registry */ }
|
|
431
|
+
}
|
|
432
|
+
return { schemaVersion: 1, plugins: {} };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Record a plugin in Droid's installed_plugins.json (user scope), pointing
|
|
436
|
+
* installPath at the marketplace install dir so no second copy is needed. This
|
|
437
|
+
* is what makes `droid plugin list` show the plugin (Active once enabledPlugins
|
|
438
|
+
* is set). Idempotent: re-running refreshes lastUpdated and preserves the
|
|
439
|
+
* original installedAt plus any non-user scope entries.
|
|
440
|
+
*/
|
|
441
|
+
export function registerDroidInstalledPlugin(pluginName, marketplaceName, installDir, version, agent, versionHome) {
|
|
442
|
+
const registry = readInstalledPlugins(agent, versionHome);
|
|
443
|
+
const key = `${pluginName}@${marketplaceName}`;
|
|
444
|
+
const now = new Date().toISOString();
|
|
445
|
+
const existing = registry.plugins[key] ?? [];
|
|
446
|
+
const priorUser = existing.find(e => e.scope === 'user');
|
|
447
|
+
const others = existing.filter(e => e.scope !== 'user');
|
|
448
|
+
registry.plugins[key] = [
|
|
449
|
+
...others,
|
|
450
|
+
{
|
|
451
|
+
scope: 'user',
|
|
452
|
+
installPath: installDir,
|
|
453
|
+
version,
|
|
454
|
+
installedAt: priorUser?.installedAt ?? now,
|
|
455
|
+
lastUpdated: now,
|
|
456
|
+
source: marketplaceName,
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
const p = installedPluginsPath(agent, versionHome);
|
|
460
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
461
|
+
fs.writeFileSync(p, JSON.stringify(registry, null, 2) + '\n', 'utf-8');
|
|
462
|
+
}
|
|
463
|
+
/** True when Droid's installed_plugins.json carries a user-scope entry for the plugin. */
|
|
464
|
+
export function isDroidPluginInstalled(pluginName, marketplaceName, agent, versionHome) {
|
|
465
|
+
const registry = readInstalledPlugins(agent, versionHome);
|
|
466
|
+
const entries = registry.plugins[`${pluginName}@${marketplaceName}`];
|
|
467
|
+
return Array.isArray(entries) && entries.some(e => e.scope === 'user');
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Remove a plugin's user-scope entry from Droid's installed_plugins.json.
|
|
471
|
+
* Inverse of registerDroidInstalledPlugin. Drops the key when no scopes remain
|
|
472
|
+
* and deletes the file when the registry is empty.
|
|
473
|
+
*/
|
|
474
|
+
export function unregisterDroidInstalledPlugin(pluginName, marketplaceName, agent, versionHome) {
|
|
475
|
+
const p = installedPluginsPath(agent, versionHome);
|
|
476
|
+
if (!fs.existsSync(p))
|
|
477
|
+
return;
|
|
478
|
+
const registry = readInstalledPlugins(agent, versionHome);
|
|
479
|
+
const key = `${pluginName}@${marketplaceName}`;
|
|
480
|
+
const entries = registry.plugins[key];
|
|
481
|
+
if (!Array.isArray(entries))
|
|
482
|
+
return;
|
|
483
|
+
const kept = entries.filter(e => e.scope !== 'user');
|
|
484
|
+
if (kept.length > 0) {
|
|
485
|
+
registry.plugins[key] = kept;
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
delete registry.plugins[key];
|
|
489
|
+
}
|
|
490
|
+
if (Object.keys(registry.plugins).length === 0) {
|
|
491
|
+
try {
|
|
492
|
+
fs.unlinkSync(p);
|
|
493
|
+
}
|
|
494
|
+
catch { /* ignore */ }
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
fs.writeFileSync(p, JSON.stringify(registry, null, 2) + '\n', 'utf-8');
|
|
498
|
+
}
|
|
499
|
+
// ─── Copilot marketplace + installed-plugin registry ──────────────────────────
|
|
500
|
+
function copilotConfigPath(agent, versionHome) {
|
|
501
|
+
return path.join(versionHome, agentConfigDirName(agent), 'config.json');
|
|
502
|
+
}
|
|
503
|
+
function readCopilotSettings(agent, versionHome) {
|
|
504
|
+
const p = settingsPath(agent, versionHome);
|
|
505
|
+
if (fs.existsSync(p)) {
|
|
506
|
+
try {
|
|
507
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
508
|
+
if (parsed && typeof parsed === 'object')
|
|
509
|
+
return parsed;
|
|
510
|
+
}
|
|
511
|
+
catch { /* fall through */ }
|
|
512
|
+
}
|
|
513
|
+
return {};
|
|
514
|
+
}
|
|
515
|
+
function writeCopilotSettings(agent, versionHome, settings) {
|
|
516
|
+
const p = settingsPath(agent, versionHome);
|
|
517
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
518
|
+
fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
519
|
+
}
|
|
520
|
+
/** Register a marketplace in Copilot's settings.json#extraKnownMarketplaces. */
|
|
521
|
+
function registerCopilotMarketplace(name, root, agent, versionHome) {
|
|
522
|
+
const settings = readCopilotSettings(agent, versionHome);
|
|
523
|
+
const known = (settings.extraKnownMarketplaces && typeof settings.extraKnownMarketplaces === 'object'
|
|
524
|
+
? settings.extraKnownMarketplaces
|
|
525
|
+
: {});
|
|
526
|
+
known[name] = { source: { source: 'directory', path: root } };
|
|
527
|
+
settings.extraKnownMarketplaces = known;
|
|
528
|
+
writeCopilotSettings(agent, versionHome, settings);
|
|
529
|
+
}
|
|
530
|
+
/** Inverse of registerCopilotMarketplace: drop the entry, prune the empty key. */
|
|
531
|
+
function unregisterCopilotMarketplace(name, agent, versionHome) {
|
|
532
|
+
const p = settingsPath(agent, versionHome);
|
|
533
|
+
if (!fs.existsSync(p))
|
|
534
|
+
return;
|
|
535
|
+
const settings = readCopilotSettings(agent, versionHome);
|
|
536
|
+
const known = settings.extraKnownMarketplaces;
|
|
537
|
+
if (!known || !(name in known))
|
|
538
|
+
return;
|
|
539
|
+
delete known[name];
|
|
540
|
+
if (Object.keys(known).length === 0)
|
|
541
|
+
delete settings.extraKnownMarketplaces;
|
|
542
|
+
writeCopilotSettings(agent, versionHome, settings);
|
|
543
|
+
}
|
|
544
|
+
function readCopilotConfig(agent, versionHome) {
|
|
545
|
+
const p = copilotConfigPath(agent, versionHome);
|
|
546
|
+
if (fs.existsSync(p)) {
|
|
547
|
+
try {
|
|
548
|
+
// config.json may carry a leading `//` comment banner written by Copilot;
|
|
549
|
+
// strip line comments before parsing so we never clobber a real config.
|
|
550
|
+
const raw = fs.readFileSync(p, 'utf-8').replace(/^\s*\/\/.*$/gm, '');
|
|
551
|
+
const parsed = JSON.parse(raw);
|
|
552
|
+
if (parsed && typeof parsed === 'object') {
|
|
553
|
+
return { ...parsed, installedPlugins: Array.isArray(parsed.installedPlugins) ? parsed.installedPlugins : [] };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch { /* fall through to fresh config */ }
|
|
557
|
+
}
|
|
558
|
+
return { installedPlugins: [] };
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Record a plugin in Copilot's config.json#installedPlugins (the registry that
|
|
562
|
+
* makes `copilot plugin list` see it). `cache_path` points at the marketplace
|
|
563
|
+
* install dir — no separate copy. Idempotent: re-running refreshes the entry and
|
|
564
|
+
* preserves the original installed_at plus every other config key.
|
|
565
|
+
*/
|
|
566
|
+
export function registerCopilotInstalledPlugin(pluginName, marketplaceName, installDir, version, enabled, agent, versionHome) {
|
|
567
|
+
const config = readCopilotConfig(agent, versionHome);
|
|
568
|
+
const now = new Date().toISOString();
|
|
569
|
+
const prior = config.installedPlugins.find(e => e.name === pluginName && e.marketplace === marketplaceName);
|
|
570
|
+
const others = config.installedPlugins.filter(e => !(e.name === pluginName && e.marketplace === marketplaceName));
|
|
571
|
+
config.installedPlugins = [
|
|
572
|
+
...others,
|
|
573
|
+
{
|
|
574
|
+
name: pluginName,
|
|
575
|
+
marketplace: marketplaceName,
|
|
576
|
+
version,
|
|
577
|
+
installed_at: prior?.installed_at ?? now,
|
|
578
|
+
enabled,
|
|
579
|
+
cache_path: installDir,
|
|
580
|
+
},
|
|
581
|
+
];
|
|
582
|
+
const p = copilotConfigPath(agent, versionHome);
|
|
583
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
584
|
+
fs.writeFileSync(p, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Remove a plugin's entry from Copilot's config.json#installedPlugins. Inverse
|
|
588
|
+
* of registerCopilotInstalledPlugin. Leaves the file (with an empty
|
|
589
|
+
* installedPlugins) rather than deleting it, since config.json may hold other
|
|
590
|
+
* Copilot-managed keys.
|
|
591
|
+
*/
|
|
592
|
+
export function unregisterCopilotInstalledPlugin(pluginName, marketplaceName, agent, versionHome) {
|
|
593
|
+
const p = copilotConfigPath(agent, versionHome);
|
|
594
|
+
if (!fs.existsSync(p))
|
|
595
|
+
return;
|
|
596
|
+
const config = readCopilotConfig(agent, versionHome);
|
|
597
|
+
const kept = config.installedPlugins.filter(e => !(e.name === pluginName && e.marketplace === marketplaceName));
|
|
598
|
+
if (kept.length === config.installedPlugins.length)
|
|
599
|
+
return;
|
|
600
|
+
config.installedPlugins = kept;
|
|
601
|
+
fs.writeFileSync(p, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
602
|
+
}
|
|
396
603
|
/**
|
|
397
604
|
* Drop a marketplace entry from known_marketplaces.json. Called when the last
|
|
398
605
|
* plugin under it is removed. Removes only its own entry; deletes the file only
|
|
@@ -400,6 +607,12 @@ export function registerMarketplace(spec, agent, versionHome) {
|
|
|
400
607
|
*/
|
|
401
608
|
export function unregisterMarketplace(specOrName, agent, versionHome) {
|
|
402
609
|
const name = nameOf(specOrName);
|
|
610
|
+
// Copilot stores registrations in settings.json#extraKnownMarketplaces, not
|
|
611
|
+
// known_marketplaces.json. Mirror the branch in registerMarketplace.
|
|
612
|
+
if (agent === 'copilot') {
|
|
613
|
+
unregisterCopilotMarketplace(name, agent, versionHome);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
403
616
|
const knownPath = knownMarketplacesPath(agent, versionHome);
|
|
404
617
|
if (!fs.existsSync(knownPath))
|
|
405
618
|
return;
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -148,6 +148,11 @@ export declare function marketplaceSpecForName(name: string | undefined, cwd?: s
|
|
|
148
148
|
* MCP servers, bin/, settings.json, and permissions once the plugin lives at the
|
|
149
149
|
* native install path and is marked enabled — see
|
|
150
150
|
* https://code.claude.com/docs/en/plugins.
|
|
151
|
+
*
|
|
152
|
+
* Droid (Factory CLI) reuses this same marketplace layout but additionally
|
|
153
|
+
* requires an installed_plugins.json registry entry and a "local"-source
|
|
154
|
+
* known_marketplaces.json entry before `droid plugin list` will see the plugin
|
|
155
|
+
* (steps handled by registerMarketplace's droid branch + step 5c below).
|
|
151
156
|
*/
|
|
152
157
|
export declare function syncPluginToVersion(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string, options?: {
|
|
153
158
|
allowExecSurfaces?: boolean;
|
package/dist/lib/plugins.js
CHANGED
|
@@ -18,7 +18,7 @@ import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
|
18
18
|
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
19
19
|
import { capableAgents, isCapable } from './capabilities.js';
|
|
20
20
|
import { shouldInstallCommandAsSkill, installCommandSkillToVersion } from './command-skills.js';
|
|
21
|
-
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, } from './plugin-marketplace.js';
|
|
21
|
+
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, registerDroidInstalledPlugin, unregisterDroidInstalledPlugin, isDroidPluginInstalled, registerCopilotInstalledPlugin, unregisterCopilotInstalledPlugin, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, } from './plugin-marketplace.js';
|
|
22
22
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
23
23
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
24
24
|
const USER_CONFIG_FILE = '.user-config.json';
|
|
@@ -416,7 +416,7 @@ export function marketplaceSpecForName(name, cwd = process.cwd()) {
|
|
|
416
416
|
* carries, not just the user one.
|
|
417
417
|
*/
|
|
418
418
|
function listVersionMarketplaceNames(agent, versionHome) {
|
|
419
|
-
const dir = path.join(versionHome,
|
|
419
|
+
const dir = path.join(versionHome, agentConfigDirName(agent), 'plugins', 'marketplaces');
|
|
420
420
|
if (!fs.existsSync(dir))
|
|
421
421
|
return [];
|
|
422
422
|
try {
|
|
@@ -444,6 +444,11 @@ function listVersionMarketplaceNames(agent, versionHome) {
|
|
|
444
444
|
* MCP servers, bin/, settings.json, and permissions once the plugin lives at the
|
|
445
445
|
* native install path and is marked enabled — see
|
|
446
446
|
* https://code.claude.com/docs/en/plugins.
|
|
447
|
+
*
|
|
448
|
+
* Droid (Factory CLI) reuses this same marketplace layout but additionally
|
|
449
|
+
* requires an installed_plugins.json registry entry and a "local"-source
|
|
450
|
+
* known_marketplaces.json entry before `droid plugin list` will see the plugin
|
|
451
|
+
* (steps handled by registerMarketplace's droid branch + step 5c below).
|
|
447
452
|
*/
|
|
448
453
|
export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
|
|
449
454
|
const result = {
|
|
@@ -491,9 +496,25 @@ export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
|
|
|
491
496
|
// .mcp.json, settings.json, permissions/) are only auto-enabled when the
|
|
492
497
|
// caller explicitly opts in. addPluginToSettings does no gating — that moved
|
|
493
498
|
// here, where plugin capabilities are inspected.
|
|
494
|
-
|
|
499
|
+
const enablePlugin = options.allowExecSurfaces === true || !hasPluginExecSurfaces(inspectPluginCapabilities(plugin.root));
|
|
500
|
+
if (enablePlugin) {
|
|
495
501
|
addPluginToSettings(plugin.name, marketplaceName, agent, versionHome);
|
|
496
502
|
}
|
|
503
|
+
// 5c. Droid diverges from Claude's marketplace+enabledPlugins model: it only
|
|
504
|
+
// "sees" a plugin that also has an entry in installed_plugins.json (with
|
|
505
|
+
// the marketplace registered as source "local"). Register the install so
|
|
506
|
+
// `droid plugin list` shows it — Active when enabled, Inactive otherwise.
|
|
507
|
+
if (agent === 'droid') {
|
|
508
|
+
registerDroidInstalledPlugin(plugin.name, marketplaceName, installDir, plugin.manifest.version, agent, versionHome);
|
|
509
|
+
}
|
|
510
|
+
// 5d. Copilot, like Droid, only "sees" a plugin that also has an entry in its
|
|
511
|
+
// auto-managed config.json#installedPlugins (registerMarketplace already
|
|
512
|
+
// wrote settings.json#extraKnownMarketplaces, and addPluginToSettings the
|
|
513
|
+
// enabledPlugins flag). cache_path points at the marketplace copy; the
|
|
514
|
+
// `enabled` flag mirrors the exec-surface trust gate above.
|
|
515
|
+
if (agent === 'copilot') {
|
|
516
|
+
registerCopilotInstalledPlugin(plugin.name, marketplaceName, installDir, plugin.manifest.version, enablePlugin, agent, versionHome);
|
|
517
|
+
}
|
|
497
518
|
// 5b. Convert plugin commands/ to skills for agents that dropped command support
|
|
498
519
|
// (Codex >= 0.117.0). Skill name is prefixed with plugin name to avoid
|
|
499
520
|
// collision with standalone command skills.
|
|
@@ -718,7 +739,15 @@ function migrateLegacyFlatLayout(plugin, agent, versionHome) {
|
|
|
718
739
|
export function isPluginSynced(plugin, agent, versionHome) {
|
|
719
740
|
if (!isCapable(agent, 'plugins'))
|
|
720
741
|
return false;
|
|
721
|
-
|
|
742
|
+
const spec = marketplaceSpecForName(plugin.marketplace);
|
|
743
|
+
if (!isInstalledInMarketplace(plugin.name, spec, agent, versionHome))
|
|
744
|
+
return false;
|
|
745
|
+
// Droid additionally requires its installed_plugins.json registry entry —
|
|
746
|
+
// without it the marketplace copy is invisible to `droid plugin list`.
|
|
747
|
+
if (agent === 'droid') {
|
|
748
|
+
return isDroidPluginInstalled(plugin.name, marketplaceNameFor(spec), agent, versionHome);
|
|
749
|
+
}
|
|
750
|
+
return true;
|
|
722
751
|
}
|
|
723
752
|
// ─── Removal ─────────────────────────────────────────────────────────────────
|
|
724
753
|
/**
|
|
@@ -746,6 +775,12 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
746
775
|
removedAny = true;
|
|
747
776
|
}
|
|
748
777
|
removePluginFromSettings(pluginName, name, agent, versionHome);
|
|
778
|
+
if (agent === 'droid') {
|
|
779
|
+
unregisterDroidInstalledPlugin(pluginName, name, agent, versionHome);
|
|
780
|
+
}
|
|
781
|
+
if (agent === 'copilot') {
|
|
782
|
+
unregisterCopilotInstalledPlugin(pluginName, name, agent, versionHome);
|
|
783
|
+
}
|
|
749
784
|
// Refresh marketplace.json so it reflects what's left under plugins/.
|
|
750
785
|
syncMarketplaceManifest(spec, agent, versionHome);
|
|
751
786
|
// If we just removed the last plugin, drop the marketplace dir and the
|
|
@@ -930,6 +965,12 @@ export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames,
|
|
|
930
965
|
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
931
966
|
fs.renameSync(path.join(mktPluginsDir, entry.name), trashDest);
|
|
932
967
|
removePluginFromSettings(entry.name, name, agent, versionHome);
|
|
968
|
+
if (agent === 'droid') {
|
|
969
|
+
unregisterDroidInstalledPlugin(entry.name, name, agent, versionHome);
|
|
970
|
+
}
|
|
971
|
+
if (agent === 'copilot') {
|
|
972
|
+
unregisterCopilotInstalledPlugin(entry.name, name, agent, versionHome);
|
|
973
|
+
}
|
|
933
974
|
removed.push(entry.name);
|
|
934
975
|
trashedHere = true;
|
|
935
976
|
}
|
|
@@ -38,6 +38,13 @@ export interface ActiveSession {
|
|
|
38
38
|
* Absent for cloud sessions (no local pid) and any pid whose env is unreadable.
|
|
39
39
|
*/
|
|
40
40
|
provenance?: SessionProvenance;
|
|
41
|
+
/**
|
|
42
|
+
* The machine this session runs on, as a normalized device id (machineId()
|
|
43
|
+
* form). Set when merging cross-machine results so the grouped `--active`
|
|
44
|
+
* view can bucket by computer. Absent for a purely local query (the renderer
|
|
45
|
+
* falls back to provenance.host, then the local machine).
|
|
46
|
+
*/
|
|
47
|
+
machine?: string;
|
|
41
48
|
teamName?: string;
|
|
42
49
|
agentId?: string;
|
|
43
50
|
cloudProvider?: string;
|
|
@@ -24,6 +24,7 @@ import { promisify } from 'util';
|
|
|
24
24
|
import { listActiveTasks } from '../cloud/store.js';
|
|
25
25
|
import { AgentManager } from '../teams/agents.js';
|
|
26
26
|
import { getTerminalsDir } from '../state.js';
|
|
27
|
+
import { readPidSessionEntry, prunePidSessionRegistry } from './pid-registry.js';
|
|
27
28
|
import { buildClaudeLabelMap } from './discover.js';
|
|
28
29
|
import { latestSessionFileForCwd } from './db.js';
|
|
29
30
|
import { extractSessionTopic } from './prompt.js';
|
|
@@ -499,8 +500,13 @@ export async function listUnattributedActive(attributed) {
|
|
|
499
500
|
const out = [];
|
|
500
501
|
for (let i = 0; i < candidates.length; i++) {
|
|
501
502
|
const { pid, kind } = candidates[i];
|
|
502
|
-
|
|
503
|
-
|
|
503
|
+
// The per-pid registry (written by `ag run`) gives the EXACT session id this
|
|
504
|
+
// pid was launched with — so N agents in one cwd resolve to N distinct
|
|
505
|
+
// sessions instead of all collapsing onto the newest .jsonl. Absent (agent
|
|
506
|
+
// not launched via `ag run`, or one that takes no session id) → heuristic.
|
|
507
|
+
const entry = readPidSessionEntry(pid);
|
|
508
|
+
const cwd = cwds[i] ?? entry?.cwd ?? undefined;
|
|
509
|
+
const sessionFile = findSessionFileForKind(kind, cwd, entry?.sessionId);
|
|
504
510
|
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
505
511
|
const host = detectHost(pid, procByPid);
|
|
506
512
|
const context = host && UI_HOSTS.has(host) ? 'terminal' : 'headless';
|
|
@@ -511,11 +517,13 @@ export async function listUnattributedActive(attributed) {
|
|
|
511
517
|
host,
|
|
512
518
|
pid,
|
|
513
519
|
cwd,
|
|
514
|
-
sessionId: sessionIdFromFile(sessionFile),
|
|
520
|
+
sessionId: entry?.sessionId ?? sessionIdFromFile(sessionFile),
|
|
515
521
|
topic,
|
|
516
522
|
sessionFile,
|
|
517
523
|
}, state, sessionFile));
|
|
518
524
|
}
|
|
525
|
+
// Housekeeping: drop registry files for pids that have since died.
|
|
526
|
+
prunePidSessionRegistry(isPidAlive);
|
|
519
527
|
return out;
|
|
520
528
|
}
|
|
521
529
|
/**
|