@phnx-labs/agents-cli 1.19.2 → 1.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -0
- package/README.md +72 -12
- package/dist/browser.js +0 -0
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +27 -10
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +38 -18
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +89 -10
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +118 -5
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +58 -5
- package/dist/commands/routines.js +107 -14
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +79 -46
- package/dist/commands/sessions.d.ts +28 -0
- package/dist/commands/sessions.js +98 -33
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +25 -8
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +61 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +134 -130
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +175 -19
- package/dist/commands/workflows.js +29 -6
- package/dist/computer.js +0 -0
- package/dist/index.js +38 -6
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +125 -34
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +46 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +2 -2
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +16 -3
- package/dist/lib/browser/profiles.js +44 -4
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +40 -5
- package/dist/lib/browser/types.d.ts +11 -4
- package/dist/lib/cli-resources.d.ts +137 -0
- package/dist/lib/cli-resources.js +477 -0
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +42 -13
- package/dist/lib/exec.js +127 -33
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +246 -11
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +46 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +55 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +216 -0
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/resources/mcp.js +37 -0
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +47 -0
- package/dist/lib/routines-format.js +194 -0
- package/dist/lib/routines.d.ts +8 -2
- package/dist/lib/routines.js +34 -14
- package/dist/lib/runner.js +83 -15
- package/dist/lib/scheduler.js +8 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +34 -17
- package/dist/lib/secrets/bundles.js +210 -36
- package/dist/lib/secrets/index.d.ts +49 -30
- package/dist/lib/secrets/index.js +126 -115
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +0 -4
- package/dist/lib/session/db.js +0 -26
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +70 -38
- package/dist/lib/state.d.ts +14 -2
- package/dist/lib/state.js +51 -20
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +48 -22
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +63 -4
- package/dist/lib/types.js +8 -3
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +45 -3
- package/dist/lib/versions.js +455 -60
- package/package.json +15 -14
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/npm-shrinkwrap.json +0 -3162
package/dist/commands/secrets.js
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
11
|
-
import { deleteKeychainToken,
|
|
11
|
+
import { deleteKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
12
12
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
13
13
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
14
14
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
15
|
+
import { registerSecretsSyncCommands } from './secrets-sync.js';
|
|
16
|
+
import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
|
|
15
17
|
/** Prompt the user for a secret value with masked input. Requires an interactive TTY. */
|
|
16
18
|
async function promptForSecret(message) {
|
|
17
19
|
if (!isInteractiveTerminal()) {
|
|
@@ -178,7 +180,6 @@ function humanAge(iso) {
|
|
|
178
180
|
function renderBundleRow(b) {
|
|
179
181
|
const entries = describeBundle(b);
|
|
180
182
|
const keys = entries.length;
|
|
181
|
-
const sync = b.icloud_sync ? chalk.cyan('icloud') : '';
|
|
182
183
|
const expiringCount = countExpiringSoon(b.meta);
|
|
183
184
|
const expiring = expiringCount > 0 ? chalk.yellow(String(expiringCount)) : chalk.gray('-');
|
|
184
185
|
// Timestamp distinction:
|
|
@@ -194,12 +195,11 @@ function renderBundleRow(b) {
|
|
|
194
195
|
: (b.created_at ? chalk.gray('never') : chalk.gray('?'));
|
|
195
196
|
const head = `${chalk.cyan(b.name.padEnd(20))} ` +
|
|
196
197
|
`${String(keys).padEnd(5)} ` +
|
|
197
|
-
`${padVisible(sync, 6)} ` +
|
|
198
198
|
`${padVisible(expiring, 9)} ` +
|
|
199
199
|
`${padVisible(created, 9)} ` +
|
|
200
200
|
`${padVisible(updated, 9)} ` +
|
|
201
201
|
`${padVisible(used, 7)}`;
|
|
202
|
-
return b.description ? `${head} ${chalk.gray(b.description)}` : head.trimEnd();
|
|
202
|
+
return b.description ? `${head} ${chalk.gray(safePrint(b.description))}` : head.trimEnd();
|
|
203
203
|
}
|
|
204
204
|
/** Colorize a variable source kind (literal, keychain, env, file, exec). */
|
|
205
205
|
function kindLabel(kind) {
|
|
@@ -220,6 +220,17 @@ function redact(value, reveal) {
|
|
|
220
220
|
return '';
|
|
221
221
|
return '*'.repeat(Math.min(value.length, 8));
|
|
222
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Strip ASCII / C1 control bytes from a string before printing it to the
|
|
225
|
+
* terminal. Bundle descriptions, notes, and remote-supplied names can carry
|
|
226
|
+
* arbitrary text and a malicious value containing ANSI escape sequences (e.g.
|
|
227
|
+
* OSC 52 clipboard set, screen-clear, cursor moves) would otherwise be
|
|
228
|
+
* interpreted by the user's terminal. Allow tab and newline so multi-line
|
|
229
|
+
* notes still render; strip everything else in the C0/C1 ranges plus DEL.
|
|
230
|
+
*/
|
|
231
|
+
function safePrint(s) {
|
|
232
|
+
return s.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, '');
|
|
233
|
+
}
|
|
223
234
|
/**
|
|
224
235
|
* Build a VarMeta patch from CLI flags. Validates each provided field. Returns
|
|
225
236
|
* undefined if no meta flag was passed (so callers know to skip meta updates).
|
|
@@ -286,7 +297,7 @@ function renderMetaLine(meta, reveal) {
|
|
|
286
297
|
parts.push(colored);
|
|
287
298
|
}
|
|
288
299
|
if (meta.note) {
|
|
289
|
-
let note = meta.note;
|
|
300
|
+
let note = safePrint(meta.note);
|
|
290
301
|
if (!reveal && note.length > 80) {
|
|
291
302
|
note = note.slice(0, 79) + '\u2026';
|
|
292
303
|
}
|
|
@@ -314,7 +325,7 @@ function countExpiringSoon(meta) {
|
|
|
314
325
|
export function registerSecretsCommands(program) {
|
|
315
326
|
const cmd = program
|
|
316
327
|
.command('secrets')
|
|
317
|
-
.description('Named bundles of env variables backed by macOS Keychain (
|
|
328
|
+
.description('Named bundles of env variables backed by macOS Keychain (device-local, biometry-gated). Inject into agents via `agents run --secrets <name>`.');
|
|
318
329
|
setHelpSections(cmd, {
|
|
319
330
|
examples: `
|
|
320
331
|
# Create a bundle
|
|
@@ -340,23 +351,22 @@ export function registerSecretsCommands(program) {
|
|
|
340
351
|
`,
|
|
341
352
|
notes: `
|
|
342
353
|
Bundles are containers; secrets are the variables inside them. Keychain values
|
|
343
|
-
never touch disk in plaintext.
|
|
344
|
-
|
|
345
|
-
iCloud sync: new bundles use the iCloud-synced keychain by default so they
|
|
346
|
-
appear on other Macs (same iCloud account, iCloud Keychain enabled). Pass
|
|
347
|
-
--no-icloud-sync at create time to keep values device-local instead.
|
|
354
|
+
never touch disk in plaintext. Every item is device-local and gated by Touch ID
|
|
355
|
+
or device passcode; cross-machine sync is handled by 'agents secrets push/pull'.
|
|
348
356
|
|
|
349
357
|
See also:
|
|
350
358
|
agents secrets rotate <bundle> <key> rotate value, preserve metadata
|
|
351
359
|
agents secrets import <bundle> --from .env bulk import from .env
|
|
352
360
|
agents secrets import <bundle> --from-1password --vault <name>
|
|
353
361
|
agents secrets generate [length] generate a random password / PIN / hex
|
|
362
|
+
agents secrets migrate-acl upgrade legacy items to the biometry ACL
|
|
354
363
|
`,
|
|
355
364
|
});
|
|
356
365
|
registerCommandGroups(cmd, [
|
|
357
366
|
{ title: 'Bundle commands', names: ['list', 'view', 'create', 'rename', 'describe', 'delete'] },
|
|
358
367
|
{ title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
|
|
359
|
-
{ title: '
|
|
368
|
+
{ title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
|
|
369
|
+
{ title: 'Utilities', names: ['exec', 'generate', 'migrate-acl'] },
|
|
360
370
|
]);
|
|
361
371
|
cmd
|
|
362
372
|
.command('list')
|
|
@@ -369,7 +379,7 @@ export function registerSecretsCommands(program) {
|
|
|
369
379
|
console.log(chalk.gray('Try: agents secrets create <name>'));
|
|
370
380
|
return;
|
|
371
381
|
}
|
|
372
|
-
console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'
|
|
382
|
+
console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
|
|
373
383
|
for (const b of bundles) {
|
|
374
384
|
console.log(renderBundleRow(b));
|
|
375
385
|
}
|
|
@@ -387,11 +397,9 @@ export function registerSecretsCommands(program) {
|
|
|
387
397
|
const entries = describeBundle(bundle);
|
|
388
398
|
console.log(chalk.bold(bundle.name));
|
|
389
399
|
if (bundle.description)
|
|
390
|
-
console.log(chalk.gray(bundle.description));
|
|
400
|
+
console.log(chalk.gray(safePrint(bundle.description)));
|
|
391
401
|
if (bundle.allow_exec)
|
|
392
402
|
console.log(chalk.yellow('allow_exec: true'));
|
|
393
|
-
if (bundle.icloud_sync)
|
|
394
|
-
console.log(chalk.cyan('icloud_sync: true'));
|
|
395
403
|
if (bundle.created_at)
|
|
396
404
|
console.log(chalk.gray(`created_at: ${bundle.created_at} (${humanAge(bundle.created_at)})`));
|
|
397
405
|
if (bundle.updated_at)
|
|
@@ -408,19 +416,30 @@ export function registerSecretsCommands(program) {
|
|
|
408
416
|
console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
|
|
409
417
|
process.exit(1);
|
|
410
418
|
}
|
|
419
|
+
// Batch every keychain read into one helper call so --reveal pops
|
|
420
|
+
// Touch ID once for the whole bundle instead of once per key.
|
|
421
|
+
const revealedValues = new Map();
|
|
422
|
+
if (reveal) {
|
|
423
|
+
const items = entries
|
|
424
|
+
.filter((e) => e.kind === 'keychain')
|
|
425
|
+
.map((e) => secretsKeychainItem(bundle.name, e.detail));
|
|
426
|
+
try {
|
|
427
|
+
const fetched = getKeychainTokens(items);
|
|
428
|
+
for (const [item, value] of fetched)
|
|
429
|
+
revealedValues.set(item, value);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Fall through to masked output on cancellation / batch failure.
|
|
433
|
+
}
|
|
434
|
+
}
|
|
411
435
|
for (const e of entries) {
|
|
412
436
|
if (e.kind === 'keychain') {
|
|
413
437
|
const item = secretsKeychainItem(bundle.name, e.detail);
|
|
414
|
-
const stored = hasKeychainToken(item
|
|
438
|
+
const stored = hasKeychainToken(item);
|
|
415
439
|
const marker = stored ? chalk.green('stored') : chalk.red('missing');
|
|
416
440
|
let valueCol = `[keychain:${e.detail}] ${marker}`;
|
|
417
|
-
if (reveal &&
|
|
418
|
-
|
|
419
|
-
valueCol = redact(getKeychainToken(item, bundle.icloud_sync), true);
|
|
420
|
-
}
|
|
421
|
-
catch {
|
|
422
|
-
// fall through to masked
|
|
423
|
-
}
|
|
441
|
+
if (reveal && revealedValues.has(item)) {
|
|
442
|
+
valueCol = redact(revealedValues.get(item), true);
|
|
424
443
|
}
|
|
425
444
|
console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${valueCol}`);
|
|
426
445
|
}
|
|
@@ -429,7 +448,7 @@ export function registerSecretsCommands(program) {
|
|
|
429
448
|
const literalValue = typeof raw === 'string'
|
|
430
449
|
? raw
|
|
431
450
|
: (raw && typeof raw === 'object' && 'value' in raw ? raw.value : '');
|
|
432
|
-
console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${literalValue}`);
|
|
451
|
+
console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${redact(literalValue, reveal)}`);
|
|
433
452
|
}
|
|
434
453
|
else {
|
|
435
454
|
console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${e.detail}`);
|
|
@@ -451,7 +470,6 @@ export function registerSecretsCommands(program) {
|
|
|
451
470
|
.description('Create an empty bundle')
|
|
452
471
|
.option('--description <text>', 'Free-form description')
|
|
453
472
|
.option('--allow-exec', 'Allow exec: refs in this bundle (off by default)')
|
|
454
|
-
.option('--no-icloud-sync', 'Store keychain values device-local instead of syncing them via iCloud Keychain')
|
|
455
473
|
.option('--force', 'Overwrite an existing bundle')
|
|
456
474
|
.action(async (name, opts) => {
|
|
457
475
|
try {
|
|
@@ -465,7 +483,6 @@ export function registerSecretsCommands(program) {
|
|
|
465
483
|
name: resolvedName,
|
|
466
484
|
description: opts.description,
|
|
467
485
|
allow_exec: opts.allowExec,
|
|
468
|
-
icloud_sync: opts.icloudSync !== false,
|
|
469
486
|
vars: {},
|
|
470
487
|
};
|
|
471
488
|
writeBundle(bundle);
|
|
@@ -600,12 +617,11 @@ export function registerSecretsCommands(program) {
|
|
|
600
617
|
secretValue = await promptForSecret(`Enter value for ${resolvedBundleName}.${resolvedKey}`);
|
|
601
618
|
}
|
|
602
619
|
const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
|
|
603
|
-
setKeychainToken(item, secretValue
|
|
620
|
+
setKeychainToken(item, secretValue);
|
|
604
621
|
bundle.vars[resolvedKey] = keychainRef(resolvedKey);
|
|
605
622
|
applyMeta();
|
|
606
623
|
writeBundle(bundle);
|
|
607
|
-
|
|
608
|
-
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in ${where} (${item}).`));
|
|
624
|
+
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in keychain (${item}).`));
|
|
609
625
|
}
|
|
610
626
|
catch (err) {
|
|
611
627
|
if (isPromptCancelled(err))
|
|
@@ -665,8 +681,7 @@ Examples:
|
|
|
665
681
|
clearMeta: opts.clearMeta,
|
|
666
682
|
meta: metaPatch,
|
|
667
683
|
});
|
|
668
|
-
|
|
669
|
-
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in ${where}.`));
|
|
684
|
+
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in keychain.`));
|
|
670
685
|
}
|
|
671
686
|
catch (err) {
|
|
672
687
|
if (isPromptCancelled(err))
|
|
@@ -679,6 +694,7 @@ Examples:
|
|
|
679
694
|
.command('remove [bundle] [key]')
|
|
680
695
|
.description('Remove a key from the bundle. Purges the keychain item if the ref was keychain:. Use --keep-secret to retain it.')
|
|
681
696
|
.option('--keep-secret', 'Leave the keychain item in place after removing the ref from the bundle')
|
|
697
|
+
.option('-y, --yes', 'Skip the confirmation prompt when purging a keychain item')
|
|
682
698
|
.action(async (bundleName, key, opts) => {
|
|
683
699
|
try {
|
|
684
700
|
const resolvedBundleName = bundleName ?? (await pickBundleName('remove from'));
|
|
@@ -689,11 +705,28 @@ Examples:
|
|
|
689
705
|
process.exit(1);
|
|
690
706
|
}
|
|
691
707
|
const raw = bundle.vars[resolvedKey];
|
|
708
|
+
const willPurge = !opts.keepSecret && typeof raw === 'string' && raw.startsWith('keychain:');
|
|
709
|
+
if (willPurge && !opts.yes) {
|
|
710
|
+
if (!isInteractiveTerminal()) {
|
|
711
|
+
console.error(chalk.red(`Refusing to purge keychain item for ${resolvedBundleName}.${resolvedKey} non-interactively. ` +
|
|
712
|
+
`Pass --yes to confirm or --keep-secret to retain the keychain entry.`));
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
716
|
+
const ok = await confirm({
|
|
717
|
+
message: `Purge keychain item for ${resolvedBundleName}.${resolvedKey}? (use --keep-secret to retain)`,
|
|
718
|
+
default: false,
|
|
719
|
+
});
|
|
720
|
+
if (!ok) {
|
|
721
|
+
console.log(chalk.gray('Aborted. Bundle metadata unchanged.'));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
692
725
|
delete bundle.vars[resolvedKey];
|
|
693
726
|
writeBundle(bundle);
|
|
694
|
-
if (
|
|
727
|
+
if (willPurge) {
|
|
695
728
|
const item = secretsKeychainItem(resolvedBundleName, raw.slice('keychain:'.length));
|
|
696
|
-
const removed = deleteKeychainToken(item
|
|
729
|
+
const removed = deleteKeychainToken(item);
|
|
697
730
|
if (removed) {
|
|
698
731
|
console.log(chalk.green(`Removed ${resolvedBundleName}.${resolvedKey} and purged keychain item.`));
|
|
699
732
|
return;
|
|
@@ -738,7 +771,7 @@ Examples:
|
|
|
738
771
|
}
|
|
739
772
|
if (!opts.keepSecrets) {
|
|
740
773
|
for (const { item } of keychainItemsForBundle(bundle)) {
|
|
741
|
-
deleteKeychainToken(item
|
|
774
|
+
deleteKeychainToken(item);
|
|
742
775
|
}
|
|
743
776
|
}
|
|
744
777
|
const existed = deleteBundle(resolvedName);
|
|
@@ -824,7 +857,7 @@ Examples:
|
|
|
824
857
|
}
|
|
825
858
|
else {
|
|
826
859
|
const item = secretsKeychainItem(resolvedBundleName, envKey);
|
|
827
|
-
setKeychainToken(item, value
|
|
860
|
+
setKeychainToken(item, value);
|
|
828
861
|
bundle.vars[envKey] = keychainRef(envKey);
|
|
829
862
|
}
|
|
830
863
|
added++;
|
|
@@ -848,7 +881,7 @@ Examples:
|
|
|
848
881
|
}
|
|
849
882
|
else {
|
|
850
883
|
const item = secretsKeychainItem(resolvedBundleName, key);
|
|
851
|
-
setKeychainToken(item, value
|
|
884
|
+
setKeychainToken(item, value);
|
|
852
885
|
bundle.vars[key] = keychainRef(key);
|
|
853
886
|
}
|
|
854
887
|
added++;
|
|
@@ -873,13 +906,12 @@ Examples:
|
|
|
873
906
|
.option('--force', 'Overwrite existing 1Password items (used with --to-1password)')
|
|
874
907
|
.action(async (bundleName, opts) => {
|
|
875
908
|
try {
|
|
876
|
-
const {
|
|
909
|
+
const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
877
910
|
const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
|
|
878
|
-
const bundle = readBundle(resolvedBundleName);
|
|
879
911
|
if (opts.to1password) {
|
|
880
912
|
assertOpAvailable();
|
|
881
913
|
const vault = await resolveVault(opts.vault);
|
|
882
|
-
const env =
|
|
914
|
+
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `1Password vault ${vault}` });
|
|
883
915
|
let created = 0;
|
|
884
916
|
let overwritten = 0;
|
|
885
917
|
let skipped = 0;
|
|
@@ -909,11 +941,11 @@ Examples:
|
|
|
909
941
|
console.log(chalk.green(`Exported to 1Password vault '${vault}': ${parts.join(', ')}.`));
|
|
910
942
|
return;
|
|
911
943
|
}
|
|
912
|
-
if (
|
|
913
|
-
console.error(chalk.red('export
|
|
944
|
+
if (!opts.plaintext) {
|
|
945
|
+
console.error(chalk.red('export prints secrets in the clear and requires --plaintext (works for TTY and pipes alike).'));
|
|
914
946
|
process.exit(1);
|
|
915
947
|
}
|
|
916
|
-
const env =
|
|
948
|
+
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `export to shell` });
|
|
917
949
|
const prefix = bundleToEnvPrefix(resolvedBundleName);
|
|
918
950
|
for (const [k, v] of Object.entries(env)) {
|
|
919
951
|
const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
|
|
@@ -939,10 +971,9 @@ Examples:
|
|
|
939
971
|
console.error(chalk.red('Usage: agents secrets exec <bundle> -- <command...>'));
|
|
940
972
|
process.exit(1);
|
|
941
973
|
}
|
|
942
|
-
const {
|
|
943
|
-
const bundle = readBundle(bundleName);
|
|
974
|
+
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
944
975
|
const [cmd, ...args] = commandParts;
|
|
945
|
-
const secretEnv =
|
|
976
|
+
const { env: secretEnv } = readAndResolveBundleEnv(bundleName, { caller: `command ${cmd}` });
|
|
946
977
|
const { spawn } = await import('child_process');
|
|
947
978
|
const proc = spawn(cmd, args, {
|
|
948
979
|
stdio: 'inherit',
|
|
@@ -1035,4 +1066,6 @@ Examples:
|
|
|
1035
1066
|
console.log(password);
|
|
1036
1067
|
}
|
|
1037
1068
|
});
|
|
1069
|
+
registerSecretsSyncCommands(cmd);
|
|
1070
|
+
registerSecretsMigrateAclCommand(cmd);
|
|
1038
1071
|
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { SessionMeta } from '../lib/session/types.js';
|
|
3
|
+
import { type ActiveSession } from '../lib/session/active.js';
|
|
4
|
+
/** Grouped + sorted view of active sessions for the --active renderer. */
|
|
5
|
+
export interface ActiveSessionsLayout {
|
|
6
|
+
workspaces: Array<{
|
|
7
|
+
/** Internal grouping key — `__cloud__`, `__unknown__`, or the cwd. */
|
|
8
|
+
key: string;
|
|
9
|
+
/** Sessions in this workspace, both windowed and flat (preserves total count). */
|
|
10
|
+
total: number;
|
|
11
|
+
/** Terminals grouped by IDE window (sorted by oldest startedAtMs). */
|
|
12
|
+
windows: Array<{
|
|
13
|
+
windowId: string;
|
|
14
|
+
sessions: ActiveSession[];
|
|
15
|
+
}>;
|
|
16
|
+
/** Everything else in this workspace: cloud, teams, headless, terminals without a windowId. */
|
|
17
|
+
flat: ActiveSession[];
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Group sessions by workspace, then split each workspace into IDE-window
|
|
22
|
+
* sub-groups + a flat bucket. Pure function — no I/O — so the renderer's
|
|
23
|
+
* grouping rules can be tested without mocking the session scanner.
|
|
24
|
+
*
|
|
25
|
+
* Sort order:
|
|
26
|
+
* - workspaces: by session count descending, then key ascending
|
|
27
|
+
* - windows within a workspace: by oldest startedAtMs ascending
|
|
28
|
+
* - sessions within a window/flat bucket: input order preserved
|
|
29
|
+
*/
|
|
30
|
+
export declare function groupActiveSessions(sessions: ActiveSession[]): ActiveSessionsLayout;
|
|
3
31
|
/**
|
|
4
32
|
* Build the shell command that resumes a picked session.
|
|
5
33
|
*
|
|
@@ -181,18 +181,45 @@ function buildSessionDescription(s) {
|
|
|
181
181
|
return s.topic;
|
|
182
182
|
return '';
|
|
183
183
|
}
|
|
184
|
-
/**
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Render a single agent-session row inside an already-printed group header.
|
|
186
|
+
* Indent is the leading whitespace (2 spaces for flat groups, 4 inside a
|
|
187
|
+
* window sub-group).
|
|
188
|
+
*/
|
|
189
|
+
function printActiveRow(s, indent) {
|
|
190
|
+
const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
|
|
191
|
+
const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
|
|
192
|
+
const statusCol = statusColor(s.status)(padRight(truncate(s.status, 7), 8));
|
|
193
|
+
const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
|
|
194
|
+
const desc = buildSessionDescription(s);
|
|
195
|
+
console.log(indent +
|
|
196
|
+
pidCol +
|
|
197
|
+
kindCol +
|
|
198
|
+
hostCol +
|
|
199
|
+
statusCol +
|
|
200
|
+
chalk.white(truncate(desc || '-', 50)));
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Short label for an IDE window. The slice key in live-terminals.json is
|
|
204
|
+
* `${vscode.env.sessionId}-${ext-host pid}`; the trailing pid is the cheap
|
|
205
|
+
* stable disambiguator. We surface it as `ext-pid` so two windows on the
|
|
206
|
+
* same repo are visibly different.
|
|
207
|
+
*/
|
|
208
|
+
function shortWindowLabel(windowId) {
|
|
209
|
+
const m = windowId.match(/-(\d+)$/);
|
|
210
|
+
return m ? `ext-pid ${m[1]}` : `win ${windowId.slice(0, 8)}`;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Group sessions by workspace, then split each workspace into IDE-window
|
|
214
|
+
* sub-groups + a flat bucket. Pure function — no I/O — so the renderer's
|
|
215
|
+
* grouping rules can be tested without mocking the session scanner.
|
|
216
|
+
*
|
|
217
|
+
* Sort order:
|
|
218
|
+
* - workspaces: by session count descending, then key ascending
|
|
219
|
+
* - windows within a workspace: by oldest startedAtMs ascending
|
|
220
|
+
* - sessions within a window/flat bucket: input order preserved
|
|
221
|
+
*/
|
|
222
|
+
export function groupActiveSessions(sessions) {
|
|
196
223
|
const byWorkspace = new Map();
|
|
197
224
|
for (const s of sessions) {
|
|
198
225
|
const key = s.cwd ?? (s.context === 'cloud' ? '__cloud__' : '__unknown__');
|
|
@@ -200,7 +227,6 @@ async function renderActiveSessions(asJson) {
|
|
|
200
227
|
list.push(s);
|
|
201
228
|
byWorkspace.set(key, list);
|
|
202
229
|
}
|
|
203
|
-
// Sort workspaces: most sessions first, then alphabetically
|
|
204
230
|
const sortedKeys = Array.from(byWorkspace.keys()).sort((a, b) => {
|
|
205
231
|
const aCount = byWorkspace.get(a).length;
|
|
206
232
|
const bCount = byWorkspace.get(b).length;
|
|
@@ -208,33 +234,71 @@ async function renderActiveSessions(asJson) {
|
|
|
208
234
|
return bCount - aCount;
|
|
209
235
|
return a.localeCompare(b);
|
|
210
236
|
});
|
|
211
|
-
|
|
212
|
-
for (const key of sortedKeys) {
|
|
237
|
+
const workspaces = sortedKeys.map((key) => {
|
|
213
238
|
const group = byWorkspace.get(key);
|
|
239
|
+
const windowedSessions = [];
|
|
240
|
+
const flat = [];
|
|
241
|
+
for (const s of group) {
|
|
242
|
+
if (s.context === 'terminal' && s.windowId)
|
|
243
|
+
windowedSessions.push(s);
|
|
244
|
+
else
|
|
245
|
+
flat.push(s);
|
|
246
|
+
}
|
|
247
|
+
const byWindow = new Map();
|
|
248
|
+
for (const s of windowedSessions) {
|
|
249
|
+
const list = byWindow.get(s.windowId) || [];
|
|
250
|
+
list.push(s);
|
|
251
|
+
byWindow.set(s.windowId, list);
|
|
252
|
+
}
|
|
253
|
+
const windowKeys = Array.from(byWindow.keys()).sort((a, b) => {
|
|
254
|
+
const aStart = Math.min(...byWindow.get(a).map(s => s.startedAtMs ?? Infinity));
|
|
255
|
+
const bStart = Math.min(...byWindow.get(b).map(s => s.startedAtMs ?? Infinity));
|
|
256
|
+
return aStart - bStart;
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
key,
|
|
260
|
+
total: group.length,
|
|
261
|
+
windows: windowKeys.map((wid) => ({ windowId: wid, sessions: byWindow.get(wid) })),
|
|
262
|
+
flat,
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
return { workspaces };
|
|
266
|
+
}
|
|
267
|
+
/** Render the unified active-session view. */
|
|
268
|
+
async function renderActiveSessions(asJson) {
|
|
269
|
+
const sessions = await getActiveSessions();
|
|
270
|
+
if (asJson) {
|
|
271
|
+
process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (sessions.length === 0) {
|
|
275
|
+
console.log(chalk.gray('No active agent sessions.'));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const layout = groupActiveSessions(sessions);
|
|
279
|
+
let first = true;
|
|
280
|
+
for (const ws of layout.workspaces) {
|
|
214
281
|
if (!first)
|
|
215
282
|
console.log();
|
|
216
283
|
first = false;
|
|
217
284
|
// Print workspace header
|
|
218
|
-
const header = key === '__cloud__'
|
|
285
|
+
const header = ws.key === '__cloud__'
|
|
219
286
|
? chalk.magenta.bold('cloud')
|
|
220
|
-
: key === '__unknown__'
|
|
287
|
+
: ws.key === '__unknown__'
|
|
221
288
|
? chalk.gray.bold('unknown')
|
|
222
|
-
: chalk.cyan.bold(shortCwd(key));
|
|
223
|
-
console.log(`${header} ${chalk.gray(`(${
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
pidCol +
|
|
233
|
-
kindCol +
|
|
234
|
-
hostCol +
|
|
235
|
-
statusCol +
|
|
236
|
-
chalk.white(truncate(desc || '-', 50)));
|
|
289
|
+
: chalk.cyan.bold(shortCwd(ws.key));
|
|
290
|
+
console.log(`${header} ${chalk.gray(`(${ws.total})`)}`);
|
|
291
|
+
for (const win of ws.windows) {
|
|
292
|
+
// Host is per-process, but every terminal in the same IDE window shares
|
|
293
|
+
// an ancestor — take the first non-empty host as the window's label.
|
|
294
|
+
const host = win.sessions.find((s) => s.host)?.host ?? 'terminal';
|
|
295
|
+
const winHeader = `${chalk.gray(host)} ${chalk.gray('·')} ${chalk.gray(shortWindowLabel(win.windowId))} ${chalk.gray(`(${win.sessions.length})`)}`;
|
|
296
|
+
console.log(' ' + winHeader);
|
|
297
|
+
for (const s of win.sessions)
|
|
298
|
+
printActiveRow(s, ' ');
|
|
237
299
|
}
|
|
300
|
+
for (const s of ws.flat)
|
|
301
|
+
printActiveRow(s, ' ');
|
|
238
302
|
}
|
|
239
303
|
const runningCount = sessions.filter(s => s.status === 'running').length;
|
|
240
304
|
const idleCount = sessions.filter(s => s.status === 'idle').length;
|
|
@@ -673,7 +737,8 @@ export function buildResumeCommand(session) {
|
|
|
673
737
|
case 'openclaw':
|
|
674
738
|
case 'rush':
|
|
675
739
|
case 'hermes':
|
|
676
|
-
|
|
740
|
+
case 'grok':
|
|
741
|
+
// Grok (and some others) sessions are captured artifacts, not resumable the same way.
|
|
677
742
|
return null;
|
|
678
743
|
}
|
|
679
744
|
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { Command } from 'commander';
|
|
|
9
9
|
export declare function runSetup(program: Command, options?: {
|
|
10
10
|
force?: boolean;
|
|
11
11
|
suppressFooter?: boolean;
|
|
12
|
+
systemRepo?: boolean;
|
|
12
13
|
}): Promise<void>;
|
|
13
14
|
/**
|
|
14
15
|
* Ensure the system repo exists before running a command that needs it.
|
package/dist/commands/setup.js
CHANGED
|
@@ -68,38 +68,46 @@ export async function runSetup(program, options = {}) {
|
|
|
68
68
|
for (const install of unmanaged) {
|
|
69
69
|
sessionCounts[install.agentId] = countSessionFiles(install.agentId);
|
|
70
70
|
}
|
|
71
|
+
const systemRepo = process.env.AGENTS_SYSTEM_REPO || DEFAULT_SYSTEM_REPO;
|
|
71
72
|
console.log(chalk.bold('\nWelcome to agents-cli.'));
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// --force on an existing repo: pull instead of re-clone
|
|
77
|
-
const result = await pullRepo(agentsDir);
|
|
78
|
-
if (!result.success) {
|
|
79
|
-
spinner.fail(`Pull failed: ${result.error}`);
|
|
80
|
-
console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
spinner.succeed(`Updated to ${result.commit}`);
|
|
73
|
+
if (options.systemRepo === false) {
|
|
74
|
+
ensureAgentsDir();
|
|
75
|
+
console.log(chalk.gray('Skipping system repo clone (--no-system-repo).'));
|
|
76
|
+
console.log(chalk.gray(`Populate ~/.agents-system/ yourself before running other commands that depend on it.\n`));
|
|
84
77
|
}
|
|
85
78
|
else {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
console.log(chalk.gray(`Cloning the system repo from ${systemRepoSlug(systemRepo)} into ~/.agents-system/.\n`));
|
|
80
|
+
ensureAgentsDir();
|
|
81
|
+
const spinner = ora('Cloning system repo...').start();
|
|
82
|
+
if (isGitRepo(agentsDir)) {
|
|
83
|
+
// --force on an existing repo: pull instead of re-clone
|
|
84
|
+
const result = await pullRepo(agentsDir);
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
spinner.fail(`Pull failed: ${result.error}`);
|
|
87
|
+
console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
spinner.succeed(`Updated to ${result.commit}`);
|
|
95
91
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
else {
|
|
93
|
+
// Check git is available
|
|
94
|
+
try {
|
|
95
|
+
const { execSync } = await import('child_process');
|
|
96
|
+
execSync('which git', { stdio: 'ignore' });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
spinner.fail('git is not installed');
|
|
100
|
+
console.log(chalk.gray('Install git first: https://git-scm.com/downloads'));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const result = await cloneIntoExisting(systemRepo, agentsDir);
|
|
104
|
+
if (!result.success) {
|
|
105
|
+
spinner.fail(`Clone failed: ${result.error}`);
|
|
106
|
+
console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
spinner.succeed(`Cloned ${systemRepoSlug(systemRepo)} (${result.commit})`);
|
|
101
110
|
}
|
|
102
|
-
spinner.succeed(`Cloned ${systemRepoSlug(DEFAULT_SYSTEM_REPO)} (${result.commit})`);
|
|
103
111
|
}
|
|
104
112
|
// Offer to import existing unmanaged installations
|
|
105
113
|
if (unmanaged.length > 0 && isInteractiveTerminal()) {
|
|
@@ -195,7 +203,8 @@ export function registerSetupCommand(program) {
|
|
|
195
203
|
const setupCmd = program
|
|
196
204
|
.command('setup')
|
|
197
205
|
.description('First-time setup. Clones a config repo and installs agent CLIs.')
|
|
198
|
-
.option('-f, --force', 'Re-run setup even if ~/.agents-system/ already exists (use with caution)')
|
|
206
|
+
.option('-f, --force', 'Re-run setup even if ~/.agents-system/ already exists (use with caution)')
|
|
207
|
+
.option('--no-system-repo', 'Skip cloning the system repo (you must populate ~/.agents-system/ yourself)');
|
|
199
208
|
setHelpSections(setupCmd, {
|
|
200
209
|
examples: `
|
|
201
210
|
# First-time setup (clones the system repo into ~/.agents-system/)
|