@phnx-labs/agents-cli 1.20.62 → 1.20.64
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 +62 -0
- package/README.md +19 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +13 -3
- package/dist/commands/exec.js +96 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- package/dist/commands/funnel.d.ts +5 -0
- package/dist/commands/funnel.js +62 -0
- package/dist/commands/hosts.js +42 -0
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/repo.d.ts +4 -4
- package/dist/commands/repo.js +30 -19
- package/dist/commands/routines.js +92 -29
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +279 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions-sync.d.ts +1 -0
- package/dist/commands/sessions-sync.js +16 -2
- package/dist/commands/sessions.js +12 -1
- package/dist/commands/setup.js +9 -0
- package/dist/commands/ssh.js +170 -5
- package/dist/commands/sync-provision.d.ts +23 -0
- package/dist/commands/sync-provision.js +107 -0
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/webhook.d.ts +9 -0
- package/dist/commands/webhook.js +93 -0
- package/dist/index.js +7 -2
- package/dist/lib/agents.d.ts +44 -0
- package/dist/lib/agents.js +85 -35
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/browser/ipc.js +5 -4
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +17 -0
- package/dist/lib/browser/service.d.ts +12 -1
- package/dist/lib/browser/service.js +48 -13
- package/dist/lib/browser/sessions-list.d.ts +40 -0
- package/dist/lib/browser/sessions-list.js +190 -0
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +158 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +24 -0
- package/dist/lib/daemon.js +8 -1
- package/dist/lib/devices/fleet.d.ts +62 -0
- package/dist/lib/devices/fleet.js +128 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/funnel.d.ts +5 -0
- package/dist/lib/funnel.js +23 -0
- package/dist/lib/git.d.ts +21 -5
- package/dist/lib/git.js +64 -14
- package/dist/lib/hosts/credentials.d.ts +28 -0
- package/dist/lib/hosts/credentials.js +48 -0
- package/dist/lib/hosts/dispatch.d.ts +25 -0
- package/dist/lib/hosts/dispatch.js +68 -2
- package/dist/lib/hosts/passthrough.d.ts +13 -10
- package/dist/lib/hosts/passthrough.js +119 -29
- package/dist/lib/mailbox-gc.js +4 -16
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/migrate.d.ts +12 -0
- package/dist/lib/migrate.js +55 -1
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +50 -12
- package/dist/lib/routines.js +82 -27
- package/dist/lib/runner.js +255 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/session/bundle.d.ts +150 -0
- package/dist/lib/session/bundle.js +189 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/sync/agents.d.ts +56 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +6 -1
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/provision.d.ts +49 -0
- package/dist/lib/session/sync/provision.js +91 -0
- package/dist/lib/session/sync/sync.d.ts +26 -2
- package/dist/lib/session/sync/sync.js +192 -69
- package/dist/lib/session/sync/transcript-crypto.d.ts +77 -0
- package/dist/lib/session/sync/transcript-crypto.js +147 -0
- package/dist/lib/ssh-tunnel.js +13 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +16 -0
- package/dist/lib/state.d.ts +10 -2
- package/dist/lib/state.js +14 -2
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/triggers/webhook.d.ts +70 -27
- package/dist/lib/triggers/webhook.js +264 -43
- package/dist/lib/types.d.ts +9 -0
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +162 -22
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents lease` — manage the disposable cloud boxes used by `agents run --lease`.
|
|
3
|
+
*
|
|
4
|
+
* Today: `agents lease gc`, which stops expired + idle "orphan" boxes that are
|
|
5
|
+
* holding a provider's server quota (the cause of the `server_limit` 403 a new
|
|
6
|
+
* lease hits). Reaping is conservative: only boxes whose lease has expired AND
|
|
7
|
+
* that have been untouched for a safety window are eligible (see `isReapSafe`),
|
|
8
|
+
* so a box a concurrent run just reused is never stopped.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
/** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
|
|
12
|
+
export declare function validateHetznerToken(token: string, fetchImpl?: typeof fetch): Promise<'valid' | 'invalid' | 'unreachable'>;
|
|
13
|
+
/**
|
|
14
|
+
* One-time credential setup for `agents run --lease` (Hetzner today). Opens the
|
|
15
|
+
* token page, collects a token, validates it against the live API, stores it in
|
|
16
|
+
* the keychain bundle `hetzner.com`, and persists it as the default lease bundle
|
|
17
|
+
* (so `--lease` needs no env var or flag afterward). Returns true on success.
|
|
18
|
+
* Never throws for expected outcomes (non-interactive, cancel, repeated failure).
|
|
19
|
+
*/
|
|
20
|
+
export declare function runLeaseSetup(opts?: {
|
|
21
|
+
provider?: string;
|
|
22
|
+
}): Promise<boolean>;
|
|
23
|
+
export declare function registerLeaseCommand(program: Command): void;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents lease` — manage the disposable cloud boxes used by `agents run --lease`.
|
|
3
|
+
*
|
|
4
|
+
* Today: `agents lease gc`, which stops expired + idle "orphan" boxes that are
|
|
5
|
+
* holding a provider's server quota (the cause of the `server_limit` 403 a new
|
|
6
|
+
* lease hits). Reaping is conservative: only boxes whose lease has expired AND
|
|
7
|
+
* that have been untouched for a safety window are eligible (see `isReapSafe`),
|
|
8
|
+
* so a box a concurrent run just reused is never stopped.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { crabboxList, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
|
|
13
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
14
|
+
import { bundleExists, readBundle, writeBundle, keychainRef, bundleItemStore } from '../lib/secrets/bundles.js';
|
|
15
|
+
import { secretsKeychainItem } from '../lib/secrets/index.js';
|
|
16
|
+
function fmtIdle(box) {
|
|
17
|
+
if (box.lastTouchedAt === null)
|
|
18
|
+
return 'idle ?';
|
|
19
|
+
const iso = new Date(box.lastTouchedAt * 1000).toISOString().slice(0, 16).replace('T', ' ');
|
|
20
|
+
return `idle since ${iso}Z`;
|
|
21
|
+
}
|
|
22
|
+
const HETZNER_BUNDLE = 'hetzner.com';
|
|
23
|
+
const HCLOUD_KEY = 'HCLOUD_TOKEN';
|
|
24
|
+
const HETZNER_CONSOLE_URL = 'https://console.hetzner.cloud/';
|
|
25
|
+
/** Best-effort: open a URL in the user's default browser. Never throws. */
|
|
26
|
+
function openUrl(url) {
|
|
27
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
28
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
29
|
+
try {
|
|
30
|
+
const p = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
31
|
+
p.on('error', () => { });
|
|
32
|
+
p.unref();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* best-effort */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
|
|
39
|
+
export async function validateHetznerToken(token, fetchImpl = fetch) {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetchImpl('https://api.hetzner.cloud/v1/servers?per_page=1', {
|
|
42
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
43
|
+
});
|
|
44
|
+
if (res.ok)
|
|
45
|
+
return 'valid';
|
|
46
|
+
if (res.status === 401 || res.status === 403)
|
|
47
|
+
return 'invalid';
|
|
48
|
+
return 'unreachable';
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return 'unreachable';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One-time credential setup for `agents run --lease` (Hetzner today). Opens the
|
|
56
|
+
* token page, collects a token, validates it against the live API, stores it in
|
|
57
|
+
* the keychain bundle `hetzner.com`, and persists it as the default lease bundle
|
|
58
|
+
* (so `--lease` needs no env var or flag afterward). Returns true on success.
|
|
59
|
+
* Never throws for expected outcomes (non-interactive, cancel, repeated failure).
|
|
60
|
+
*/
|
|
61
|
+
export async function runLeaseSetup(opts = {}) {
|
|
62
|
+
const provider = opts.provider ?? 'hetzner';
|
|
63
|
+
if (provider !== 'hetzner') {
|
|
64
|
+
console.error(chalk.yellow(`lease setup: only 'hetzner' is supported today (got '${provider}').`));
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (!isInteractiveTerminal()) {
|
|
68
|
+
console.error(chalk.red('lease setup needs an interactive terminal. For CI/headless, set AGENTS_LEASE_SECRETS_BUNDLE ' +
|
|
69
|
+
'or store HCLOUD_TOKEN in a keychain bundle.'));
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
console.error(chalk.bold('\nSet up leasing (Hetzner) — one time (~30s):'));
|
|
73
|
+
console.error(chalk.dim('Opening the Hetzner console. Create/select a project, then Security → API Tokens →'));
|
|
74
|
+
console.error(chalk.dim('Generate a token with Read & Write permission, and copy it.\n'));
|
|
75
|
+
openUrl(HETZNER_CONSOLE_URL);
|
|
76
|
+
const { password } = await import('@inquirer/prompts');
|
|
77
|
+
const ora = (await import('ora')).default;
|
|
78
|
+
try {
|
|
79
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
80
|
+
const token = (await password({ message: 'Paste your Hetzner API token:', mask: true })).trim();
|
|
81
|
+
if (!token) {
|
|
82
|
+
console.error(chalk.yellow('No token entered.'));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const spinner = ora('Validating token against the Hetzner API…').start();
|
|
86
|
+
const result = await validateHetznerToken(token);
|
|
87
|
+
if (result === 'invalid') {
|
|
88
|
+
spinner.fail('Token rejected by Hetzner (401/403). Try again.');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (result === 'unreachable')
|
|
92
|
+
spinner.warn('Could not reach the Hetzner API to validate — storing anyway.');
|
|
93
|
+
else
|
|
94
|
+
spinner.succeed('Token valid — Hetzner API reachable.');
|
|
95
|
+
// Store into the `hetzner.com` keychain bundle (mirrors writeSyncBundle).
|
|
96
|
+
const bundle = bundleExists(HETZNER_BUNDLE)
|
|
97
|
+
? readBundle(HETZNER_BUNDLE)
|
|
98
|
+
: { name: HETZNER_BUNDLE, description: 'Hetzner Cloud API token for crabbox leases', vars: {} };
|
|
99
|
+
const store = bundleItemStore(bundle.backend);
|
|
100
|
+
store.set(secretsKeychainItem(HETZNER_BUNDLE, HCLOUD_KEY), token);
|
|
101
|
+
bundle.vars[HCLOUD_KEY] = keychainRef(HCLOUD_KEY);
|
|
102
|
+
writeBundle(bundle);
|
|
103
|
+
setLeaseSecretsBundle(HETZNER_BUNDLE);
|
|
104
|
+
console.error(chalk.green(`\n✔ Stored in keychain bundle '${HETZNER_BUNDLE}' and set as the default lease provider.`));
|
|
105
|
+
console.error(chalk.dim(' Run `agents run <agent> "…" --lease` — no env var, no flag needed.'));
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
console.error(chalk.yellow('lease setup: no valid token after 3 attempts — aborted.'));
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
if (isPromptCancelled(e)) {
|
|
113
|
+
console.error(chalk.yellow('lease setup cancelled.'));
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function registerLeaseCommand(program) {
|
|
120
|
+
const lease = program
|
|
121
|
+
.command('lease')
|
|
122
|
+
.description('Manage the disposable cloud boxes used by `agents run --lease`.');
|
|
123
|
+
lease
|
|
124
|
+
.command('setup')
|
|
125
|
+
.description('One-time credential setup so `agents run --lease` works with no env var or flag.')
|
|
126
|
+
.option('--provider <name>', 'Cloud provider (only hetzner today)', 'hetzner')
|
|
127
|
+
.action(async (opts) => {
|
|
128
|
+
const ok = await runLeaseSetup({ provider: opts.provider });
|
|
129
|
+
process.exit(ok ? 0 : 1);
|
|
130
|
+
});
|
|
131
|
+
lease
|
|
132
|
+
.command('gc')
|
|
133
|
+
.description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
|
|
134
|
+
.option('--dry-run', 'List reap-safe orphan boxes without stopping any', false)
|
|
135
|
+
.option('--yes', 'Stop them without the interactive confirm', false)
|
|
136
|
+
.option('--json', 'Output JSON', false)
|
|
137
|
+
.action(async (opts) => {
|
|
138
|
+
const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
|
|
139
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
140
|
+
let candidates;
|
|
141
|
+
try {
|
|
142
|
+
candidates = reapSafeOrphans(crabboxList(boxOpts), nowSecs);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
console.error(chalk.red(`lease gc: ${e.message}`));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (candidates.length === 0) {
|
|
150
|
+
if (opts.json)
|
|
151
|
+
console.log(JSON.stringify({ candidates: [], reaped: [] }));
|
|
152
|
+
else
|
|
153
|
+
console.error(chalk.gray('No reap-safe orphan boxes — nothing to collect.'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!opts.json) {
|
|
157
|
+
console.error(chalk.bold(`${candidates.length} reap-safe orphan box(es):`));
|
|
158
|
+
for (const b of candidates) {
|
|
159
|
+
console.error(` ${chalk.cyan(b.slug)} ${chalk.dim(`(${b.class ?? '?'}, ${fmtIdle(b)})`)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (opts.dryRun) {
|
|
163
|
+
if (opts.json)
|
|
164
|
+
console.log(JSON.stringify({ candidates, reaped: [] }, null, 2));
|
|
165
|
+
else
|
|
166
|
+
console.error(chalk.gray('\n--dry-run: nothing stopped. Re-run with --yes to stop them.'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Destructive: stopping boxes the agent did not create needs an explicit yes.
|
|
170
|
+
if (!opts.yes) {
|
|
171
|
+
const { isInteractiveTerminal, isPromptCancelled } = await import('./utils.js');
|
|
172
|
+
if (!isInteractiveTerminal()) {
|
|
173
|
+
console.error(chalk.yellow('Refusing to stop boxes without --yes in a non-interactive shell.'));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
179
|
+
const ok = await confirm({ message: `Stop these ${candidates.length} box(es)?`, default: false });
|
|
180
|
+
if (!ok) {
|
|
181
|
+
console.error(chalk.yellow('Aborted — no boxes stopped.'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (isPromptCancelled(e)) {
|
|
187
|
+
console.error(chalk.yellow('Aborted — no boxes stopped.'));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Re-list at stop time (freshness re-check) so a box touched since the
|
|
194
|
+
// preview is not stopped out from under an active run.
|
|
195
|
+
const { candidates: stopped, reaped } = reapOrphans({ ...boxOpts, nowSecs });
|
|
196
|
+
if (opts.json)
|
|
197
|
+
console.log(JSON.stringify({ candidates: stopped, reaped }, null, 2));
|
|
198
|
+
else
|
|
199
|
+
console.error(chalk.green(`Stopped ${reaped.length}/${stopped.length} box(es): ${reaped.join(', ') || '(none)'}`));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents mailboxes` — fleet comms: a read-only window onto the agent mailbox spool.
|
|
3
|
+
*
|
|
4
|
+
* The mailbox spool (`~/.agents/.history/mailbox/<id>/{inbox,processing,consumed}`)
|
|
5
|
+
* is the transport under `agents message` / `agents feed` / `agents teams message`:
|
|
6
|
+
* one box per logical agent (session UUID / teams agentId / loop runId). This
|
|
7
|
+
* command surfaces which boxes exist, how much mail each holds, the messages that
|
|
8
|
+
* flowed BETWEEN agents (including already-consumed ones), and — with `--watch` —
|
|
9
|
+
* the live stream as it happens. Rendering rides the shared comms engine
|
|
10
|
+
* (`lib/comms-render.ts`): masthead, sparkline, aggregate, graphEdges.
|
|
11
|
+
*
|
|
12
|
+
* agents mailboxes overview: masthead + 24h sparkline + boxes + recent log
|
|
13
|
+
* agents mailboxes <id> one box in full (inbox / processing / consumed)
|
|
14
|
+
* agents mailboxes --watch live tail of cross-box traffic until ⌃C
|
|
15
|
+
* agents mailboxes --between <a> <b> one relationship as a thread, either direction
|
|
16
|
+
* agents mailboxes --graph who-talks-to-whom adjacency, busiest first
|
|
17
|
+
* --from/--to/--since filter the overview log and the --watch stream
|
|
18
|
+
*/
|
|
19
|
+
import type { Command } from 'commander';
|
|
20
|
+
export declare function registerMailboxesCommand(program: Command): void;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { die, humanDuration, relTime, truncate, visibleWidth } from '../lib/format.js';
|
|
5
|
+
import { listBoxes, readBox, mailboxDir, isValidMailboxId, watchMessages, } from '../lib/mailbox.js';
|
|
6
|
+
import { GLYPH, masthead, sparkline, aggregate, hourlyCounts, graphEdges, } from '../lib/comms-render.js';
|
|
7
|
+
import { getMailboxRootDir } from '../lib/state.js';
|
|
8
|
+
import { getActiveSessions } from '../lib/session/active.js';
|
|
9
|
+
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
10
|
+
/** Short, human label for a box id — the live session's topic when running, else the id stem. */
|
|
11
|
+
function labelForBox(id, byMailbox) {
|
|
12
|
+
const s = byMailbox.get(id);
|
|
13
|
+
if (s) {
|
|
14
|
+
const topic = s.name || s.topic || s.label;
|
|
15
|
+
return { label: topic ? truncate(topic, 40) : id.slice(0, 8), live: true };
|
|
16
|
+
}
|
|
17
|
+
return { label: id.slice(0, 8), live: false };
|
|
18
|
+
}
|
|
19
|
+
/** Build the per-box views, resolving live-session labels once. */
|
|
20
|
+
async function collectBoxes(root) {
|
|
21
|
+
let sessions = [];
|
|
22
|
+
try {
|
|
23
|
+
sessions = await getActiveSessions();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Label enrichment is best-effort; a box with no live session still lists.
|
|
27
|
+
}
|
|
28
|
+
const byMailbox = new Map();
|
|
29
|
+
for (const s of sessions) {
|
|
30
|
+
const mid = mailboxIdForActiveSession(s);
|
|
31
|
+
if (mid)
|
|
32
|
+
byMailbox.set(mid, s);
|
|
33
|
+
}
|
|
34
|
+
return listBoxes(root).map((id) => {
|
|
35
|
+
const { label, live } = labelForBox(id, byMailbox);
|
|
36
|
+
return { id, label, live, messages: readBox(mailboxDir(id, root)) };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function pending(box) {
|
|
40
|
+
return box.messages.filter((m) => m.state !== 'consumed').length;
|
|
41
|
+
}
|
|
42
|
+
/** `from` label for a message — agents stamp `claude/<slug>`; operator sends may omit it. */
|
|
43
|
+
function senderOf(m) {
|
|
44
|
+
return m.from || 'operator';
|
|
45
|
+
}
|
|
46
|
+
const STATE_TAG = {
|
|
47
|
+
inbox: chalk.yellow('pending'),
|
|
48
|
+
processing: chalk.cyan('in-flight'),
|
|
49
|
+
consumed: chalk.dim('delivered'),
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The watching agent's own box id. Spawn wiring (`buildExecEnv`,
|
|
53
|
+
* lib/exec.ts) hands every agent `AGENTS_MAILBOX_DIR` keyed by its box, so
|
|
54
|
+
* `basename` is the id that resolves to "you". Unset for a human operator at
|
|
55
|
+
* a plain terminal — nothing in the spool is addressed to them.
|
|
56
|
+
*/
|
|
57
|
+
function selfMailboxId() {
|
|
58
|
+
const dir = process.env.AGENTS_MAILBOX_DIR;
|
|
59
|
+
if (!dir)
|
|
60
|
+
return undefined;
|
|
61
|
+
const id = path.basename(dir.replace(/[/\\]+$/, ''));
|
|
62
|
+
return id || undefined;
|
|
63
|
+
}
|
|
64
|
+
/** Parse `--since`: relative offsets (30s/5m/2h/7d/4w) or an ISO/absolute date. Returns epoch ms. */
|
|
65
|
+
function parseSinceArg(s) {
|
|
66
|
+
const m = s.match(/^(\d+)([smhdw])$/);
|
|
67
|
+
if (m) {
|
|
68
|
+
const n = parseInt(m[1], 10);
|
|
69
|
+
const unitMs = {
|
|
70
|
+
s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000,
|
|
71
|
+
};
|
|
72
|
+
return Date.now() - n * unitMs[m[2]];
|
|
73
|
+
}
|
|
74
|
+
const ms = Date.parse(s);
|
|
75
|
+
if (Number.isNaN(ms))
|
|
76
|
+
die(`Invalid --since value: ${JSON.stringify(s)} (use e.g. 2h, 7d, or an ISO date).`);
|
|
77
|
+
return ms;
|
|
78
|
+
}
|
|
79
|
+
function buildFilters(opts) {
|
|
80
|
+
return {
|
|
81
|
+
from: opts.from,
|
|
82
|
+
to: opts.to,
|
|
83
|
+
sinceMs: opts.since ? parseSinceArg(opts.since) : undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function hasFilters(f) {
|
|
87
|
+
return Boolean(f.from || f.to || f.sinceMs != null);
|
|
88
|
+
}
|
|
89
|
+
/** Sender substring on `from`, recipient substring on box id/label, recency cutoff — all case-insensitive. */
|
|
90
|
+
function matchesFilters(msg, toLabel, boxId, f) {
|
|
91
|
+
if (f.sinceMs != null) {
|
|
92
|
+
const t = Date.parse(msg.ts);
|
|
93
|
+
if (Number.isNaN(t) || t < f.sinceMs)
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (f.from && !msg.from.toLowerCase().includes(f.from.toLowerCase()))
|
|
97
|
+
return false;
|
|
98
|
+
if (f.to && !toLabel.toLowerCase().includes(f.to.toLowerCase()) && !boxId.toLowerCase().includes(f.to.toLowerCase()))
|
|
99
|
+
return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/** HH:MM:SS local wall-clock for the --watch stream; unparseable stamps render as dashes. */
|
|
103
|
+
function clockTime(iso) {
|
|
104
|
+
const d = new Date(iso);
|
|
105
|
+
if (Number.isNaN(d.getTime()))
|
|
106
|
+
return '--:--:--';
|
|
107
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
108
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
109
|
+
}
|
|
110
|
+
function renderOverview(boxes, limit, filters) {
|
|
111
|
+
const nonEmpty = boxes.filter((b) => b.messages.length > 0);
|
|
112
|
+
if (nonEmpty.length === 0) {
|
|
113
|
+
console.log(chalk.dim('No mailboxes with messages. Boxes are created on the first `agents message`.'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// 1. Masthead + 24h volume sparkline over the whole spool.
|
|
117
|
+
const msgs = aggregate(nonEmpty);
|
|
118
|
+
const live = boxes.filter((b) => b.live).length;
|
|
119
|
+
const awaiting = msgs.filter((m) => m.state !== 'consumed').length;
|
|
120
|
+
console.log(masthead({
|
|
121
|
+
title: 'fleet comms',
|
|
122
|
+
host: os.hostname(),
|
|
123
|
+
accent: 'cyan',
|
|
124
|
+
right: `${live} live · ${boxes.length} boxes`,
|
|
125
|
+
stats: [`${msgs.length} messages`, `${awaiting} awaiting delivery`, `last ${relTime(msgs[0].ts)}`],
|
|
126
|
+
}));
|
|
127
|
+
console.log(` ${chalk.dim('24h')} ${chalk.cyan(sparkline(hourlyCounts(msgs, 24)))}`);
|
|
128
|
+
console.log();
|
|
129
|
+
// 2. Box summary — one row per box that has ever held mail.
|
|
130
|
+
const rows = [...nonEmpty].sort((a, b) => lastTs(b) - lastTs(a));
|
|
131
|
+
for (const box of rows) {
|
|
132
|
+
const p = pending(box);
|
|
133
|
+
const dot = box.live ? chalk.green(GLYPH.live) : chalk.dim(GLYPH.idle);
|
|
134
|
+
const counts = [
|
|
135
|
+
p > 0 ? chalk.yellow(`${p} pending`) : null,
|
|
136
|
+
chalk.dim(`${box.messages.length} total`),
|
|
137
|
+
].filter(Boolean).join(chalk.dim(' · '));
|
|
138
|
+
const last = box.messages.length ? chalk.dim(relTime(newestMessage(box).ts)) : '';
|
|
139
|
+
console.log(` ${dot} ${chalk.bold(box.label.padEnd(40))} ${counts} ${last}`);
|
|
140
|
+
console.log(` ${chalk.dim(box.id)}`);
|
|
141
|
+
}
|
|
142
|
+
// 3. Recent cross-box message log — the agent-to-agent chatter, newest first.
|
|
143
|
+
const all = msgs
|
|
144
|
+
.filter((m) => matchesFilters(m, m.toLabel, m.box, filters))
|
|
145
|
+
.slice(0, limit);
|
|
146
|
+
console.log();
|
|
147
|
+
console.log(chalk.bold(`Recent messages`) + chalk.dim(` (${all.length})`));
|
|
148
|
+
if (all.length === 0) {
|
|
149
|
+
console.log(chalk.dim(' Nothing matches the current filters.'));
|
|
150
|
+
}
|
|
151
|
+
for (const m of all) {
|
|
152
|
+
const when = chalk.dim(relTime(m.ts).padStart(10));
|
|
153
|
+
const route = `${chalk.magenta(truncate(m.from, 24))} ${chalk.dim(GLYPH.route)} ${chalk.cyan(truncate(m.toLabel, 40))}`;
|
|
154
|
+
console.log(` ${when} ${STATE_TAG[m.state]} ${route}`);
|
|
155
|
+
console.log(` ${truncate(m.text.replace(/\s+/g, ' ').trim(), 100)}`);
|
|
156
|
+
}
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(chalk.dim('Tip: `agents mailboxes <id>` one box · `--watch` live tail · `--between <a> <b>` thread · `--graph` routes · `--json` machine output'));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* --watch: the money shot. Stream every new cross-box message as it lands,
|
|
162
|
+
* resolved to live labels; a message addressed to the watching agent's own
|
|
163
|
+
* box (AGENTS_MAILBOX_DIR) renders as `▲ you` so an orchestrator sees its
|
|
164
|
+
* replies light up. ⌃C aborts the poller cleanly via AbortController.
|
|
165
|
+
*/
|
|
166
|
+
async function runWatch(root, opts) {
|
|
167
|
+
const boxes = await collectBoxes(root);
|
|
168
|
+
const labels = new Map(boxes.map((b) => [b.id, b.label]));
|
|
169
|
+
const self = selfMailboxId();
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
const onSigint = () => controller.abort();
|
|
172
|
+
process.on('SIGINT', onSigint);
|
|
173
|
+
try {
|
|
174
|
+
if (!opts.json) {
|
|
175
|
+
console.log(masthead({
|
|
176
|
+
title: 'fleet comms',
|
|
177
|
+
host: os.hostname(),
|
|
178
|
+
accent: 'cyan',
|
|
179
|
+
right: chalk.green(GLYPH.live) + chalk.dim(' live'),
|
|
180
|
+
stats: ['watching — ⌃C to stop'],
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
// --since backfills: the watcher replays existing mail and the recency
|
|
184
|
+
// filter keeps only the requested window, then the tail continues live.
|
|
185
|
+
for await (const m of watchMessages(root, {
|
|
186
|
+
signal: controller.signal,
|
|
187
|
+
backfill: opts.filters.sinceMs != null,
|
|
188
|
+
})) {
|
|
189
|
+
const toLabel = labels.get(m.box) ?? m.toLabel;
|
|
190
|
+
if (!matchesFilters(m, toLabel, m.box, opts.filters))
|
|
191
|
+
continue;
|
|
192
|
+
const msg = { ...m, toLabel };
|
|
193
|
+
if (opts.json) {
|
|
194
|
+
console.log(JSON.stringify(msg));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const addressedToYou = self != null && m.to === self;
|
|
198
|
+
const to = addressedToYou
|
|
199
|
+
? chalk.yellow(`${GLYPH.ask} you`)
|
|
200
|
+
: chalk.cyan(truncate(toLabel, 24));
|
|
201
|
+
const text = truncate(m.text.replace(/\s+/g, ' ').trim(), 100);
|
|
202
|
+
console.log(`${chalk.dim(clockTime(m.ts))} ${chalk.magenta(truncate(m.from, 24))} ${chalk.dim(GLYPH.stream)} ${to} ${text}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
process.removeListener('SIGINT', onSigint);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Sender stamp match: `from` is freeform, so match the counterpart's full id, an id prefix, or its resolved label. */
|
|
210
|
+
function senderIsBox(from, box) {
|
|
211
|
+
if (from === box.id)
|
|
212
|
+
return true;
|
|
213
|
+
if (from.length >= 4 && box.id.startsWith(from))
|
|
214
|
+
return true;
|
|
215
|
+
return from.toLowerCase() === box.label.toLowerCase();
|
|
216
|
+
}
|
|
217
|
+
function renderBetween(boxes, a, b, json) {
|
|
218
|
+
const resolveBox = (q) => {
|
|
219
|
+
const found = boxes.find((x) => x.id === q)
|
|
220
|
+
?? boxes.find((x) => x.id.startsWith(q))
|
|
221
|
+
?? boxes.find((x) => x.label.toLowerCase().includes(q.toLowerCase()));
|
|
222
|
+
if (!found)
|
|
223
|
+
die(`No mailbox matching ${JSON.stringify(q)}. Run \`agents mailboxes\` to list boxes.`);
|
|
224
|
+
return found;
|
|
225
|
+
};
|
|
226
|
+
const boxA = resolveBox(a);
|
|
227
|
+
const boxB = resolveBox(b);
|
|
228
|
+
if (boxA.id === boxB.id)
|
|
229
|
+
die('--between needs two different boxes.');
|
|
230
|
+
// Both directions, each stamped with its route so the thread reads chronologically.
|
|
231
|
+
const thread = [
|
|
232
|
+
...boxB.messages.filter((m) => senderIsBox(senderOf(m), boxA)).map((m) => ({
|
|
233
|
+
from: senderOf(m), to: boxB.id, toLabel: boxB.label, ts: m.ts, text: m.text, state: m.state, box: boxB.id,
|
|
234
|
+
})),
|
|
235
|
+
...boxA.messages.filter((m) => senderIsBox(senderOf(m), boxB)).map((m) => ({
|
|
236
|
+
from: senderOf(m), to: boxA.id, toLabel: boxA.label, ts: m.ts, text: m.text, state: m.state, box: boxA.id,
|
|
237
|
+
})),
|
|
238
|
+
].sort((x, y) => (x.ts < y.ts ? -1 : x.ts > y.ts ? 1 : 0));
|
|
239
|
+
if (json) {
|
|
240
|
+
console.log(JSON.stringify({
|
|
241
|
+
a: { id: boxA.id, label: boxA.label },
|
|
242
|
+
b: { id: boxB.id, label: boxB.label },
|
|
243
|
+
count: thread.length,
|
|
244
|
+
messages: thread,
|
|
245
|
+
}, null, 2));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const span = thread.length >= 2
|
|
249
|
+
? humanDuration(Math.max(0, Date.parse(thread[thread.length - 1].ts) - Date.parse(thread[0].ts)))
|
|
250
|
+
: '0s';
|
|
251
|
+
console.log(` ${chalk.magenta(boxA.label)} ${chalk.dim(GLYPH.thread)} ${chalk.cyan(boxB.label)}` +
|
|
252
|
+
chalk.dim(` ${thread.length} message${thread.length === 1 ? '' : 's'} · ${span}`));
|
|
253
|
+
if (thread.length === 0) {
|
|
254
|
+
console.log(chalk.dim(' No messages between these boxes yet. Sender stamps match on box id or label.'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
console.log();
|
|
258
|
+
for (const m of thread) {
|
|
259
|
+
const when = chalk.dim(relTime(m.ts).padStart(10));
|
|
260
|
+
const route = `${chalk.magenta(truncate(m.from, 24))} ${chalk.dim(GLYPH.route)} ${chalk.cyan(truncate(m.toLabel, 24))}`;
|
|
261
|
+
console.log(` ${when} ${STATE_TAG[m.state]} ${route}`);
|
|
262
|
+
console.log(` ${truncate(m.text.replace(/\s+/g, ' ').trim(), 100)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function renderGraph(boxes, filters, json) {
|
|
266
|
+
const nonEmpty = boxes.filter((b) => b.messages.length > 0);
|
|
267
|
+
const msgs = aggregate(nonEmpty).filter((m) => matchesFilters(m, m.toLabel, m.box, filters));
|
|
268
|
+
const edges = graphEdges(msgs);
|
|
269
|
+
if (json) {
|
|
270
|
+
console.log(JSON.stringify(edges, null, 2));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.log(masthead({
|
|
274
|
+
title: 'fleet comms',
|
|
275
|
+
host: os.hostname(),
|
|
276
|
+
accent: 'cyan',
|
|
277
|
+
right: `${edges.length} route${edges.length === 1 ? '' : 's'}`,
|
|
278
|
+
stats: [`${msgs.length} messages`],
|
|
279
|
+
}));
|
|
280
|
+
console.log();
|
|
281
|
+
if (edges.length === 0) {
|
|
282
|
+
console.log(chalk.dim(' No cross-box routes yet.'));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const e of edges) {
|
|
286
|
+
const route = ` ${chalk.magenta(truncate(e.from, 28))} ${chalk.dim('└─▶')} ${chalk.cyan(truncate(e.to, 40))}`;
|
|
287
|
+
const dots = '.'.repeat(Math.max(2, 76 - visibleWidth(route)));
|
|
288
|
+
console.log(`${route} ${chalk.dim(dots)} ${e.count}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function renderBox(box) {
|
|
292
|
+
const dot = box.live ? chalk.green(`${GLYPH.live} live`) : chalk.dim(`${GLYPH.idle} not running`);
|
|
293
|
+
console.log(chalk.bold(box.label) + ' ' + dot);
|
|
294
|
+
console.log(chalk.dim(box.id));
|
|
295
|
+
if (box.messages.length === 0) {
|
|
296
|
+
console.log(chalk.dim(' (empty)'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.log();
|
|
300
|
+
for (const m of box.messages) {
|
|
301
|
+
const when = chalk.dim(relTime(m.ts));
|
|
302
|
+
console.log(` ${STATE_TAG[m.state]} ${chalk.magenta(senderOf(m))} ${when}${m.blockId ? chalk.dim(` block ${m.blockId.slice(0, 12)}`) : ''}`);
|
|
303
|
+
console.log(` ${truncate(m.text.replace(/\s+/g, ' ').trim(), 200)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function lastTs(box) {
|
|
307
|
+
if (box.messages.length === 0)
|
|
308
|
+
return 0;
|
|
309
|
+
const t = Date.parse(newestMessage(box).ts);
|
|
310
|
+
return Number.isNaN(t) ? 0 : t;
|
|
311
|
+
}
|
|
312
|
+
function newestMessage(box) {
|
|
313
|
+
return box.messages.reduce((a, b) => (a.ts >= b.ts ? a : b));
|
|
314
|
+
}
|
|
315
|
+
export function registerMailboxesCommand(program) {
|
|
316
|
+
program
|
|
317
|
+
.command('mailboxes')
|
|
318
|
+
.alias('mailbox')
|
|
319
|
+
.argument('[id]', 'A mailbox id (session UUID / teams agentId) to inspect in full')
|
|
320
|
+
.description('Fleet comms — boxes, live cross-box traffic, threads, and routes across the agent mailbox spool')
|
|
321
|
+
.option('--json', 'Output as JSON (NDJSON stream with --watch)')
|
|
322
|
+
.option('-n, --limit <n>', 'Max recent messages in the overview log', '20')
|
|
323
|
+
.option('-f, --watch', 'Live tail: stream new cross-box messages until Ctrl-C')
|
|
324
|
+
.option('--between <boxes...>', 'Thread view: every message between two boxes (id or label), either direction')
|
|
325
|
+
.option('--from <agent>', 'Only messages whose sender contains <agent>')
|
|
326
|
+
.option('--to <agent>', 'Only messages to boxes whose id or label contains <agent>')
|
|
327
|
+
.option('--since <dur>', 'Only messages newer than <dur> (30s/5m/2h/7d or ISO date); with --watch, backfills the window')
|
|
328
|
+
.option('--graph', 'Who-talks-to-whom adjacency, busiest first')
|
|
329
|
+
.action(async (id, opts) => {
|
|
330
|
+
const root = getMailboxRootDir();
|
|
331
|
+
const limit = Math.max(1, Number.parseInt(opts.limit ?? '20', 10) || 20);
|
|
332
|
+
const filters = buildFilters(opts);
|
|
333
|
+
if (opts.between && opts.between.length !== 2) {
|
|
334
|
+
die(`--between takes exactly two boxes: \`agents mailboxes --between <a> <b>\`.`);
|
|
335
|
+
}
|
|
336
|
+
// The views are mutually exclusive — never silently drop a flag.
|
|
337
|
+
const viewCount = (id ? 1 : 0) + (opts.between ? 1 : 0) + (opts.graph ? 1 : 0);
|
|
338
|
+
if (opts.watch && viewCount > 0) {
|
|
339
|
+
die('--watch streams the whole fleet and combines with no other view. Drop <id>/--between/--graph, or use --from/--to/--since to filter the stream.');
|
|
340
|
+
}
|
|
341
|
+
if (!opts.watch && viewCount > 1) {
|
|
342
|
+
die('Pick one view: <id>, --between, or --graph.');
|
|
343
|
+
}
|
|
344
|
+
if (opts.watch) {
|
|
345
|
+
await runWatch(root, { json: opts.json, filters });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const boxes = await collectBoxes(root);
|
|
349
|
+
if (opts.between) {
|
|
350
|
+
renderBetween(boxes, opts.between[0], opts.between[1], opts.json);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (opts.graph) {
|
|
354
|
+
renderGraph(boxes, filters, opts.json);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (id) {
|
|
358
|
+
if (!isValidMailboxId(id))
|
|
359
|
+
die(`Invalid mailbox id ${JSON.stringify(id)}.`);
|
|
360
|
+
const box = boxes.find((b) => b.id === id) ?? boxes.find((b) => b.id.startsWith(id));
|
|
361
|
+
if (!box)
|
|
362
|
+
die(`No mailbox ${JSON.stringify(id)} under ${root}.`);
|
|
363
|
+
if (opts.json) {
|
|
364
|
+
console.log(JSON.stringify(box, null, 2));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
renderBox(box);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (opts.json) {
|
|
371
|
+
// Unfiltered output keeps the legacy shape byte-for-byte; with filters
|
|
372
|
+
// the JSON mirrors the filtered overview log — pending/total recount.
|
|
373
|
+
console.log(JSON.stringify(boxes.map((b) => {
|
|
374
|
+
const messages = hasFilters(filters)
|
|
375
|
+
? b.messages.filter((m) => matchesFilters({ from: senderOf(m), ts: m.ts }, b.label, b.id, filters))
|
|
376
|
+
: b.messages;
|
|
377
|
+
return {
|
|
378
|
+
id: b.id,
|
|
379
|
+
label: b.label,
|
|
380
|
+
live: b.live,
|
|
381
|
+
pending: messages.filter((m) => m.state !== 'consumed').length,
|
|
382
|
+
total: messages.length,
|
|
383
|
+
messages,
|
|
384
|
+
};
|
|
385
|
+
}), null, 2));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
renderOverview(boxes, limit, filters);
|
|
389
|
+
});
|
|
390
|
+
}
|
package/dist/commands/repo.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extra DotAgent repo management.
|
|
3
3
|
*
|
|
4
|
-
* Registers `agents
|
|
5
|
-
* additional DotAgent repos alongside the primary ~/.agents/.system/
|
|
6
|
-
* private, work, or team skills can ship separately from public ones.
|
|
4
|
+
* Registers `agents repos add|init|list|remove|enable|disable` (`repo` alias)
|
|
5
|
+
* which manage additional DotAgent repos alongside the primary ~/.agents/.system/
|
|
6
|
+
* repo so private, work, or team skills can ship separately from public ones.
|
|
7
7
|
*
|
|
8
8
|
* Extras are user-level config: managed clones live at ~/.agents-<alias>/ as
|
|
9
9
|
* peer dirs to ~/.agents/, and user-owned repos may live anywhere. All extras
|
|
@@ -73,6 +73,6 @@ export declare function formatResourceDelta(entries: {
|
|
|
73
73
|
export declare function repoSlug(url: string): string;
|
|
74
74
|
/** Pack colored phrases into `, `-joined lines no wider than `width`. */
|
|
75
75
|
export declare function wrapPhrases(parts: string[], width: number): string[];
|
|
76
|
-
/** Register the `agents
|
|
76
|
+
/** Register the `agents repos` command tree (`repo` is a convenience alias). */
|
|
77
77
|
export declare function registerRepoCommands(program: Command): void;
|
|
78
78
|
export {};
|