@phnx-labs/agents-cli 1.20.93 → 1.21.0
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 +264 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +7 -0
- package/dist/lib/activity.js +19 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/scheduler.js +12 -2
- 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/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +17 -1
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
|
@@ -17,32 +17,12 @@ import { spawn } from 'child_process';
|
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
19
19
|
import { sshTargetFor } from '../devices/connect.js';
|
|
20
|
-
import {
|
|
21
|
-
import { loadDevices, isControlDevice, isDialableDevice } from '../devices/registry.js';
|
|
20
|
+
import { loadDevices } from '../devices/registry.js';
|
|
22
21
|
import { remoteShellFor, buildWindowsAgentsCommand } from '../hosts/remote-cmd.js';
|
|
23
|
-
import {
|
|
22
|
+
import { gatherRemoteAgentsJson } from '../remote-agents-json.js';
|
|
23
|
+
import { normalizeHost } from './sync/config.js';
|
|
24
24
|
import { NO_FANOUT_ENV } from './remote-active.js';
|
|
25
25
|
import { terminalWidth } from './width.js';
|
|
26
|
-
/** Per-host SSH budget. Slightly above SSH_OPTS' ConnectTimeout=10 so a
|
|
27
|
-
* reachable-but-slow remote still answers before we give up. */
|
|
28
|
-
const REMOTE_TIMEOUT_MS = 12_000;
|
|
29
|
-
/**
|
|
30
|
-
* The command run on each peer: answer for itself, as JSON, without recursing.
|
|
31
|
-
* `forwardedArgs` carry the caller's own query/filters (already including the
|
|
32
|
-
* leading `sessions` and a `--json`) so each peer returns a comparable slice.
|
|
33
|
-
* A Windows peer gets a PowerShell invocation (ssh lands in cmd.exe/PowerShell
|
|
34
|
-
* there, where `bash -lc` is not a command); every other OS keeps `bash -lc`.
|
|
35
|
-
*/
|
|
36
|
-
export function remoteListCommand(forwardedArgs, os) {
|
|
37
|
-
if (remoteShellFor(os) === 'powershell') {
|
|
38
|
-
return buildWindowsAgentsCommand({
|
|
39
|
-
args: forwardedArgs,
|
|
40
|
-
env: { [NO_FANOUT_ENV]: '1' },
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
const inner = [`${NO_FANOUT_ENV}=1`, 'agents', ...forwardedArgs].map((t, i) => i === 0 ? t : shellQuote(t)).join(' ');
|
|
44
|
-
return `bash -lc ${shellQuote(inner)}`;
|
|
45
|
-
}
|
|
46
26
|
/**
|
|
47
27
|
* Parse a peer's `sessions --json` stdout into `SessionMeta[]`, tagging each
|
|
48
28
|
* with `machine`. Defensive against version skew / partial output: non-JSON or
|
|
@@ -83,61 +63,22 @@ export function parseRemoteListPayload(stdout, machine, safeResolver = false) {
|
|
|
83
63
|
parsed = JSON.parse(stdout);
|
|
84
64
|
}
|
|
85
65
|
catch {
|
|
86
|
-
return {
|
|
66
|
+
return { items: [], valid: false };
|
|
87
67
|
}
|
|
88
68
|
if (!Array.isArray(parsed))
|
|
89
|
-
return {
|
|
69
|
+
return { items: [], valid: false };
|
|
90
70
|
const out = [];
|
|
91
71
|
for (const x of parsed) {
|
|
92
72
|
if (!x || typeof x !== 'object' || Array.isArray(x))
|
|
93
|
-
return {
|
|
73
|
+
return { items: [], valid: false };
|
|
94
74
|
if (safeResolver && !isSafeResolverRow(x)) {
|
|
95
|
-
return {
|
|
75
|
+
return { items: [], valid: false };
|
|
96
76
|
}
|
|
97
77
|
// `_remote` marks these as living on the peer's disk (not a local mirror),
|
|
98
78
|
// so the picker routes read/resume back over SSH instead of the local FS.
|
|
99
79
|
out.push({ ...x, machine, _remote: true });
|
|
100
80
|
}
|
|
101
|
-
return {
|
|
102
|
-
}
|
|
103
|
-
/** Convert one completed peer process into the exact aggregation result. This
|
|
104
|
-
* is the production parent/peer seam and is exercised with real child output. */
|
|
105
|
-
export function remoteListCaptureResult(code, stdout, machine, display, safeResolver = false) {
|
|
106
|
-
if (code !== 0)
|
|
107
|
-
return { sessions: [], unreachable: display };
|
|
108
|
-
const parsed = parseRemoteListPayload(stdout, machine, safeResolver);
|
|
109
|
-
return parsed.valid ? { sessions: parsed.sessions } : { sessions: [], unreachable: display };
|
|
110
|
-
}
|
|
111
|
-
/** Run one remote `agents sessions … --json` and capture stdout. Resolves
|
|
112
|
-
* `{ code: null }` on spawn error or timeout (host treated as dead). */
|
|
113
|
-
function sshCapture(target, remoteCmd, timeoutMs) {
|
|
114
|
-
assertValidSshTarget(target);
|
|
115
|
-
return new Promise((resolve) => {
|
|
116
|
-
const args = [...SSH_OPTS, ...controlOpts(), target, remoteCmd];
|
|
117
|
-
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
118
|
-
let stdout = '';
|
|
119
|
-
let settled = false;
|
|
120
|
-
const done = (code) => {
|
|
121
|
-
if (settled)
|
|
122
|
-
return;
|
|
123
|
-
settled = true;
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
resolve({ code, stdout });
|
|
126
|
-
};
|
|
127
|
-
const timer = setTimeout(() => { child.kill('SIGKILL'); done(null); }, timeoutMs);
|
|
128
|
-
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
129
|
-
child.on('error', () => done(null));
|
|
130
|
-
child.on('close', (code) => done(code));
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
async function fetchByTarget(target, machine, display, forwardedArgs, os) {
|
|
134
|
-
const { code, stdout } = await sshCapture(target, remoteListCommand(forwardedArgs, os), REMOTE_TIMEOUT_MS);
|
|
135
|
-
const safeResolver = forwardedArgs.includes('--resolve-safe-v1');
|
|
136
|
-
const result = remoteListCaptureResult(code, stdout, machine, display, safeResolver);
|
|
137
|
-
if (result.unreachable) {
|
|
138
|
-
process.stderr.write(chalk.gray(` ${display}: unreachable or no agents CLI — skipped\n`));
|
|
139
|
-
}
|
|
140
|
-
return result;
|
|
81
|
+
return { items: out, valid: true };
|
|
141
82
|
}
|
|
142
83
|
/**
|
|
143
84
|
* Gather listing sessions from other machines. With an explicit `hosts` list
|
|
@@ -147,51 +88,21 @@ async function fetchByTarget(target, machine, display, forwardedArgs, os) {
|
|
|
147
88
|
* already `--json`) so every peer returns the same slice this machine asked for.
|
|
148
89
|
*/
|
|
149
90
|
export async function gatherRemoteList(forwardedArgs, hosts) {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
let reg;
|
|
159
|
-
try {
|
|
160
|
-
reg = await loadDevices();
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return { sessions: [], deviceCount: 0, unreachable: ['device registry'] };
|
|
164
|
-
}
|
|
165
|
-
for (const d of Object.values(reg)) {
|
|
166
|
-
// Live SSH-probe verdict first, cached tailscale snapshot only as a
|
|
167
|
-
// fallback — see isDialableDevice. A manually-registered device has no
|
|
168
|
-
// tailscale peer entry, so gating on `online` alone hid its sessions.
|
|
169
|
-
if (!isDialableDevice(d))
|
|
170
|
-
continue;
|
|
171
|
-
if (normalizeHost(d.name) === self)
|
|
172
|
-
continue;
|
|
173
|
-
// Control-only devices (a phone/tablet running the cockpit) drive the fleet
|
|
174
|
-
// but never run agents — never dial them, whatever their platform reads as.
|
|
175
|
-
if (isControlDevice(d))
|
|
176
|
-
continue;
|
|
177
|
-
// Only machines that can actually run the CLI. iOS/tablet nodes register as
|
|
178
|
-
// `unknown` platform and can never answer, so skip them rather than burn a
|
|
179
|
-
// full ConnectTimeout on each.
|
|
180
|
-
if (d.platform !== 'windows' && d.platform !== 'linux' && d.platform !== 'macos')
|
|
181
|
-
continue;
|
|
182
|
-
try {
|
|
183
|
-
targets.push({ target: sshTargetFor(d), machine: normalizeHost(d.name), name: d.name, os: d.platform });
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
// No address on the profile — nothing to dial; skip silently.
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const results = await Promise.all(targets.map((t) => fetchByTarget(t.target, t.machine, t.name, forwardedArgs, t.os)));
|
|
91
|
+
const safeResolver = forwardedArgs.includes('--resolve-safe-v1');
|
|
92
|
+
const result = await gatherRemoteAgentsJson({
|
|
93
|
+
args: forwardedArgs,
|
|
94
|
+
noFanoutEnv: NO_FANOUT_ENV,
|
|
95
|
+
hosts,
|
|
96
|
+
parse: (stdout, machine) => parseRemoteListPayload(stdout, machine, safeResolver),
|
|
97
|
+
});
|
|
191
98
|
return {
|
|
192
|
-
sessions:
|
|
193
|
-
deviceCount:
|
|
194
|
-
unreachable:
|
|
99
|
+
sessions: result.items,
|
|
100
|
+
deviceCount: result.deviceCount,
|
|
101
|
+
unreachable: [
|
|
102
|
+
...(result.discoveryFailed ? ['device registry'] : []),
|
|
103
|
+
...result.skipped,
|
|
104
|
+
...result.parseFailed,
|
|
105
|
+
],
|
|
195
106
|
};
|
|
196
107
|
}
|
|
197
108
|
/** Resolve a peer's SSH target (and OS) from the device registry by its
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* R2 credential resolution + this machine's stable identity. The only field
|
|
3
|
+
* `agents sessions export --encrypt` / `import` actually consume is the
|
|
4
|
+
* shared `R2_SYNC_ENC_KEY` transcript key (`resolveSyncEncKey`); the other R2
|
|
5
|
+
* fields (account/bucket/access keys) are validated here as a presence gate —
|
|
6
|
+
* no `r2.backups` bundle configured means no shared key, so export/import
|
|
7
|
+
* fall back to an ephemeral one. There is no R2 network client in this
|
|
8
|
+
* codebase today (it moved with the rest of the R2/CRDT background sync this
|
|
9
|
+
* bundle used to back — see git history for `./r2.ts`); a future R2-backed
|
|
10
|
+
* feature (an export destination, a fleet cache) would add one where it's
|
|
11
|
+
* actually wired. Credentials come from the `r2.backups` secrets bundle (OS
|
|
12
|
+
* keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
5
13
|
*/
|
|
6
14
|
/** Secrets bundle holding the R2 credentials. */
|
|
7
15
|
export declare const SYNC_BUNDLE = "r2.backups";
|
|
@@ -16,8 +24,8 @@ export interface R2Config {
|
|
|
16
24
|
* Shared 32-byte key (hex or base64) for client-side transcript encryption,
|
|
17
25
|
* held in the bundle as `R2_SYNC_ENC_KEY`. Optional and deliberately separate
|
|
18
26
|
* from the R2 credentials so rotating the access token never orphans already
|
|
19
|
-
* encrypted
|
|
20
|
-
*
|
|
27
|
+
* encrypted bundles. `agents sessions export --encrypt` prefers this key when
|
|
28
|
+
* present, else mints and prints an ephemeral one — see transcript-crypto.ts.
|
|
21
29
|
*/
|
|
22
30
|
syncEncKey?: string;
|
|
23
31
|
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* R2 credential resolution + this machine's stable identity. The only field
|
|
3
|
+
* `agents sessions export --encrypt` / `import` actually consume is the
|
|
4
|
+
* shared `R2_SYNC_ENC_KEY` transcript key (`resolveSyncEncKey`); the other R2
|
|
5
|
+
* fields (account/bucket/access keys) are validated here as a presence gate —
|
|
6
|
+
* no `r2.backups` bundle configured means no shared key, so export/import
|
|
7
|
+
* fall back to an ephemeral one. There is no R2 network client in this
|
|
8
|
+
* codebase today (it moved with the rest of the R2/CRDT background sync this
|
|
9
|
+
* bundle used to back — see git history for `./r2.ts`); a future R2-backed
|
|
10
|
+
* feature (an export destination, a fleet cache) would add one where it's
|
|
11
|
+
* actually wired. Credentials come from the `r2.backups` secrets bundle (OS
|
|
12
|
+
* keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
5
13
|
*/
|
|
6
14
|
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../../secrets/bundles.js';
|
|
7
15
|
/** Secrets bundle holding the R2 credentials. */
|
|
@@ -12,13 +20,13 @@ export const SYNC_BUNDLE = 'r2.backups';
|
|
|
12
20
|
* without real credentials (no silent fallback).
|
|
13
21
|
*/
|
|
14
22
|
function resolveR2Config() {
|
|
15
|
-
//
|
|
16
|
-
// isHeadlessSecretsContext()
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// and lets any interactive caller of loadR2Config still prompt.
|
|
21
|
-
const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: '
|
|
23
|
+
// A headless caller (no TTY — e.g. a routine or SSH-dispatched command) must
|
|
24
|
+
// resolve broker-only: isHeadlessSecretsContext() true means a broker miss
|
|
25
|
+
// can never pop an unattended Touch ID sheet on the user's screen (the same
|
|
26
|
+
// broker-only-when-headless rationale the secrets readers use). Using the
|
|
27
|
+
// shared predicate rather than a literal keeps it consistent with the other
|
|
28
|
+
// callers and lets any interactive caller of loadR2Config still prompt.
|
|
29
|
+
const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'session-transport', agentOnly: isHeadlessSecretsContext() });
|
|
22
30
|
const accountId = env.R2_ACCOUNT_ID?.trim();
|
|
23
31
|
const bucket = env.R2_BUCKET_NAME?.trim();
|
|
24
32
|
const accessKeyId = env.R2_ACCESS_KEY_ID?.trim();
|
|
@@ -31,7 +39,7 @@ function resolveR2Config() {
|
|
|
31
39
|
!secretAccessKey && 'R2_SECRET_ACCESS_KEY',
|
|
32
40
|
].filter(Boolean);
|
|
33
41
|
if (missing.length > 0) {
|
|
34
|
-
throw new Error(`
|
|
42
|
+
throw new Error(`Session R2 transport: bundle '${SYNC_BUNDLE}' is missing ${missing.join(', ')}. ` +
|
|
35
43
|
`Add them with: agents secrets add ${SYNC_BUNDLE} <KEY>`);
|
|
36
44
|
}
|
|
37
45
|
return {
|
|
@@ -40,8 +48,8 @@ function resolveR2Config() {
|
|
|
40
48
|
accessKeyId: accessKeyId,
|
|
41
49
|
secretAccessKey: secretAccessKey,
|
|
42
50
|
// Default to the account's R2 endpoint; an explicit R2_ENDPOINT override
|
|
43
|
-
// points
|
|
44
|
-
//
|
|
51
|
+
// points at any S3-compatible store (MinIO, another provider) — which is
|
|
52
|
+
// also how the feature is verified end-to-end without live R2.
|
|
45
53
|
endpoint: env.R2_ENDPOINT?.trim() || `https://${accountId}.r2.cloudflarestorage.com`,
|
|
46
54
|
syncEncKey,
|
|
47
55
|
};
|
|
@@ -67,7 +67,6 @@ export declare const loadBeta: ModuleLoader;
|
|
|
67
67
|
export declare const loadSync: ModuleLoader;
|
|
68
68
|
export declare const loadLock: ModuleLoader;
|
|
69
69
|
export declare const loadRefreshRules: ModuleLoader;
|
|
70
|
-
export declare const loadDrive: ModuleLoader;
|
|
71
70
|
export declare const loadFactory: ModuleLoader;
|
|
72
71
|
export declare const loadUsage: ModuleLoader;
|
|
73
72
|
export declare const loadCost: ModuleLoader;
|
|
@@ -96,7 +95,6 @@ export declare const loadTeams: ModuleLoader;
|
|
|
96
95
|
export declare const loadCloud: ModuleLoader;
|
|
97
96
|
export declare const loadMessage: ModuleLoader;
|
|
98
97
|
export declare const loadSend: ModuleLoader;
|
|
99
|
-
export declare const loadHq: ModuleLoader;
|
|
100
98
|
export declare const loadFeed: ModuleLoader;
|
|
101
99
|
export declare const loadActivity: ModuleLoader;
|
|
102
100
|
export declare const loadMailboxes: ModuleLoader;
|
|
@@ -45,7 +45,6 @@ export const loadBeta = async () => (await import('../../commands/beta.js')).reg
|
|
|
45
45
|
export const loadSync = async () => (await import('../../commands/sync.js')).registerSyncCommand;
|
|
46
46
|
export const loadLock = async () => (await import('../../commands/lock.js')).registerLockCommand;
|
|
47
47
|
export const loadRefreshRules = async () => (await import('../../commands/refresh-rules.js')).registerRefreshRulesCommand;
|
|
48
|
-
export const loadDrive = async () => (await import('../../commands/drive.js')).registerDriveCommands;
|
|
49
48
|
export const loadFactory = async () => (await import('../../commands/factory.js')).registerFactoryCommands;
|
|
50
49
|
export const loadUsage = async () => (await import('../../commands/usage.js')).registerUsageCommand;
|
|
51
50
|
export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
|
|
@@ -74,7 +73,6 @@ export const loadTeams = async () => (await import('../../commands/teams.js')).r
|
|
|
74
73
|
export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
|
|
75
74
|
export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
|
|
76
75
|
export const loadSend = async () => (await import('../../commands/send.js')).registerSendCommand;
|
|
77
|
-
export const loadHq = async () => (await import('../../commands/hq.js')).registerHqCommand;
|
|
78
76
|
export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
|
|
79
77
|
export const loadActivity = async () => (await import('../../commands/activity.js')).registerActivityCommand;
|
|
80
78
|
export const loadMailboxes = async () => (await import('../../commands/mailboxes.js')).registerMailboxesCommand;
|
|
@@ -90,7 +88,7 @@ export const loadFunnel = async () => (await import('../../commands/funnel.js'))
|
|
|
90
88
|
* inherit the root's custom help formatter rather than getting the per-command
|
|
91
89
|
* recursive pass. Keeping that ordering preserves their `--help` output exactly.
|
|
92
90
|
*/
|
|
93
|
-
export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', '
|
|
91
|
+
export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', 'serve']);
|
|
94
92
|
/**
|
|
95
93
|
* User-typed top-level command name -> ordered list of module loaders to run.
|
|
96
94
|
*
|
|
@@ -161,7 +159,6 @@ export const COMMAND_LOADERS = {
|
|
|
161
159
|
sync: [loadSync],
|
|
162
160
|
lock: [loadLock],
|
|
163
161
|
'refresh-rules': [loadRefreshRules],
|
|
164
|
-
drive: [loadDrive],
|
|
165
162
|
factory: [loadFactory],
|
|
166
163
|
usage: [loadUsage],
|
|
167
164
|
cost: [loadCost],
|
|
@@ -199,7 +196,6 @@ export const COMMAND_LOADERS = {
|
|
|
199
196
|
message: [loadMessage],
|
|
200
197
|
send: [loadSend],
|
|
201
198
|
notify: [loadSend],
|
|
202
|
-
hq: [loadHq],
|
|
203
199
|
feed: [loadFeed],
|
|
204
200
|
activity: [loadActivity],
|
|
205
201
|
mailboxes: [loadMailboxes],
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
* and leaves a back-compat symlink at the old path.
|
|
24
24
|
*/
|
|
25
25
|
import type { Meta } from './types.js';
|
|
26
|
+
/**
|
|
27
|
+
* Header prepended to every agents.yaml the CLI writes (central and per-device
|
|
28
|
+
* docs). Carries the yaml-language-server schema hint so editors validate the
|
|
29
|
+
* file against `schema/agents-yaml.schema.json`. Exported so
|
|
30
|
+
* `lib/device-config.ts` writes a sibling device's doc with the same header.
|
|
31
|
+
*/
|
|
32
|
+
export declare const META_HEADER = "# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agents-cli/main/apps/cli/schema/agents-yaml.schema.json\n\n";
|
|
26
33
|
/** Root of the system data directory (~/.agents/.system/). */
|
|
27
34
|
export declare function getAgentsDir(): string;
|
|
28
35
|
/** Root of the system data directory (~/.agents/.system/). */
|
|
@@ -194,8 +201,6 @@ export declare function getSystemPluginsDir(): string;
|
|
|
194
201
|
export declare function getExtraPluginsDir(alias: string): string;
|
|
195
202
|
/** Path to a project-scoped plugins directory (<project>/.agents/plugins/), or null when none. */
|
|
196
203
|
export declare function getProjectPluginsDir(cwd?: string): string | null;
|
|
197
|
-
/** Path to synced remote session data (~/.agents/.cache/drive/). */
|
|
198
|
-
export declare function getDriveDir(): string;
|
|
199
204
|
/** Path to soft-deleted resources (~/.agents/.history/trash/). */
|
|
200
205
|
export declare function getTrashDir(): string;
|
|
201
206
|
/** Path to local session indexer storage (~/.agents/.history/sessions/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -26,7 +26,9 @@ import * as fs from 'fs';
|
|
|
26
26
|
import * as path from 'path';
|
|
27
27
|
import * as os from 'os';
|
|
28
28
|
import * as yaml from 'yaml';
|
|
29
|
+
import { execFileSync } from 'child_process';
|
|
29
30
|
import { ensureLockTarget, atomicWriteFileSync, withFileLock } from './fs-atomic.js';
|
|
31
|
+
import { DEFAULT_SYSTEM_REPO, systemRepoSlug } from './types.js';
|
|
30
32
|
import { machineId } from './machine-id.js';
|
|
31
33
|
const HOME = process.env.HOME ?? os.homedir();
|
|
32
34
|
/**
|
|
@@ -116,7 +118,6 @@ const PACKAGES_DIR = path.join(CACHE_DIR, 'packages');
|
|
|
116
118
|
// They live at the user-root so they're git-tracked as source of truth.
|
|
117
119
|
const PLUGINS_DIR = path.join(USER_AGENTS_DIR, 'plugins');
|
|
118
120
|
const CLOUD_DIR = path.join(CACHE_DIR, 'cloud');
|
|
119
|
-
const DRIVE_DIR = path.join(CACHE_DIR, 'drive');
|
|
120
121
|
const TERMINALS_DIR = path.join(CACHE_DIR, 'terminals');
|
|
121
122
|
const LOGS_DIR = path.join(CACHE_DIR, 'logs');
|
|
122
123
|
/** Disposable performance samples (~/.agents/.cache/perf/) — safe to wipe. */
|
|
@@ -144,9 +145,16 @@ const USER_SUBAGENTS_DIR = path.join(USER_AGENTS_DIR, 'subagents');
|
|
|
144
145
|
const USER_WORKFLOWS_DIR = path.join(USER_AGENTS_DIR, 'workflows');
|
|
145
146
|
const USER_SECRETS_DIR = path.join(USER_AGENTS_DIR, 'secrets');
|
|
146
147
|
const USER_PROMPTCUTS_FILE = path.join(USER_AGENTS_DIR, 'hooks', 'promptcuts.yaml');
|
|
147
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Header prepended to every agents.yaml the CLI writes (central and per-device
|
|
150
|
+
* docs). Carries the yaml-language-server schema hint so editors validate the
|
|
151
|
+
* file against `schema/agents-yaml.schema.json`. Exported so
|
|
152
|
+
* `lib/device-config.ts` writes a sibling device's doc with the same header.
|
|
153
|
+
*/
|
|
154
|
+
export const META_HEADER = `# agents-cli metadata
|
|
148
155
|
# Auto-generated - do not edit manually
|
|
149
156
|
# https://github.com/phnx-labs/agents-cli
|
|
157
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agents-cli/main/apps/cli/schema/agents-yaml.schema.json
|
|
150
158
|
|
|
151
159
|
`;
|
|
152
160
|
// ─── Root getters ─────────────────────────────────────────────────────────────
|
|
@@ -190,13 +198,69 @@ export function getOptionalUserAgentsDir() {
|
|
|
190
198
|
}
|
|
191
199
|
return USER_AGENTS_DIR;
|
|
192
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Origin `owner/repo` slug (lowercased, `.git` stripped) of a git checkout, or
|
|
203
|
+
* null when `dir` isn't a git repo / has no origin. Extracts the slug from any
|
|
204
|
+
* remote URL form: `git@host:owner/repo.git`, `https://host/owner/repo.git`,
|
|
205
|
+
* `ssh://git@host/owner/repo`. Sync (mirrors readGitConfigUser in git.ts) so it
|
|
206
|
+
* can be used from the synchronous getProjectAgentsDir walk.
|
|
207
|
+
*/
|
|
208
|
+
function gitOriginSlug(dir) {
|
|
209
|
+
let url;
|
|
210
|
+
try {
|
|
211
|
+
url = execFileSync('git', ['-C', dir, 'config', '--get', 'remote.origin.url'], {
|
|
212
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
213
|
+
}).toString().trim();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (!url)
|
|
219
|
+
return null;
|
|
220
|
+
const m = url.replace(/\.git$/i, '').match(/[:/]([^/:]+\/[^/]+)$/);
|
|
221
|
+
return m ? m[1].toLowerCase() : null;
|
|
222
|
+
}
|
|
223
|
+
/** Slugs of the user + system DotAgents repos — a checkout of any of these is not a project layer. */
|
|
224
|
+
function canonicalDotAgentsRepoSlugs() {
|
|
225
|
+
const slugs = new Set([systemRepoSlug(DEFAULT_SYSTEM_REPO).toLowerCase()]);
|
|
226
|
+
for (const dir of [USER_AGENTS_DIR, SYSTEM_AGENTS_DIR]) {
|
|
227
|
+
const slug = gitOriginSlug(dir);
|
|
228
|
+
if (slug)
|
|
229
|
+
slugs.add(slug);
|
|
230
|
+
}
|
|
231
|
+
return slugs;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* True when `agentsPath` is itself a git checkout of the user's or system's
|
|
235
|
+
* DotAgents repo — i.e. a *clone* of the very repo whose rules already load as
|
|
236
|
+
* the user/system layer (e.g. `git clone …/.agents.git ~/src/github.com/<you>/.agents`).
|
|
237
|
+
* Such a clone must NOT also be treated as a *project* layer: because project
|
|
238
|
+
* outranks user, a stale clone would silently shadow the live user rules by
|
|
239
|
+
* filename and plant a compiled AGENTS.md in an ancestor dir (RUSH-2037).
|
|
240
|
+
*
|
|
241
|
+
* A legitimate project layer is a plain subdirectory of a project and is never
|
|
242
|
+
* itself a git-repo root, so the cheap `.git` gate skips the (git-spawning)
|
|
243
|
+
* origin comparison for the common case — only a `.agents/` that is its own
|
|
244
|
+
* checkout pays it, and even then it's kept unless its origin matches the
|
|
245
|
+
* user/system DotAgents repo (an unrelated repo checked out at `.agents/`,
|
|
246
|
+
* or a project's own versioned `.agents/`, stays a valid project layer).
|
|
247
|
+
*/
|
|
248
|
+
function isUserOrSystemRepoCheckout(agentsPath) {
|
|
249
|
+
if (!fs.existsSync(path.join(agentsPath, '.git')))
|
|
250
|
+
return false;
|
|
251
|
+
const origin = gitOriginSlug(agentsPath);
|
|
252
|
+
if (!origin)
|
|
253
|
+
return false;
|
|
254
|
+
return canonicalDotAgentsRepoSlugs().has(origin);
|
|
255
|
+
}
|
|
193
256
|
/** Walk up from startPath to find a project-scoped .agents/ directory (skipping both roots). */
|
|
194
257
|
export function getProjectAgentsDir(startPath = process.cwd()) {
|
|
195
258
|
let dir = path.resolve(startPath);
|
|
196
259
|
while (true) {
|
|
197
260
|
const agentsPath = path.join(dir, '.agents');
|
|
198
261
|
if (fs.existsSync(agentsPath) && fs.statSync(agentsPath).isDirectory()) {
|
|
199
|
-
if (!isSamePath(agentsPath, SYSTEM_AGENTS_DIR) && !isSamePath(agentsPath, USER_AGENTS_DIR)
|
|
262
|
+
if (!isSamePath(agentsPath, SYSTEM_AGENTS_DIR) && !isSamePath(agentsPath, USER_AGENTS_DIR)
|
|
263
|
+
&& !isUserOrSystemRepoCheckout(agentsPath)) {
|
|
200
264
|
return agentsPath;
|
|
201
265
|
}
|
|
202
266
|
}
|
|
@@ -418,8 +482,6 @@ export function getProjectPluginsDir(cwd = process.cwd()) {
|
|
|
418
482
|
return null;
|
|
419
483
|
return path.join(projectAgentsDir, 'plugins');
|
|
420
484
|
}
|
|
421
|
-
/** Path to synced remote session data (~/.agents/.cache/drive/). */
|
|
422
|
-
export function getDriveDir() { return DRIVE_DIR; }
|
|
423
485
|
/** Path to soft-deleted resources (~/.agents/.history/trash/). */
|
|
424
486
|
export function getTrashDir() { return TRASH_DIR; }
|
|
425
487
|
/** Path to local session indexer storage (~/.agents/.history/sessions/). */
|
|
@@ -673,8 +735,9 @@ function writeIfChanged(filePath, content) {
|
|
|
673
735
|
/**
|
|
674
736
|
* Partition the in-memory Meta across three files by sync-domain:
|
|
675
737
|
* - central `~/.agents/agents.yaml` — portable, everything else
|
|
738
|
+
* (including the user-scope `config:` block)
|
|
676
739
|
* - device `~/.agents/devices/<machine>/agents.yaml` — `agents:` pins +
|
|
677
|
-
* `defaultBrowserProfile:` (
|
|
740
|
+
* `defaultBrowserProfile:` + device-scope `config:` (all per-device)
|
|
678
741
|
* - history `~/.agents/.history/version-resources.json` — `versions:` (machine-local)
|
|
679
742
|
* All callers funnel through writeMeta → here, so nothing else changes. Empty
|
|
680
743
|
* `agents:` / `versions:` are not written (no empty committed files).
|
|
@@ -736,7 +799,7 @@ function serializeCentral(central) {
|
|
|
736
799
|
return isEmpty ? META_HEADER : doc.toString({ collectionStyle: 'block' });
|
|
737
800
|
}
|
|
738
801
|
function writeMetaUnlocked(meta) {
|
|
739
|
-
const { agents, isolatedAgents, versions, defaultBrowserProfile, ...central } = meta;
|
|
802
|
+
const { agents, isolatedAgents, versions, defaultBrowserProfile, deviceConfig, ...central } = meta;
|
|
740
803
|
// Write the machine-local files FIRST, then strip central — so a crash mid-write
|
|
741
804
|
// never removes pins/versions from central before they're persisted elsewhere.
|
|
742
805
|
const devicePath = getDeviceMetaPath();
|
|
@@ -747,9 +810,13 @@ function writeMetaUnlocked(meta) {
|
|
|
747
810
|
// it does not have.
|
|
748
811
|
const hasIsolatedAgents = !!isolatedAgents && Object.keys(isolatedAgents).length > 0;
|
|
749
812
|
const hasDefaultBrowser = !!defaultBrowserProfile;
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
813
|
+
// Device-scope config (`maxAgents`, `schedulerEnabled`, …) is per-machine, so it
|
|
814
|
+
// rides the device doc under `config:` — never the central doc that syncs.
|
|
815
|
+
const hasDeviceConfig = !!deviceConfig && Object.keys(deviceConfig).length > 0;
|
|
816
|
+
if (hasAgents || hasIsolatedAgents || hasDefaultBrowser || hasDeviceConfig) {
|
|
817
|
+
// Device-local doc carries `agents:` pins, `defaultBrowserProfile:`, and the
|
|
818
|
+
// device-scope `config:` — all per-machine and must never land in central
|
|
819
|
+
// agents.yaml (which syncs).
|
|
753
820
|
const deviceDoc = {};
|
|
754
821
|
if (hasAgents)
|
|
755
822
|
deviceDoc.agents = agents;
|
|
@@ -757,6 +824,8 @@ function writeMetaUnlocked(meta) {
|
|
|
757
824
|
deviceDoc.isolatedAgents = isolatedAgents;
|
|
758
825
|
if (hasDefaultBrowser)
|
|
759
826
|
deviceDoc.defaultBrowserProfile = defaultBrowserProfile;
|
|
827
|
+
if (hasDeviceConfig)
|
|
828
|
+
deviceDoc.config = deviceConfig;
|
|
760
829
|
fs.mkdirSync(path.dirname(devicePath), { recursive: true });
|
|
761
830
|
writeIfChanged(devicePath, META_HEADER + yaml.stringify(deviceDoc));
|
|
762
831
|
}
|
|
@@ -782,6 +851,10 @@ function writeMetaUnlocked(meta) {
|
|
|
782
851
|
* still has pins)
|
|
783
852
|
* - `defaultBrowserProfile:` from the device file (device is the sole source;
|
|
784
853
|
* the field is stripped from central on write, so nothing to merge against)
|
|
854
|
+
* - `config:` from the device file into `deviceConfig` (device is the sole
|
|
855
|
+
* source for device-scope config, same routing as `defaultBrowserProfile`;
|
|
856
|
+
* the in-memory field is named `deviceConfig` so it can never collide with
|
|
857
|
+
* the user-scope `config:` central carries)
|
|
785
858
|
* - `versions:` from the history JSON (wholesale replace; falls back to
|
|
786
859
|
* whatever central carried when the history file doesn't exist yet)
|
|
787
860
|
*/
|
|
@@ -796,6 +869,8 @@ function overlayMachineLocal(meta) {
|
|
|
796
869
|
meta.isolatedAgents = { ...meta.isolatedAgents, ...dm.isolatedAgents };
|
|
797
870
|
if (dm?.defaultBrowserProfile)
|
|
798
871
|
meta.defaultBrowserProfile = dm.defaultBrowserProfile;
|
|
872
|
+
if (dm?.config)
|
|
873
|
+
meta.deviceConfig = dm.config;
|
|
799
874
|
}
|
|
800
875
|
catch { /* ignore malformed device file */ }
|
|
801
876
|
}
|
|
@@ -2,25 +2,23 @@
|
|
|
2
2
|
* Umbrella `agents sync` orchestration — "make this machine current".
|
|
3
3
|
*
|
|
4
4
|
* Bare `agents sync` fetches the config repos then reconciles them into every
|
|
5
|
-
* installed agent's version home. Secrets
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* installed agent's version home. Secrets are an opt-in stage (`--secrets`) —
|
|
6
|
+
* see `planUmbrellaStages` for why it's off by default. Each stage is an
|
|
7
|
+
* existing exported library function; this module only sequences them and
|
|
8
|
+
* decides — from the flags — which stages run. The planner is pure so the
|
|
9
|
+
* flag matrix is unit-tested without any I/O.
|
|
10
10
|
*
|
|
11
11
|
* Stage backends:
|
|
12
12
|
* repos -> git pull of ~/.agents + enabled ~/.agents-* extras (pullRepo)
|
|
13
13
|
* secrets -> listRemoteBundles + pullBundle (needs a passphrase; skipped
|
|
14
14
|
* cleanly when none is available — tokenized non-interactive auth
|
|
15
15
|
* arrives with `agents login`, #366/#367)
|
|
16
|
-
* sessions -> syncSessions(), gated by the session-sync beta opt-in + isSyncConfigured(), like the daemon
|
|
17
16
|
* reconcile-> refresh({ skipPrompts }) — re-materialize resources into homes
|
|
18
17
|
*/
|
|
19
|
-
/** The
|
|
18
|
+
/** The umbrella flags off `agents sync`. */
|
|
20
19
|
export interface UmbrellaFlags {
|
|
21
20
|
repos?: boolean;
|
|
22
21
|
secrets?: boolean;
|
|
23
|
-
sessions?: boolean;
|
|
24
22
|
cloud?: boolean;
|
|
25
23
|
local?: boolean;
|
|
26
24
|
}
|
|
@@ -28,7 +26,6 @@ export interface UmbrellaFlags {
|
|
|
28
26
|
export interface UmbrellaPlan {
|
|
29
27
|
fetchRepos: boolean;
|
|
30
28
|
fetchSecrets: boolean;
|
|
31
|
-
fetchSessions: boolean;
|
|
32
29
|
reconcile: boolean;
|
|
33
30
|
}
|
|
34
31
|
/**
|
|
@@ -36,14 +33,12 @@ export interface UmbrellaPlan {
|
|
|
36
33
|
* bare (no flags) fetch repos, then reconcile
|
|
37
34
|
* --local reconcile only, no fetch
|
|
38
35
|
* --cloud fetch repos (or the selected subset), skip reconcile
|
|
39
|
-
* --repos/--secrets
|
|
36
|
+
* --repos/--secrets fetch only the selected types, then reconcile
|
|
40
37
|
* `--local` wins over everything; `--cloud` suppresses reconcile.
|
|
41
38
|
*
|
|
42
|
-
* Secrets
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* default, and session transcripts are queryable on demand (`agents sessions
|
|
46
|
-
* --host <machine>`) so they don't need eager mirroring.
|
|
39
|
+
* Secrets are NOT part of the bare default — they are opt-in via `--secrets`.
|
|
40
|
+
* Pulling every secret bundle onto the machine on a bare `agents sync` is more
|
|
41
|
+
* blast radius than the verb should carry by default.
|
|
47
42
|
*/
|
|
48
43
|
export declare function planUmbrellaStages(f: UmbrellaFlags): UmbrellaPlan;
|
|
49
44
|
export interface UmbrellaResult {
|
|
@@ -58,13 +53,6 @@ export interface UmbrellaResult {
|
|
|
58
53
|
reason?: string;
|
|
59
54
|
errors: string[];
|
|
60
55
|
};
|
|
61
|
-
sessions?: {
|
|
62
|
-
ran: boolean;
|
|
63
|
-
pushed: number;
|
|
64
|
-
pulled: number;
|
|
65
|
-
merged: number;
|
|
66
|
-
error?: string;
|
|
67
|
-
};
|
|
68
56
|
devices?: {
|
|
69
57
|
synced: number;
|
|
70
58
|
pending: number;
|
|
@@ -82,7 +70,7 @@ export interface RunUmbrellaArgs {
|
|
|
82
70
|
passphrase?: string;
|
|
83
71
|
}
|
|
84
72
|
/**
|
|
85
|
-
* Execute the planned stages in order: repos -> secrets ->
|
|
73
|
+
* Execute the planned stages in order: repos -> secrets -> reconcile.
|
|
86
74
|
* A failure in one fetch stage is recorded and does not abort the others or the
|
|
87
75
|
* reconcile — `agents sync` should make as much current as it can in one pass.
|
|
88
76
|
*/
|