@phnx-labs/agents-cli 1.20.27 → 1.20.28
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 +3 -0
- package/dist/commands/doctor.js +57 -4
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +177 -6
- package/dist/commands/hosts.d.ts +11 -0
- package/dist/commands/hosts.js +229 -0
- package/dist/commands/repo.d.ts +29 -0
- package/dist/commands/repo.js +174 -38
- package/dist/commands/secrets.d.ts +2 -7
- package/dist/commands/secrets.js +15 -23
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +7 -24
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +22 -5
- package/dist/commands/view.js +27 -11
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +1 -0
- package/dist/lib/agents.js +44 -4
- package/dist/lib/browser/drivers/ssh.d.ts +47 -2
- package/dist/lib/browser/drivers/ssh.js +113 -24
- package/dist/lib/browser/profiles.js +28 -1
- package/dist/lib/browser/runtime-state.js +28 -8
- package/dist/lib/browser/types.d.ts +10 -1
- package/dist/lib/cli-resources.js +10 -1
- package/dist/lib/doctor-diff.d.ts +12 -0
- package/dist/lib/doctor-diff.js +89 -2
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +62 -19
- package/dist/lib/hooks.d.ts +17 -0
- package/dist/lib/hooks.js +127 -3
- package/dist/lib/hosts/dispatch.d.ts +26 -0
- package/dist/lib/hosts/dispatch.js +71 -0
- package/dist/lib/hosts/progress.d.ts +21 -0
- package/dist/lib/hosts/progress.js +49 -0
- package/dist/lib/hosts/providers/local.d.ts +17 -0
- package/dist/lib/hosts/providers/local.js +81 -0
- package/dist/lib/hosts/ready.d.ts +37 -0
- package/dist/lib/hosts/ready.js +88 -0
- package/dist/lib/hosts/registry.d.ts +22 -0
- package/dist/lib/hosts/registry.js +65 -0
- package/dist/lib/hosts/ssh-config.d.ts +37 -0
- package/dist/lib/hosts/ssh-config.js +157 -0
- package/dist/lib/hosts/tasks.d.ts +32 -0
- package/dist/lib/hosts/tasks.js +58 -0
- package/dist/lib/hosts/types.d.ts +51 -0
- package/dist/lib/hosts/types.js +21 -0
- package/dist/lib/loop.d.ts +9 -0
- package/dist/lib/loop.js +13 -1
- package/dist/lib/mcp.js +12 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +9 -5
- package/dist/lib/platform/exec.d.ts +10 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/links.d.ts +15 -0
- package/dist/lib/platform/links.js +42 -0
- package/dist/lib/platform/paths.d.ts +18 -0
- package/dist/lib/platform/paths.js +22 -0
- package/dist/lib/platform/posixpath.d.ts +28 -0
- package/dist/lib/platform/posixpath.js +153 -0
- package/dist/lib/plugins.d.ts +10 -0
- package/dist/lib/plugins.js +1 -1
- package/dist/lib/project-launch.js +6 -3
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/self-update.js +7 -2
- package/dist/lib/session/db.d.ts +23 -0
- package/dist/lib/session/db.js +76 -1
- package/dist/lib/session/discover.d.ts +26 -0
- package/dist/lib/session/discover.js +75 -4
- package/dist/lib/session/relative-time.d.ts +7 -0
- package/dist/lib/session/relative-time.js +28 -0
- package/dist/lib/session/remote.d.ts +31 -3
- package/dist/lib/session/remote.js +121 -14
- package/dist/lib/ssh-exec.d.ts +45 -0
- package/dist/lib/ssh-exec.js +61 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/types.d.ts +21 -0
- package/dist/lib/versions.d.ts +6 -2
- package/dist/lib/versions.js +8 -4
- package/package.json +1 -1
- package/scripts/postinstall.js +62 -0
package/dist/commands/repo.js
CHANGED
|
@@ -68,26 +68,117 @@ function deriveAlias(source) {
|
|
|
68
68
|
// becomes ~/.agents-<alias>/ on disk (e.g. ".agents-work" -> "work").
|
|
69
69
|
return base.replace(/^\.+/, '').replace(/^agents-/, '') || 'repo';
|
|
70
70
|
}
|
|
71
|
+
/** Top-level dir -> resource kind. Directory-based resources collapse to one unit. */
|
|
72
|
+
const RESOURCE_DIRS = {
|
|
73
|
+
skills: 'skill', commands: 'command', prompts: 'command', plugins: 'plugin',
|
|
74
|
+
hooks: 'hook', mcp: 'mcp', subagents: 'subagent', rules: 'rule',
|
|
75
|
+
workflows: 'workflow', routines: 'routine', profiles: 'profile',
|
|
76
|
+
permissions: 'permission', cli: 'cli',
|
|
77
|
+
};
|
|
78
|
+
/** [singular, plural] display labels per kind. */
|
|
79
|
+
const RESOURCE_LABELS = {
|
|
80
|
+
skill: ['skill', 'skills'], command: ['command', 'commands'],
|
|
81
|
+
plugin: ['plugin', 'plugins'], hook: ['hook', 'hooks'], mcp: ['MCP', 'MCPs'],
|
|
82
|
+
subagent: ['subagent', 'subagents'], rule: ['rule', 'rules'],
|
|
83
|
+
workflow: ['workflow', 'workflows'], routine: ['routine', 'routines'],
|
|
84
|
+
profile: ['profile', 'profiles'], permission: ['permission', 'permissions'],
|
|
85
|
+
cli: ['CLI', 'CLIs'], config: ['config file', 'config files'],
|
|
86
|
+
other: ['other file', 'other files'],
|
|
87
|
+
};
|
|
88
|
+
/** Display order — the resources a user cares about most come first. */
|
|
89
|
+
const RESOURCE_ORDER = [
|
|
90
|
+
'skill', 'command', 'plugin', 'hook', 'mcp', 'subagent', 'rule',
|
|
91
|
+
'workflow', 'routine', 'profile', 'permission', 'cli', 'config', 'other',
|
|
92
|
+
];
|
|
71
93
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
94
|
+
* Map a repo-relative path to the resource unit it belongs to. Directory-based
|
|
95
|
+
* resources (skills/foo/SKILL.md) collapse to `skills/foo` so all their files
|
|
96
|
+
* count as one unit; flat config files (agents.yaml, hooks.yaml) count alone.
|
|
75
97
|
*/
|
|
76
|
-
function
|
|
98
|
+
export function resourceUnit(file) {
|
|
99
|
+
const parts = file.split('/');
|
|
100
|
+
const top = parts[0];
|
|
101
|
+
if (top === 'agents.yaml' || top === 'hooks.yaml')
|
|
102
|
+
return { kind: 'config', unit: top };
|
|
103
|
+
const kind = RESOURCE_DIRS[top];
|
|
104
|
+
if (kind)
|
|
105
|
+
return { kind, unit: parts.length > 1 ? `${top}/${parts[1]}` : file };
|
|
106
|
+
return { kind: 'other', unit: file };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Render a set of changed files as a resource-level summary, e.g.
|
|
110
|
+
* `2 new skills, 1 changed hook`. Counts distinct resource units (a skill whose
|
|
111
|
+
* three files all changed is "1 changed skill"), grouped by action then kind,
|
|
112
|
+
* and colors each phrase green/yellow/red for new/changed/removed. Caps at
|
|
113
|
+
* `maxParts` phrases, appending `+N more` so a big diff stays scannable.
|
|
114
|
+
*/
|
|
115
|
+
export function formatResourceDelta(entries, maxParts = 5) {
|
|
116
|
+
// Gather every action seen across a unit's files, then collapse to one action.
|
|
117
|
+
const units = new Map();
|
|
118
|
+
for (const { action, file } of entries) {
|
|
119
|
+
const { kind, unit } = resourceUnit(file);
|
|
120
|
+
const key = `${kind} ${unit}`;
|
|
121
|
+
const cur = units.get(key) ?? { kind, actions: new Set() };
|
|
122
|
+
cur.actions.add(action);
|
|
123
|
+
units.set(key, cur);
|
|
124
|
+
}
|
|
125
|
+
const counts = new Map(); // `${action} ${kind}` -> count
|
|
126
|
+
for (const { kind, actions } of units.values()) {
|
|
127
|
+
let action;
|
|
128
|
+
if (actions.size === 1)
|
|
129
|
+
action = [...actions][0];
|
|
130
|
+
else if ([...actions].every((a) => a === 'new'))
|
|
131
|
+
action = 'new';
|
|
132
|
+
else if ([...actions].every((a) => a === 'removed'))
|
|
133
|
+
action = 'removed';
|
|
134
|
+
else
|
|
135
|
+
action = 'changed'; // mixed add+modify within one unit reads as a change
|
|
136
|
+
const key = `${action} ${kind}`;
|
|
137
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
138
|
+
}
|
|
139
|
+
const COLOR = {
|
|
140
|
+
new: chalk.green, changed: chalk.yellow, removed: chalk.red,
|
|
141
|
+
};
|
|
77
142
|
const parts = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
143
|
+
for (const action of ['new', 'changed', 'removed']) {
|
|
144
|
+
for (const kind of RESOURCE_ORDER) {
|
|
145
|
+
const n = counts.get(`${action} ${kind}`);
|
|
146
|
+
if (!n)
|
|
147
|
+
continue;
|
|
148
|
+
const [singular, plural] = RESOURCE_LABELS[kind];
|
|
149
|
+
parts.push(COLOR[action](`${n} ${action} ${n === 1 ? singular : plural}`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (parts.length > maxParts) {
|
|
153
|
+
const shown = parts.slice(0, maxParts);
|
|
154
|
+
shown.push(chalk.gray(`+${parts.length - maxParts} more`));
|
|
155
|
+
return shown.join(', ');
|
|
156
|
+
}
|
|
157
|
+
return parts.join(', ');
|
|
158
|
+
}
|
|
159
|
+
/** Parse `git diff --name-status <range>` into resource-delta entries. */
|
|
160
|
+
async function diffResourceEntries(git, range) {
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = await git.diff(['--name-status', range]);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return []; // no upstream resolvable / range invalid — caller falls back to counts
|
|
167
|
+
}
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const line of raw.split('\n')) {
|
|
170
|
+
if (!line.trim())
|
|
171
|
+
continue;
|
|
172
|
+
const cols = line.split('\t');
|
|
173
|
+
const code = cols[0] ?? '';
|
|
174
|
+
const file = cols[cols.length - 1] ?? ''; // renames put the new path last
|
|
175
|
+
if (!file)
|
|
176
|
+
continue;
|
|
177
|
+
const c = code[0];
|
|
178
|
+
const action = c === 'A' ? 'new' : c === 'D' ? 'removed' : 'changed';
|
|
179
|
+
out.push({ action, file });
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
91
182
|
}
|
|
92
183
|
/** Visible character width of a string with embedded ANSI color codes. */
|
|
93
184
|
function visibleWidth(s) {
|
|
@@ -98,46 +189,75 @@ function padVisible(s, width) {
|
|
|
98
189
|
return s + ' '.repeat(Math.max(0, width - visibleWidth(s)));
|
|
99
190
|
}
|
|
100
191
|
/**
|
|
101
|
-
* Render one row
|
|
102
|
-
*
|
|
103
|
-
* `agents repo status` alias.
|
|
192
|
+
* Render one repo's row data: branch, resource-level sync (what a pull/push would
|
|
193
|
+
* move), resource-level local edits, and the remote URL + commit. Used by
|
|
194
|
+
* `agents repo list` and the hidden `agents repo status` alias.
|
|
104
195
|
*/
|
|
105
196
|
async function renderRepoRow(t) {
|
|
106
|
-
const aliasCol = chalk.cyan(t.alias.padEnd(12));
|
|
107
197
|
if (!fs.existsSync(t.dir)) {
|
|
108
|
-
return
|
|
198
|
+
return { alias: t.alias, raw: `${chalk.red('missing')} ${chalk.gray(t.dir)}` };
|
|
109
199
|
}
|
|
110
200
|
if (!isGitRepo(t.dir)) {
|
|
111
|
-
return
|
|
201
|
+
return { alias: t.alias, raw: `${chalk.gray('local (no git remote)')} ${chalk.gray(t.dir)}` };
|
|
112
202
|
}
|
|
113
203
|
try {
|
|
114
204
|
const git = simpleGit(t.dir);
|
|
115
205
|
const status = await git.status();
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
206
|
+
// Show the local branch name; the upstream remote is already implied by URL.
|
|
207
|
+
const branch = status.current || (status.tracking ? status.tracking.replace(/^origin\//, '') : '(detached)');
|
|
208
|
+
// SYNC: what a pull brings in / a push sends out, described by resource.
|
|
209
|
+
const ahead = status.ahead ?? 0;
|
|
210
|
+
const behind = status.behind ?? 0;
|
|
211
|
+
let sync;
|
|
212
|
+
if (!status.tracking) {
|
|
213
|
+
sync = chalk.gray('no upstream');
|
|
214
|
+
}
|
|
215
|
+
else if (ahead === 0 && behind === 0) {
|
|
216
|
+
sync = chalk.green('up to date');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const pieces = [];
|
|
220
|
+
if (behind > 0) {
|
|
221
|
+
// Three-dot isolates upstream's side via the merge-base, so a diverged
|
|
222
|
+
// branch reports exactly what a pull adds (not the inverse of local commits).
|
|
223
|
+
const incoming = formatResourceDelta(await diffResourceEntries(git, 'HEAD...@{upstream}'));
|
|
224
|
+
pieces.push(`${incoming || chalk.yellow(`${behind} commit${behind > 1 ? 's' : ''}`)} ${chalk.gray('to pull')}`);
|
|
225
|
+
}
|
|
226
|
+
if (ahead > 0) {
|
|
227
|
+
const outgoing = formatResourceDelta(await diffResourceEntries(git, '@{upstream}...HEAD'));
|
|
228
|
+
pieces.push(`${outgoing || chalk.yellow(`${ahead} commit${ahead > 1 ? 's' : ''}`)} ${chalk.gray('to push')}`);
|
|
229
|
+
}
|
|
230
|
+
sync = pieces.join(chalk.gray(' · '));
|
|
231
|
+
}
|
|
232
|
+
// CHANGES: uncommitted working-tree edits, described by resource.
|
|
233
|
+
const localEntries = [
|
|
234
|
+
...status.created.map((f) => ({ action: 'new', file: f })),
|
|
235
|
+
...status.not_added.map((f) => ({ action: 'new', file: f })),
|
|
236
|
+
...status.modified.map((f) => ({ action: 'changed', file: f })),
|
|
237
|
+
...status.conflicted.map((f) => ({ action: 'changed', file: f })),
|
|
238
|
+
...status.renamed.map((r) => ({ action: 'changed', file: r.to })),
|
|
239
|
+
...status.deleted.map((f) => ({ action: 'removed', file: f })),
|
|
240
|
+
];
|
|
241
|
+
const changes = status.isClean() ? chalk.green('clean') : formatResourceDelta(localEntries);
|
|
122
242
|
const remotes = await git.getRemotes(true);
|
|
123
243
|
const origin = remotes.find((r) => r.name === 'origin');
|
|
124
244
|
const url = origin?.refs?.fetch || '';
|
|
125
245
|
const commit = (await git.log({ maxCount: 1 })).latest?.hash.slice(0, 8) || '';
|
|
126
|
-
const
|
|
246
|
+
const remote = url
|
|
127
247
|
? chalk.gray(`${url}${commit ? ` (${commit})` : ''}`)
|
|
128
248
|
: commit
|
|
129
249
|
? chalk.gray(`(${commit})`)
|
|
130
250
|
: '';
|
|
131
|
-
return
|
|
251
|
+
return { alias: t.alias, cells: [branch, sync, changes, remote] };
|
|
132
252
|
}
|
|
133
253
|
catch (err) {
|
|
134
|
-
return
|
|
254
|
+
return { alias: t.alias, raw: `${chalk.red('error')} ${err.message}` };
|
|
135
255
|
}
|
|
136
256
|
}
|
|
137
257
|
/**
|
|
138
258
|
* Shared action body for `agents repo list` and the hidden `agents repo status`
|
|
139
|
-
* alias. Prints
|
|
140
|
-
* remote URL
|
|
259
|
+
* alias. Prints an aligned table: repo, branch, resource-level sync (to pull /
|
|
260
|
+
* to push), resource-level local changes, and remote URL + short commit.
|
|
141
261
|
*/
|
|
142
262
|
async function listRepos(alias) {
|
|
143
263
|
const targets = collectRepoTargets(alias);
|
|
@@ -149,10 +269,26 @@ async function listRepos(alias) {
|
|
|
149
269
|
console.log(chalk.gray('No repos to show.'));
|
|
150
270
|
return;
|
|
151
271
|
}
|
|
272
|
+
const rows = await Promise.all(targets.map(renderRepoRow));
|
|
273
|
+
const tableRows = rows.filter((r) => r.cells);
|
|
274
|
+
// Column widths grow to fit the widest visible content (resource summaries vary
|
|
275
|
+
// a lot in length), so the table stays aligned without truncating detail.
|
|
276
|
+
const headers = ['REPO', 'BRANCH', 'SYNC', 'CHANGES'];
|
|
277
|
+
const aliasW = Math.max(headers[0].length, ...rows.map((r) => r.alias.length));
|
|
278
|
+
const branchW = Math.max(headers[1].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[0])));
|
|
279
|
+
const syncW = Math.max(headers[2].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[1])));
|
|
280
|
+
const changesW = Math.max(headers[3].length, 0, ...tableRows.map((r) => visibleWidth(r.cells[2])));
|
|
152
281
|
console.log('');
|
|
153
|
-
console.log(` ${chalk.gray(
|
|
154
|
-
for (const
|
|
155
|
-
|
|
282
|
+
console.log(` ${chalk.gray(headers[0].padEnd(aliasW))} ${chalk.gray(headers[1].padEnd(branchW))} ${chalk.gray(headers[2].padEnd(syncW))} ${chalk.gray(headers[3].padEnd(changesW))} ${chalk.gray('REMOTE')}`);
|
|
283
|
+
for (const r of rows) {
|
|
284
|
+
const aliasCol = chalk.cyan(r.alias.padEnd(aliasW));
|
|
285
|
+
if (r.cells) {
|
|
286
|
+
const [branch, sync, changes, remote] = r.cells;
|
|
287
|
+
console.log(` ${aliasCol} ${padVisible(branch, branchW)} ${padVisible(sync, syncW)} ${padVisible(changes, changesW)} ${remote}`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
console.log(` ${aliasCol} ${r.raw}`);
|
|
291
|
+
}
|
|
156
292
|
}
|
|
157
293
|
const userDir = getUserAgentsDir();
|
|
158
294
|
if (!isGitRepo(userDir) && fs.existsSync(userDir)) {
|
|
@@ -358,7 +494,7 @@ export function registerRepoCommands(program) {
|
|
|
358
494
|
repoCmd
|
|
359
495
|
.command('list [alias]')
|
|
360
496
|
.alias('ls')
|
|
361
|
-
.description('Show all repos
|
|
497
|
+
.description('Show all repos with resource-level sync (skills/commands/plugins to pull or push) and local changes.')
|
|
362
498
|
.action(async (alias) => {
|
|
363
499
|
await listRepos(alias);
|
|
364
500
|
});
|
|
@@ -6,13 +6,8 @@
|
|
|
6
6
|
* Keychain. Bundles are injected at run time via `agents run --secrets`.
|
|
7
7
|
*/
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* or `user@host`. The strict allowlist blocks shell metacharacters and a leading `-`
|
|
12
|
-
* so a target can't be smuggled in as an ssh argv flag.
|
|
13
|
-
*/
|
|
14
|
-
export declare const SSH_TARGET_RE: RegExp;
|
|
15
|
-
export declare function assertValidSshTarget(host: string): void;
|
|
9
|
+
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
10
|
+
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
16
11
|
/**
|
|
17
12
|
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
18
13
|
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
package/dist/commands/secrets.js
CHANGED
|
@@ -9,6 +9,7 @@ import { Option } from 'commander';
|
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { spawnSync } from 'child_process';
|
|
12
|
+
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
12
13
|
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
13
14
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
14
15
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
@@ -134,17 +135,9 @@ function readStdinSync() {
|
|
|
134
135
|
}
|
|
135
136
|
return Buffer.concat(chunks).toString('utf-8').trim();
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* so a target can't be smuggled in as an ssh argv flag.
|
|
141
|
-
*/
|
|
142
|
-
export const SSH_TARGET_RE = /^[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
143
|
-
export function assertValidSshTarget(host) {
|
|
144
|
-
if (!SSH_TARGET_RE.test(host)) {
|
|
145
|
-
throw new Error(`Invalid SSH target ${JSON.stringify(host)}. Expected a host alias or user@host (letters, digits, '.', '_', '-').`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
138
|
+
// SSH target validation is defined canonically in src/lib/ssh-exec.ts and
|
|
139
|
+
// re-exported here for back-compat with existing importers of these symbols.
|
|
140
|
+
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
148
141
|
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
149
142
|
function shellQuote(s) {
|
|
150
143
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -414,7 +407,7 @@ export function registerSecretsCommands(program) {
|
|
|
414
407
|
eval "$(agents secrets export prod --plaintext)"
|
|
415
408
|
|
|
416
409
|
# Push the bundle to remote machine(s) over SSH (lands as a native bundle there)
|
|
417
|
-
agents secrets export prod --
|
|
410
|
+
agents secrets export prod --host yosemite-s0 --host yosemite-s1 --force
|
|
418
411
|
|
|
419
412
|
# Run a one-off command with secrets injected
|
|
420
413
|
agents secrets exec prod -- ./deploy.sh
|
|
@@ -1017,7 +1010,7 @@ Examples:
|
|
|
1017
1010
|
const requestedBackend = parseBackendOpt(opts.backend);
|
|
1018
1011
|
// Read the bundle if it exists (inheriting its backend); otherwise
|
|
1019
1012
|
// create it with the requested backend so a single `import --backend
|
|
1020
|
-
// file` works (this is what `export --
|
|
1013
|
+
// file` works (this is what `export --host ... --remote-backend file`
|
|
1021
1014
|
// drives on the remote).
|
|
1022
1015
|
let bundle;
|
|
1023
1016
|
if (bundleExists(resolvedBundleName)) {
|
|
@@ -1094,23 +1087,22 @@ Examples:
|
|
|
1094
1087
|
});
|
|
1095
1088
|
cmd
|
|
1096
1089
|
.command('export [bundle]')
|
|
1097
|
-
.description('Resolve a bundle and print KEY=VALUE lines, push it to a 1Password vault with --to-1password, or push it to remote machine(s) over SSH with --
|
|
1090
|
+
.description('Resolve a bundle and print KEY=VALUE lines, push it to a 1Password vault with --to-1password, or push it to remote machine(s) over SSH with --host.')
|
|
1098
1091
|
.option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear (shell export mode)')
|
|
1099
1092
|
.option('--to-1password', 'Push every key in the bundle as a PASSWORD item in a 1Password vault')
|
|
1100
1093
|
.option('--vault <name>', '1Password vault name (used with --to-1password)')
|
|
1101
|
-
.option('--
|
|
1102
|
-
.option('--
|
|
1103
|
-
.option('--
|
|
1104
|
-
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --to-ssh)')
|
|
1094
|
+
.option('--host <target...>', 'Push the bundle over SSH to this target (host alias or user@host); repeatable for multiple machines')
|
|
1095
|
+
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file (passphrase-encrypted, headless-readable). file forwards AGENTS_SECRETS_PASSPHRASE over stdin.', 'keychain')
|
|
1096
|
+
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --host)')
|
|
1105
1097
|
.action(async (bundleName, opts) => {
|
|
1106
1098
|
try {
|
|
1107
1099
|
const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
1108
1100
|
const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1101
|
+
// The presence of --host selects SSH push: --host is the destination
|
|
1102
|
+
// and carries the mode (no separate --to-ssh needed — it would be
|
|
1103
|
+
// strictly redundant since SSH always requires at least one host).
|
|
1104
|
+
const hosts = opts.host ?? [];
|
|
1105
|
+
if (hosts.length > 0) {
|
|
1114
1106
|
for (const h of hosts)
|
|
1115
1107
|
assertValidSshTarget(h);
|
|
1116
1108
|
const remoteBackend = parseBackendOpt(opts.remoteBackend);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { SessionMeta } from '../lib/session/types.js';
|
|
3
3
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
4
|
+
import { type PickedSession } from './sessions-picker.js';
|
|
4
5
|
/** Grouped + sorted view of active sessions for the --active renderer. */
|
|
5
6
|
export interface ActiveSessionsLayout {
|
|
6
7
|
workspaces: Array<{
|
|
@@ -28,6 +29,7 @@ export interface ActiveSessionsLayout {
|
|
|
28
29
|
* - sessions within a window/flat bucket: input order preserved
|
|
29
30
|
*/
|
|
30
31
|
export declare function groupActiveSessions(sessions: ActiveSession[]): ActiveSessionsLayout;
|
|
32
|
+
export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number): Promise<PickedSession | null>;
|
|
31
33
|
/**
|
|
32
34
|
* Build the shell command that resumes a picked session.
|
|
33
35
|
*
|
|
@@ -21,6 +21,7 @@ import { discoverSessions, countSessionsInScope, resolveSessionById, searchConte
|
|
|
21
21
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
22
22
|
import { parseSession } from '../lib/session/parse.js';
|
|
23
23
|
import { runRemoteSessions } from '../lib/session/remote.js';
|
|
24
|
+
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
24
25
|
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
|
|
25
26
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
26
27
|
import { colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
@@ -656,7 +657,7 @@ function formatPickerLabel(s, query) {
|
|
|
656
657
|
renderTopicCell(label, topic, query, 48, 50) +
|
|
657
658
|
chalk.gray(when));
|
|
658
659
|
}
|
|
659
|
-
async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
|
|
660
|
+
export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
|
|
660
661
|
if (hiddenCount > 0) {
|
|
661
662
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
662
663
|
}
|
|
@@ -1332,7 +1333,11 @@ function findClaudeResumeTimestamp(filePath, targetTimestampMs) {
|
|
|
1332
1333
|
}
|
|
1333
1334
|
}
|
|
1334
1335
|
function isWithinProject(sessionCwd, projectRoot) {
|
|
1335
|
-
|
|
1336
|
+
// Compare separator- and case-normalized (Windows folds `\`→`/` and lowercases)
|
|
1337
|
+
// so a backslash session cwd matches a forward-slash project root and vice versa.
|
|
1338
|
+
const cwd = toComparablePath(sessionCwd);
|
|
1339
|
+
const root = toComparablePath(projectRoot);
|
|
1340
|
+
return cwd === root || cwd.startsWith(root + '/');
|
|
1336
1341
|
}
|
|
1337
1342
|
function sessionDistance(session, historyEntry) {
|
|
1338
1343
|
if (!historyEntry.timestampMs)
|
|
@@ -1360,25 +1365,3 @@ function padRight(s, width) {
|
|
|
1360
1365
|
function truncate(s, max) {
|
|
1361
1366
|
return s.length > max ? s.slice(0, max - 1) + '.' : s;
|
|
1362
1367
|
}
|
|
1363
|
-
function formatRelativeTime(isoTimestamp) {
|
|
1364
|
-
const now = Date.now();
|
|
1365
|
-
const then = new Date(isoTimestamp).getTime();
|
|
1366
|
-
if (isNaN(then))
|
|
1367
|
-
return isoTimestamp;
|
|
1368
|
-
const diffMs = now - then;
|
|
1369
|
-
const diffMin = Math.floor(diffMs / 60_000);
|
|
1370
|
-
const diffHrs = Math.floor(diffMs / 3_600_000);
|
|
1371
|
-
const diffDays = Math.floor(diffMs / 86_400_000);
|
|
1372
|
-
if (diffMin < 1)
|
|
1373
|
-
return 'just now';
|
|
1374
|
-
if (diffMin < 60)
|
|
1375
|
-
return `${diffMin} min ago`;
|
|
1376
|
-
if (diffHrs < 24)
|
|
1377
|
-
return `${diffHrs} hour${diffHrs === 1 ? '' : 's'} ago`;
|
|
1378
|
-
if (diffDays < 7)
|
|
1379
|
-
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
|
|
1380
|
-
// Older: show date
|
|
1381
|
-
const d = new Date(then);
|
|
1382
|
-
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
1383
|
-
return `${months[d.getMonth()]} ${d.getDate()}`;
|
|
1384
|
-
}
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* agents sync claude # one agent: uses default/sole installed version
|
|
10
10
|
* agents sync claude@2.1.142 # one agent: explicit version
|
|
11
11
|
* agents sync claude@latest # one agent: newest installed
|
|
12
|
+
* agents sync claude@oldest # one agent: oldest installed
|
|
13
|
+
* agents sync claude@pinned (= claude@default) # one agent: the pinned default version
|
|
12
14
|
* agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
|
|
13
15
|
*
|
|
14
16
|
* The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
|
package/dist/commands/sync.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* agents sync claude # one agent: uses default/sole installed version
|
|
10
10
|
* agents sync claude@2.1.142 # one agent: explicit version
|
|
11
11
|
* agents sync claude@latest # one agent: newest installed
|
|
12
|
+
* agents sync claude@oldest # one agent: oldest installed
|
|
13
|
+
* agents sync claude@pinned (= claude@default) # one agent: the pinned default version
|
|
12
14
|
* agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
|
|
13
15
|
*
|
|
14
16
|
* The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
import * as path from 'path';
|
|
29
31
|
import chalk from 'chalk';
|
|
30
32
|
import { agentLabel, resolveAgentName } from '../lib/agents.js';
|
|
31
|
-
import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, } from '../lib/versions.js';
|
|
33
|
+
import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, } from '../lib/versions.js';
|
|
32
34
|
import { compileRulesForProject } from '../lib/rules/compile.js';
|
|
33
35
|
import { runLaunchSync } from '../lib/project-launch.js';
|
|
34
36
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -38,7 +40,7 @@ export function registerSyncCommand(program) {
|
|
|
38
40
|
program
|
|
39
41
|
.command('sync [agentSpec]')
|
|
40
42
|
.summary('Make this machine current, or sync resources into one agent')
|
|
41
|
-
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude"
|
|
43
|
+
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", or a selector: @latest / @oldest / @pinned (= @default).\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
|
|
42
44
|
.option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
|
|
43
45
|
.option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version")')
|
|
44
46
|
.option('--project-dir <path>', 'Path to project-level .agents/ directory containing project-scoped resources')
|
|
@@ -116,17 +118,23 @@ async function runSync(agentSpec, opts) {
|
|
|
116
118
|
// ---------- 1. Resolve agent + version ----------
|
|
117
119
|
let agentId;
|
|
118
120
|
let version;
|
|
121
|
+
// A positional @selector typed by the user (latest/oldest/pinned/default/
|
|
122
|
+
// explicit). parseAgentSpec defaults a missing version to 'latest', so a bare
|
|
123
|
+
// `agents sync claude` and `agents sync claude@latest` are indistinguishable
|
|
124
|
+
// after parsing — we only treat the version as a selector when an '@' was
|
|
125
|
+
// actually typed, keeping bare `claude` on the default-version path.
|
|
126
|
+
let selector;
|
|
119
127
|
if (agentSpec) {
|
|
120
128
|
const parsed = parseAgentSpec(agentSpec);
|
|
121
129
|
if (!parsed) {
|
|
122
130
|
errLog(chalk.red(`Invalid agent spec '${agentSpec}'.`));
|
|
123
|
-
errLog(chalk.gray('Examples: claude, claude@2.1.142,
|
|
131
|
+
errLog(chalk.gray('Examples: claude, claude@2.1.142, claude@latest, claude@oldest, claude@pinned'));
|
|
124
132
|
process.exitCode = 1;
|
|
125
133
|
return;
|
|
126
134
|
}
|
|
127
135
|
agentId = parsed.agent;
|
|
128
|
-
if (
|
|
129
|
-
|
|
136
|
+
if (agentSpec.includes('@'))
|
|
137
|
+
selector = parsed.version;
|
|
130
138
|
}
|
|
131
139
|
if (opts.agent) {
|
|
132
140
|
const resolved = resolveAgentName(opts.agent);
|
|
@@ -138,6 +146,8 @@ async function runSync(agentSpec, opts) {
|
|
|
138
146
|
agentId = resolved;
|
|
139
147
|
}
|
|
140
148
|
if (opts.agentVersion) {
|
|
149
|
+
// Legacy flag and the launch-shim hot path (`--agent-version <concrete>`):
|
|
150
|
+
// pass through verbatim. Selector aliases are a positional-spec feature.
|
|
141
151
|
version = opts.agentVersion;
|
|
142
152
|
}
|
|
143
153
|
if (!agentId) {
|
|
@@ -147,6 +157,13 @@ async function runSync(agentSpec, opts) {
|
|
|
147
157
|
return;
|
|
148
158
|
}
|
|
149
159
|
// ---------- 2. Resolve version (project pin → global default → sole installed) ----------
|
|
160
|
+
// A positional @selector wins over the default-resolution below.
|
|
161
|
+
// @latest / @oldest → newest / oldest installed (process.exit if none)
|
|
162
|
+
// @pinned / @default → undefined → fall through to the default path
|
|
163
|
+
// @x.y.z → that version (process.exit if not installed)
|
|
164
|
+
if (selector !== undefined && !version) {
|
|
165
|
+
version = resolveVersionAlias(agentId, selector);
|
|
166
|
+
}
|
|
150
167
|
if (!version) {
|
|
151
168
|
version = resolveVersion(agentId, process.cwd()) || undefined;
|
|
152
169
|
if (!version) {
|
package/dist/commands/view.js
CHANGED
|
@@ -21,6 +21,9 @@ import { listProfiles, profileSummary } from '../lib/profiles.js';
|
|
|
21
21
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
22
22
|
import { confirm } from '@inquirer/prompts';
|
|
23
23
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
24
|
+
// Shown in the email column for agents that are signed in but expose no email
|
|
25
|
+
// address locally (Antigravity, Kimi store an opaque OAuth/JWT credential).
|
|
26
|
+
const SIGNED_IN_LABEL = 'signed in';
|
|
24
27
|
/**
|
|
25
28
|
* Group profile summaries by their host harness, optionally filtered to a
|
|
26
29
|
* single agent. Profile YAMLs that fail validation are silently skipped by
|
|
@@ -350,6 +353,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
350
353
|
const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
351
354
|
if (info?.email)
|
|
352
355
|
maxEmail = Math.max(maxEmail, info.email.length);
|
|
356
|
+
else if (info?.signedIn)
|
|
357
|
+
maxEmail = Math.max(maxEmail, SIGNED_IN_LABEL.length);
|
|
353
358
|
if (info?.plan)
|
|
354
359
|
maxPlanWidth = Math.max(maxPlanWidth, info.plan.length);
|
|
355
360
|
}
|
|
@@ -406,6 +411,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
406
411
|
// Build columns, trimming trailing whitespace when columns are empty
|
|
407
412
|
const parts = [` ${label}`];
|
|
408
413
|
const hasEmail = !!vInfo?.email;
|
|
414
|
+
const signedIn = !!vInfo?.signedIn;
|
|
409
415
|
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth);
|
|
410
416
|
const hasUsage = usageStr.length > 0;
|
|
411
417
|
// Only show lastActive for versions with an actual logged-in account.
|
|
@@ -418,14 +424,17 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
418
424
|
runDefaultBits.push(`mode:${runDefaults.mode}`);
|
|
419
425
|
if (runDefaults.model)
|
|
420
426
|
runDefaultBits.push(`model:${runDefaults.model}`);
|
|
421
|
-
if (!hasEmail && !hasUsage) {
|
|
427
|
+
if (!hasEmail && !hasUsage && !signedIn) {
|
|
422
428
|
// Installed but never signed in
|
|
423
429
|
parts.push(chalk.gray('(not signed in — run ' + agent.cliCommand + ' to log in)'));
|
|
424
430
|
}
|
|
425
431
|
else {
|
|
426
|
-
if (hasEmail || hasUsage || hasActive) {
|
|
427
|
-
|
|
428
|
-
|
|
432
|
+
if (hasEmail || hasUsage || hasActive || signedIn) {
|
|
433
|
+
// Signed-in agents without a local email (Antigravity, Kimi) show a
|
|
434
|
+
// "signed in" placeholder so they read as logged in, not blank.
|
|
435
|
+
const display = vInfo?.email || (signedIn ? SIGNED_IN_LABEL : '');
|
|
436
|
+
const emailCol = display.padEnd(maxEmail);
|
|
437
|
+
parts.push(display ? chalk.cyan(emailCol) : ' '.repeat(maxEmail));
|
|
429
438
|
}
|
|
430
439
|
if (hasUsage || hasActive) {
|
|
431
440
|
const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageStr)));
|
|
@@ -503,8 +512,10 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
503
512
|
const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
|
|
504
513
|
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null);
|
|
505
514
|
const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
|
|
506
|
-
if (gInfo?.email || gUsageStr || gActiveStr)
|
|
507
|
-
|
|
515
|
+
if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
|
|
516
|
+
const gDisplay = gInfo?.email || (gInfo?.signedIn ? SIGNED_IN_LABEL : '');
|
|
517
|
+
parts.push(gDisplay ? chalk.cyan(gDisplay) : '');
|
|
518
|
+
}
|
|
508
519
|
if (gUsageStr || gActiveStr)
|
|
509
520
|
parts.push(gUsageStr);
|
|
510
521
|
const gStatusStr = formatUsageStatusBadge(gInfo?.usageStatus);
|
|
@@ -814,7 +825,11 @@ async function showAgentResources(agentId, requestedVersion, filter) {
|
|
|
814
825
|
cliVersion: version,
|
|
815
826
|
info: accountInfo,
|
|
816
827
|
});
|
|
817
|
-
const emailStr = accountInfo.email
|
|
828
|
+
const emailStr = accountInfo.email
|
|
829
|
+
? chalk.cyan(` ${accountInfo.email}`)
|
|
830
|
+
: accountInfo.signedIn
|
|
831
|
+
? chalk.cyan(` ${SIGNED_IN_LABEL}`)
|
|
832
|
+
: '';
|
|
818
833
|
const status = chalk.green(version);
|
|
819
834
|
const usageStr = formatUsageSummary(accountInfo.plan, null);
|
|
820
835
|
const usagePart = usageStr ? ` ${usageStr}` : '';
|
|
@@ -1006,7 +1021,7 @@ async function collectAgentsJson(filterAgentId) {
|
|
|
1006
1021
|
const entry = {
|
|
1007
1022
|
version,
|
|
1008
1023
|
isDefault: version === globalDefault,
|
|
1009
|
-
signedIn:
|
|
1024
|
+
signedIn: info.signedIn,
|
|
1010
1025
|
email: info.email,
|
|
1011
1026
|
plan: info.plan,
|
|
1012
1027
|
usageStatus: info.usageStatus,
|
|
@@ -1273,9 +1288,10 @@ export async function viewAction(agentArg, options) {
|
|
|
1273
1288
|
console.log(chalk.red(formatAgentError(agentName)));
|
|
1274
1289
|
process.exit(1);
|
|
1275
1290
|
}
|
|
1276
|
-
// Keep 'default' as-is since showAgentResources handles
|
|
1277
|
-
// returns undefined for '
|
|
1278
|
-
|
|
1291
|
+
// Keep 'default'/'pinned' as-is since showAgentResources handles 'default';
|
|
1292
|
+
// resolveVersionAlias returns undefined for both (they're synonyms), which
|
|
1293
|
+
// would otherwise skip the detailed view.
|
|
1294
|
+
const requestedVersion = (parts[1] === 'default' || parts[1] === 'pinned')
|
|
1279
1295
|
? 'default'
|
|
1280
1296
|
: (resolveVersionAlias(agentId, parts[1]) ?? null);
|
|
1281
1297
|
if (prune) {
|