@phnx-labs/agents-cli 1.20.71 → 1.20.73
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 +325 -1
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/commands.js +2 -0
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +39 -10
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +8 -0
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +26 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +3 -1
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/skills.js +2 -0
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/status.js +8 -2
- package/dist/commands/subagents.js +3 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/commands/uninstall.d.ts +11 -0
- package/dist/commands/uninstall.js +158 -0
- package/dist/commands/utils.d.ts +29 -0
- package/dist/commands/utils.js +24 -0
- package/dist/commands/versions.js +120 -11
- package/dist/index.js +83 -12
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.js +16 -5
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +14 -5
- package/dist/lib/daemon.js +52 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/logs.d.ts +12 -0
- package/dist/lib/hosts/logs.js +18 -0
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +291 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +36 -0
- package/dist/lib/shims.js +120 -16
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +19 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +73 -0
- package/dist/lib/uninstall.d.ts +84 -0
- package/dist/lib/uninstall.js +347 -0
- package/dist/lib/usage.d.ts +13 -1
- package/dist/lib/usage.js +32 -14
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +27 -0
- package/dist/lib/versions.js +248 -79
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
|
@@ -103,10 +103,41 @@ export function inferLeaseRuntime(agentName, detected) {
|
|
|
103
103
|
// Custom/workflow agent: fall back to the signed-in runtime (preferring claude).
|
|
104
104
|
return signedIn.find((d) => d.id === 'claude')?.id ?? signedIn[0]?.id ?? null;
|
|
105
105
|
}
|
|
106
|
+
const PROFILE_AUTH_ENV_KEYS_BY_RUNTIME = {
|
|
107
|
+
claude: [
|
|
108
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
109
|
+
'ANTHROPIC_API_KEY',
|
|
110
|
+
'AWS_ACCESS_KEY_ID',
|
|
111
|
+
'AWS_PROFILE',
|
|
112
|
+
'AWS_BEARER_TOKEN_BEDROCK',
|
|
113
|
+
'GOOGLE_APPLICATION_CREDENTIALS',
|
|
114
|
+
'GOOGLE_CLOUD_PROJECT',
|
|
115
|
+
'ANTHROPIC_FOUNDRY_API_KEY',
|
|
116
|
+
],
|
|
117
|
+
codex: ['OPENAI_API_KEY'],
|
|
118
|
+
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
119
|
+
grok: ['XAI_API_KEY', 'GROK_API_KEY'],
|
|
120
|
+
};
|
|
121
|
+
/** True when a profile already carries auth for its host runtime via env. */
|
|
122
|
+
export function profileNeedsBaseRuntimeCredentials(agent, env, authEnvVar) {
|
|
123
|
+
if (!LEASE_RUNTIMES.some((c) => c.id === agent))
|
|
124
|
+
return false;
|
|
125
|
+
if (authEnvVar && typeof env[authEnvVar] === 'string' && env[authEnvVar].trim() !== '')
|
|
126
|
+
return false;
|
|
127
|
+
const keys = PROFILE_AUTH_ENV_KEYS_BY_RUNTIME[agent] ?? [];
|
|
128
|
+
return !keys.some((key) => typeof env[key] === 'string' && env[key].trim() !== '');
|
|
129
|
+
}
|
|
106
130
|
// A long random sentinel makes an accidental (or malicious) collision with a
|
|
107
131
|
// token's contents effectively impossible, so the quoted heredoc can never be
|
|
108
132
|
// closed early by the credential body.
|
|
109
133
|
const CRED_EOF = 'AGENTS_LEASE_CRED_EOF_9f3c1a7b5e2d4068';
|
|
134
|
+
/** Build a quoted heredoc write to a path under the remote user's home. */
|
|
135
|
+
export function buildHomeFileWriteScript(remote, contents) {
|
|
136
|
+
const dir = path.posix.dirname(remote);
|
|
137
|
+
const mkdir = dir && dir !== '.' ? `mkdir -p "$HOME/${dir}"\n` : '';
|
|
138
|
+
return (`${mkdir}cat > "$HOME/${remote}" <<'${CRED_EOF}'\n${contents}${contents.endsWith('\n') ? '' : '\n'}${CRED_EOF}\n` +
|
|
139
|
+
`chmod 600 "$HOME/${remote}"`);
|
|
140
|
+
}
|
|
110
141
|
/**
|
|
111
142
|
* Where Claude Code reads its OAuth token on the box. `.claude.json` (the file
|
|
112
143
|
* LEASE_RUNTIMES copies) is config/account-metadata ONLY — the actual token
|
|
@@ -206,12 +237,6 @@ export async function resolveClaudeCredentialsBlob(opts) {
|
|
|
206
237
|
export function buildCredentialScript(picked, detected, extras) {
|
|
207
238
|
const byId = new Map(detected.map((d) => [d.id, d]));
|
|
208
239
|
const parts = [];
|
|
209
|
-
const writeFile = (remote, contents) => {
|
|
210
|
-
const dir = path.posix.dirname(remote);
|
|
211
|
-
const mkdir = dir && dir !== '.' ? `mkdir -p "$HOME/${dir}"\n` : '';
|
|
212
|
-
return (`${mkdir}cat > "$HOME/${remote}" <<'${CRED_EOF}'\n${contents}${contents.endsWith('\n') ? '' : '\n'}${CRED_EOF}\n` +
|
|
213
|
-
`chmod 600 "$HOME/${remote}"`);
|
|
214
|
-
};
|
|
215
240
|
for (const id of picked) {
|
|
216
241
|
const d = byId.get(id);
|
|
217
242
|
const cred = LEASE_RUNTIMES.find((c) => c.id === id);
|
|
@@ -224,11 +249,11 @@ export function buildCredentialScript(picked, detected, extras) {
|
|
|
224
249
|
catch {
|
|
225
250
|
continue;
|
|
226
251
|
}
|
|
227
|
-
parts.push(
|
|
252
|
+
parts.push(buildHomeFileWriteScript(cred.remote, contents));
|
|
228
253
|
// For claude the file above is config/state only; the OAuth token is a
|
|
229
254
|
// second artifact — without it the box comes up "Not logged in".
|
|
230
255
|
if (id === 'claude' && extras?.claudeCredentialsJson) {
|
|
231
|
-
parts.push(
|
|
256
|
+
parts.push(buildHomeFileWriteScript(CLAUDE_TOKEN_REMOTE, extras.claudeCredentialsJson));
|
|
232
257
|
}
|
|
233
258
|
}
|
|
234
259
|
return parts.join('\n');
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup-copy for `agents run --lease` (RUSH-1920), transport = PUSH-FROM-LOCAL.
|
|
3
|
+
*
|
|
4
|
+
* A fresh crabbox box has agents-cli installed but none of the caller's own
|
|
5
|
+
* `~/.agents` config (skills, hooks, commands, MCP, profiles). This module
|
|
6
|
+
* replicates the *git-tracked* subset of the LOCAL `~/.agents` onto the box:
|
|
7
|
+
*
|
|
8
|
+
* 1. `git -C <userAgentsDir> ls-files` — tracked files only. Tracked-only is
|
|
9
|
+
* the safety boundary: it excludes `.history/`, `.cache/`, `.system/` and
|
|
10
|
+
* any keychain-backed secrets by construction (those are gitignored), so no
|
|
11
|
+
* credential material is ever pushed.
|
|
12
|
+
* 2. `rsync` that exact file set to `~/.agents` on the box over crabbox's OWN
|
|
13
|
+
* ssh invocation (`crabboxSshArgv`) — crabbox provisions a per-lease identity
|
|
14
|
+
* key, so a raw `ssh crabbox@ip` fails publickey; only crabbox's key works.
|
|
15
|
+
* 3. `agents repo refresh` on the box so the copied config takes effect.
|
|
16
|
+
*
|
|
17
|
+
* NEVER copies `~/.claude` / `~/.claude.json` — they live in `$HOME`, not
|
|
18
|
+
* `~/.agents`, and are rebuilt on the box by `agents add` + refresh; an explicit
|
|
19
|
+
* filter belts-and-braces the tracked-only guarantee.
|
|
20
|
+
*
|
|
21
|
+
* This is a self-contained local function the command layer calls; it does NOT
|
|
22
|
+
* ride the box-side bootstrap script (that only echoes the `copy-setup` progress
|
|
23
|
+
* sentinel — see `buildBootstrapScript`).
|
|
24
|
+
*/
|
|
25
|
+
/** Remote path (relative to the box user's home) the tracked config lands in. */
|
|
26
|
+
export declare const REMOTE_AGENTS_DIR = ".agents/";
|
|
27
|
+
export interface CopySetupOptions {
|
|
28
|
+
/** crabbox box slug to push to — its per-lease ssh key does the auth. */
|
|
29
|
+
slug: string;
|
|
30
|
+
/** Secrets bundle whose env the ssh/rsync children inherit (crabbox parity). */
|
|
31
|
+
secretsBundle?: string;
|
|
32
|
+
/** Override the local `~/.agents` dir (defaults to `getUserAgentsDir()`). */
|
|
33
|
+
userAgentsDir?: string;
|
|
34
|
+
/** Receives combined stdout/stderr of the rsync + refresh, if set. */
|
|
35
|
+
onData?: (chunk: string) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Run `agents repo refresh` on the box after the push (default `true`). Set
|
|
38
|
+
* `false` when the caller runs the refresh itself in the box bootstrap — the
|
|
39
|
+
* lease path does this so the refresh runs AFTER the box installs agents-cli
|
|
40
|
+
* (this host-side push happens before the box boots agents-cli, so a host-side
|
|
41
|
+
* refresh here could not find the CLI).
|
|
42
|
+
*/
|
|
43
|
+
refresh?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface CopySetupResult {
|
|
46
|
+
/** The tracked files enumerated for the push (post-exclusion). */
|
|
47
|
+
files: string[];
|
|
48
|
+
/** rsync exit code, or null when the process failed to spawn. */
|
|
49
|
+
pushExitCode: number | null;
|
|
50
|
+
/** `agents repo refresh` exit code, or null when it was skipped/failed to spawn. */
|
|
51
|
+
refreshExitCode: number | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* git-tracked files under `dir` (paths relative to `dir`), minus the never-copy
|
|
55
|
+
* set. Returns `[]` when `dir` is not a git repo — a caller with no tracked
|
|
56
|
+
* `~/.agents` simply copies nothing.
|
|
57
|
+
*/
|
|
58
|
+
export declare function enumerateTrackedFiles(dir: string): string[];
|
|
59
|
+
/**
|
|
60
|
+
* Split crabbox's ssh argv (`['ssh', …opts, 'crabbox@host']`) into the `-e`
|
|
61
|
+
* transport string (`ssh …opts`) and the `crabbox@host` endpoint. This is what
|
|
62
|
+
* carries the per-lease identity key + known_hosts a raw ssh lacks.
|
|
63
|
+
*/
|
|
64
|
+
export declare function sshTransportFromArgv(sshArgv: string[]): {
|
|
65
|
+
rsh: string;
|
|
66
|
+
host: string;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* rsync argv to push the tracked file set to `~/.agents` on the box. Reads the
|
|
70
|
+
* NUL-separated list at `filesFrom` (`--from0`, matching `ls-files -z`) so paths
|
|
71
|
+
* with spaces survive, and tunnels over crabbox's own ssh (`rsh`).
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildSetupRsyncArgs(opts: {
|
|
74
|
+
rsh: string;
|
|
75
|
+
host: string;
|
|
76
|
+
filesFrom: string;
|
|
77
|
+
source: string;
|
|
78
|
+
remoteDir?: string;
|
|
79
|
+
}): string[];
|
|
80
|
+
/**
|
|
81
|
+
* Replicate the git-tracked subset of the local `~/.agents` onto the crabbox box
|
|
82
|
+
* and refresh it. Enumerates tracked files, rsyncs them over ssh, then runs
|
|
83
|
+
* `agents repo refresh` on the box. Refresh is skipped when the push fails or the
|
|
84
|
+
* file set is empty (nothing to refresh). Never throws — surfaces failure through
|
|
85
|
+
* the returned exit codes.
|
|
86
|
+
*/
|
|
87
|
+
export declare function copySetupToBox(opts: CopySetupOptions): Promise<CopySetupResult>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup-copy for `agents run --lease` (RUSH-1920), transport = PUSH-FROM-LOCAL.
|
|
3
|
+
*
|
|
4
|
+
* A fresh crabbox box has agents-cli installed but none of the caller's own
|
|
5
|
+
* `~/.agents` config (skills, hooks, commands, MCP, profiles). This module
|
|
6
|
+
* replicates the *git-tracked* subset of the LOCAL `~/.agents` onto the box:
|
|
7
|
+
*
|
|
8
|
+
* 1. `git -C <userAgentsDir> ls-files` — tracked files only. Tracked-only is
|
|
9
|
+
* the safety boundary: it excludes `.history/`, `.cache/`, `.system/` and
|
|
10
|
+
* any keychain-backed secrets by construction (those are gitignored), so no
|
|
11
|
+
* credential material is ever pushed.
|
|
12
|
+
* 2. `rsync` that exact file set to `~/.agents` on the box over crabbox's OWN
|
|
13
|
+
* ssh invocation (`crabboxSshArgv`) — crabbox provisions a per-lease identity
|
|
14
|
+
* key, so a raw `ssh crabbox@ip` fails publickey; only crabbox's key works.
|
|
15
|
+
* 3. `agents repo refresh` on the box so the copied config takes effect.
|
|
16
|
+
*
|
|
17
|
+
* NEVER copies `~/.claude` / `~/.claude.json` — they live in `$HOME`, not
|
|
18
|
+
* `~/.agents`, and are rebuilt on the box by `agents add` + refresh; an explicit
|
|
19
|
+
* filter belts-and-braces the tracked-only guarantee.
|
|
20
|
+
*
|
|
21
|
+
* This is a self-contained local function the command layer calls; it does NOT
|
|
22
|
+
* ride the box-side bootstrap script (that only echoes the `copy-setup` progress
|
|
23
|
+
* sentinel — see `buildBootstrapScript`).
|
|
24
|
+
*/
|
|
25
|
+
import { spawn, spawnSync } from 'child_process';
|
|
26
|
+
import * as fs from 'fs';
|
|
27
|
+
import * as os from 'os';
|
|
28
|
+
import * as path from 'path';
|
|
29
|
+
import { getUserAgentsDir } from '../state.js';
|
|
30
|
+
import { crabboxEnv, crabboxSshArgv } from './cli.js';
|
|
31
|
+
/** Remote path (relative to the box user's home) the tracked config lands in. */
|
|
32
|
+
export const REMOTE_AGENTS_DIR = '.agents/';
|
|
33
|
+
/** Top-level paths that must never be pushed, even if somehow tracked. */
|
|
34
|
+
const NEVER_COPY = new Set(['.claude', '.claude.json']);
|
|
35
|
+
/**
|
|
36
|
+
* git-tracked files under `dir` (paths relative to `dir`), minus the never-copy
|
|
37
|
+
* set. Returns `[]` when `dir` is not a git repo — a caller with no tracked
|
|
38
|
+
* `~/.agents` simply copies nothing.
|
|
39
|
+
*/
|
|
40
|
+
export function enumerateTrackedFiles(dir) {
|
|
41
|
+
const r = spawnSync('git', ['-C', dir, 'ls-files', '-z'], { encoding: 'utf-8' });
|
|
42
|
+
if (r.status !== 0 || !r.stdout)
|
|
43
|
+
return [];
|
|
44
|
+
return r.stdout
|
|
45
|
+
.split('\0')
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.filter((f) => {
|
|
48
|
+
const top = f.split('/')[0];
|
|
49
|
+
return !NEVER_COPY.has(f) && !NEVER_COPY.has(top);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Split crabbox's ssh argv (`['ssh', …opts, 'crabbox@host']`) into the `-e`
|
|
54
|
+
* transport string (`ssh …opts`) and the `crabbox@host` endpoint. This is what
|
|
55
|
+
* carries the per-lease identity key + known_hosts a raw ssh lacks.
|
|
56
|
+
*/
|
|
57
|
+
export function sshTransportFromArgv(sshArgv) {
|
|
58
|
+
const host = sshArgv[sshArgv.length - 1];
|
|
59
|
+
const rsh = sshArgv.slice(0, -1).join(' '); // 'ssh -i <key> -o … -p 2222'
|
|
60
|
+
return { rsh, host };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* rsync argv to push the tracked file set to `~/.agents` on the box. Reads the
|
|
64
|
+
* NUL-separated list at `filesFrom` (`--from0`, matching `ls-files -z`) so paths
|
|
65
|
+
* with spaces survive, and tunnels over crabbox's own ssh (`rsh`).
|
|
66
|
+
*/
|
|
67
|
+
export function buildSetupRsyncArgs(opts) {
|
|
68
|
+
const remote = `${opts.host}:${opts.remoteDir ?? REMOTE_AGENTS_DIR}`;
|
|
69
|
+
const source = opts.source.endsWith('/') ? opts.source : `${opts.source}/`;
|
|
70
|
+
return ['-az', '--files-from', opts.filesFrom, '--from0', '-e', opts.rsh, source, remote];
|
|
71
|
+
}
|
|
72
|
+
function runStreaming(cmd, args, env, onData) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const proc = spawn(cmd, args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
75
|
+
const pump = (chunk) => {
|
|
76
|
+
const s = chunk.toString('utf-8');
|
|
77
|
+
if (onData)
|
|
78
|
+
onData(s);
|
|
79
|
+
else
|
|
80
|
+
process.stdout.write(s);
|
|
81
|
+
};
|
|
82
|
+
proc.stdout.on('data', pump);
|
|
83
|
+
proc.stderr.on('data', pump);
|
|
84
|
+
proc.on('error', () => resolve(null));
|
|
85
|
+
proc.on('close', (code) => resolve(code));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Replicate the git-tracked subset of the local `~/.agents` onto the crabbox box
|
|
90
|
+
* and refresh it. Enumerates tracked files, rsyncs them over ssh, then runs
|
|
91
|
+
* `agents repo refresh` on the box. Refresh is skipped when the push fails or the
|
|
92
|
+
* file set is empty (nothing to refresh). Never throws — surfaces failure through
|
|
93
|
+
* the returned exit codes.
|
|
94
|
+
*/
|
|
95
|
+
export async function copySetupToBox(opts) {
|
|
96
|
+
const dir = opts.userAgentsDir ?? getUserAgentsDir();
|
|
97
|
+
const files = enumerateTrackedFiles(dir);
|
|
98
|
+
if (files.length === 0) {
|
|
99
|
+
return { files, pushExitCode: null, refreshExitCode: null };
|
|
100
|
+
}
|
|
101
|
+
// crabbox provisions a per-lease ssh key; a raw `ssh crabbox@ip` fails publickey.
|
|
102
|
+
// Ask crabbox for its exact ssh invocation and tunnel rsync through it.
|
|
103
|
+
const sshArgv = crabboxSshArgv(opts.slug, { secretsBundle: opts.secretsBundle });
|
|
104
|
+
if (!sshArgv) {
|
|
105
|
+
return { files, pushExitCode: null, refreshExitCode: null };
|
|
106
|
+
}
|
|
107
|
+
const { rsh, host } = sshTransportFromArgv(sshArgv);
|
|
108
|
+
const env = crabboxEnv({ secretsBundle: opts.secretsBundle });
|
|
109
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-setup-copy-'));
|
|
110
|
+
const listPath = path.join(tmp, 'files.lst');
|
|
111
|
+
try {
|
|
112
|
+
// NUL-separated list, matching `buildSetupRsyncArgs`'s `--from0`.
|
|
113
|
+
fs.writeFileSync(listPath, files.join('\0'), 'utf-8');
|
|
114
|
+
const rsyncArgs = buildSetupRsyncArgs({ rsh, host, filesFrom: listPath, source: dir });
|
|
115
|
+
const pushExitCode = await runStreaming('rsync', rsyncArgs, env, opts.onData);
|
|
116
|
+
let refreshExitCode = null;
|
|
117
|
+
if (pushExitCode === 0 && opts.refresh !== false) {
|
|
118
|
+
// ssh <opts> crabbox@host bash -lc 'agents repo refresh'
|
|
119
|
+
const refreshArgs = [...sshArgv.slice(1), 'bash', '-lc', 'agents repo refresh'];
|
|
120
|
+
refreshExitCode = await runStreaming('ssh', refreshArgs, env, opts.onData);
|
|
121
|
+
}
|
|
122
|
+
return { files, pushExitCode, refreshExitCode };
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
* log output, reload (SIGHUP), and graceful shutdown are handled here.
|
|
8
8
|
*/
|
|
9
9
|
import { getAgentsBinPath } from './cli-entry.js';
|
|
10
|
+
/**
|
|
11
|
+
* RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
|
|
12
|
+
* broker. The startup host decision is one-shot; this drives the periodic
|
|
13
|
+
* self-heal re-check. Take over ONLY when the daemon is not already hosting AND
|
|
14
|
+
* no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
|
|
15
|
+
* start has since died or crash-looped. Never take over while our in-process
|
|
16
|
+
* broker is hosting, and never clobber a reachable (healthy) broker.
|
|
17
|
+
*/
|
|
18
|
+
export declare function shouldTakeOverBroker(isHosting: boolean, brokerReachable: boolean): boolean;
|
|
10
19
|
/** Read the stored daemon PID from disk. Returns null if not present or invalid. */
|
|
11
20
|
export declare function readDaemonPid(): number | null;
|
|
12
21
|
/** Write the daemon PID to the pid file. */
|
|
@@ -24,9 +33,9 @@ export declare function isDaemonWedged(): boolean;
|
|
|
24
33
|
/** Check if the daemon process is alive by sending signal 0 to the stored PID. */
|
|
25
34
|
export declare function isDaemonRunning(): boolean;
|
|
26
35
|
/**
|
|
27
|
-
* Single-instance claim for the daemon
|
|
36
|
+
* Single-instance claim for the daemon foreground entrypoint.
|
|
28
37
|
*
|
|
29
|
-
* `agents
|
|
38
|
+
* `agents __daemon-run` is reachable directly — a manual invocation, or a
|
|
30
39
|
* service-manager restart that races a still-alive predecessor — bypassing the
|
|
31
40
|
* start lock in startDaemon(). Without this guard runDaemon() would call
|
|
32
41
|
* writeDaemonPid() unconditionally, clobber a live daemon's recorded PID, and
|
|
@@ -40,7 +49,7 @@ export declare function isDaemonRunning(): boolean;
|
|
|
40
49
|
*/
|
|
41
50
|
export declare function claimDaemonInstance(): boolean;
|
|
42
51
|
/**
|
|
43
|
-
* Reap stray duplicate daemon processes — a `
|
|
52
|
+
* Reap stray duplicate daemon processes — a `__daemon-run` of THIS install that
|
|
44
53
|
* isn't this process and isn't the pid-file owner. Mirrors the browser orphan
|
|
45
54
|
* reaper (below): a predecessor that was SIGKILLed/OOM-ed without cleaning up,
|
|
46
55
|
* or a duplicate that lost the pid-file write race, would otherwise keep a
|
|
@@ -132,8 +141,8 @@ export declare function ensureDaemonStarted(): {
|
|
|
132
141
|
*/
|
|
133
142
|
export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv, oauthToken?: string | null): NodeJS.ProcessEnv;
|
|
134
143
|
/**
|
|
135
|
-
* Resolve how to launch the daemon: `node <entry>
|
|
136
|
-
* exact form that works under a direct `
|
|
144
|
+
* Resolve how to launch the daemon: `node <entry> __daemon-run`, matching the
|
|
145
|
+
* exact form that works under a direct `__daemon-run`.
|
|
137
146
|
*
|
|
138
147
|
* We spawn the Node runtime (`process.execPath`) with the CLI entry as an
|
|
139
148
|
* argument rather than executing the entry path directly. Executing the `.js`
|
package/dist/lib/daemon.js
CHANGED
|
@@ -38,6 +38,17 @@ const WEDGE_THRESHOLD_TICKS = 3;
|
|
|
38
38
|
// session (which expires between runs and produces intermittent 401s).
|
|
39
39
|
const DAEMON_OAUTH_BUNDLE = 'claude';
|
|
40
40
|
const DAEMON_OAUTH_KEY = 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
41
|
+
/**
|
|
42
|
+
* RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
|
|
43
|
+
* broker. The startup host decision is one-shot; this drives the periodic
|
|
44
|
+
* self-heal re-check. Take over ONLY when the daemon is not already hosting AND
|
|
45
|
+
* no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
|
|
46
|
+
* start has since died or crash-looped. Never take over while our in-process
|
|
47
|
+
* broker is hosting, and never clobber a reachable (healthy) broker.
|
|
48
|
+
*/
|
|
49
|
+
export function shouldTakeOverBroker(isHosting, brokerReachable) {
|
|
50
|
+
return !isHosting && !brokerReachable;
|
|
51
|
+
}
|
|
41
52
|
function getDaemonDir() {
|
|
42
53
|
const dir = getDaemonDirRoot();
|
|
43
54
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -176,9 +187,9 @@ export function isDaemonRunning() {
|
|
|
176
187
|
return false;
|
|
177
188
|
}
|
|
178
189
|
/**
|
|
179
|
-
* Single-instance claim for the daemon
|
|
190
|
+
* Single-instance claim for the daemon foreground entrypoint.
|
|
180
191
|
*
|
|
181
|
-
* `agents
|
|
192
|
+
* `agents __daemon-run` is reachable directly — a manual invocation, or a
|
|
182
193
|
* service-manager restart that races a still-alive predecessor — bypassing the
|
|
183
194
|
* start lock in startDaemon(). Without this guard runDaemon() would call
|
|
184
195
|
* writeDaemonPid() unconditionally, clobber a live daemon's recorded PID, and
|
|
@@ -205,7 +216,7 @@ export function claimDaemonInstance() {
|
|
|
205
216
|
}
|
|
206
217
|
}
|
|
207
218
|
/**
|
|
208
|
-
* Reap stray duplicate daemon processes — a `
|
|
219
|
+
* Reap stray duplicate daemon processes — a `__daemon-run` of THIS install that
|
|
209
220
|
* isn't this process and isn't the pid-file owner. Mirrors the browser orphan
|
|
210
221
|
* reaper (below): a predecessor that was SIGKILLed/OOM-ed without cleaning up,
|
|
211
222
|
* or a duplicate that lost the pid-file write race, would otherwise keep a
|
|
@@ -241,10 +252,10 @@ export function reapStrayDaemons(keepPid = process.pid) {
|
|
|
241
252
|
const args = m[2];
|
|
242
253
|
if (isNaN(pid) || pid === keepPid || pid === process.pid || pid === ownerPid)
|
|
243
254
|
continue;
|
|
244
|
-
// Same install (same launch entry) AND a `
|
|
255
|
+
// Same install (same launch entry) AND a `__daemon-run` command line.
|
|
245
256
|
if (!args.includes(selfEntry))
|
|
246
257
|
continue;
|
|
247
|
-
if (!/\
|
|
258
|
+
if (!/\b__daemon-run\b/.test(args))
|
|
248
259
|
continue;
|
|
249
260
|
try {
|
|
250
261
|
process.kill(pid, 'SIGTERM');
|
|
@@ -284,7 +295,7 @@ export function log(level, message) {
|
|
|
284
295
|
}
|
|
285
296
|
/** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
|
|
286
297
|
export async function runDaemon() {
|
|
287
|
-
// Single-instance guard: a direct `agents
|
|
298
|
+
// Single-instance guard: a direct `agents __daemon-run` (manual, or a
|
|
288
299
|
// service-manager restart racing a live predecessor) must not clobber a
|
|
289
300
|
// running daemon's pid file and start a second scheduler.
|
|
290
301
|
if (!claimDaemonInstance()) {
|
|
@@ -661,6 +672,37 @@ export async function runDaemon() {
|
|
|
661
672
|
};
|
|
662
673
|
const fleetCacheInterval = setInterval(() => { void runFleetCacheWarm(); }, 3 * 60_000);
|
|
663
674
|
const fleetCacheKickoff = setTimeout(() => { void runFleetCacheWarm(); }, 60_000);
|
|
675
|
+
// RUSH-1817: the startup host decision above is one-shot. If a standalone
|
|
676
|
+
// broker answered agentPing() at daemon start, the daemon declined to host —
|
|
677
|
+
// but should that standalone later die or crash-loop, nothing takes over and
|
|
678
|
+
// every `agents secrets unlock|export|start` fails until a manual restart
|
|
679
|
+
// (this wedged all keychain-backed secrets on zion and blocked a release).
|
|
680
|
+
// Re-probe on a cadence: whenever the daemon is NOT itself hosting AND no
|
|
681
|
+
// healthy broker answers a ping, take over hosting. startHostedBroker binds
|
|
682
|
+
// the socket only when it is free, so a take-over never races a live broker.
|
|
683
|
+
let selfHealingBroker = false;
|
|
684
|
+
const runBrokerSelfHeal = async () => {
|
|
685
|
+
if (selfHealingBroker)
|
|
686
|
+
return;
|
|
687
|
+
selfHealingBroker = true;
|
|
688
|
+
try {
|
|
689
|
+
const { agentPing, startHostedBroker } = await import('./secrets/agent.js');
|
|
690
|
+
const reachable = (await agentPing()).reachable;
|
|
691
|
+
if (!shouldTakeOverBroker(hostedBroker != null, reachable))
|
|
692
|
+
return;
|
|
693
|
+
hostedBroker = await startHostedBroker();
|
|
694
|
+
if (hostedBroker) {
|
|
695
|
+
log('WARN', 'Secrets broker was unreachable; daemon took over hosting (self-heal)');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
log('WARN', `Secrets broker self-heal skipped: ${err.message}`);
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
selfHealingBroker = false;
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
const brokerSelfHealInterval = setInterval(() => { void runBrokerSelfHeal(); }, 60_000);
|
|
664
706
|
const handleReload = () => {
|
|
665
707
|
log('INFO', 'Reloading jobs (SIGHUP)');
|
|
666
708
|
scheduler.reloadAll();
|
|
@@ -695,6 +737,7 @@ export async function runDaemon() {
|
|
|
695
737
|
clearTimeout(launchHealthKickoff);
|
|
696
738
|
clearInterval(fleetCacheInterval);
|
|
697
739
|
clearTimeout(fleetCacheKickoff);
|
|
740
|
+
clearInterval(brokerSelfHealInterval);
|
|
698
741
|
hostedBroker?.close();
|
|
699
742
|
removeDaemonPid();
|
|
700
743
|
removeHeartbeat();
|
|
@@ -962,8 +1005,8 @@ export function buildDetachedDaemonEnv(baseEnv = process.env, oauthToken = readD
|
|
|
962
1005
|
return env;
|
|
963
1006
|
}
|
|
964
1007
|
/**
|
|
965
|
-
* Resolve how to launch the daemon: `node <entry>
|
|
966
|
-
* exact form that works under a direct `
|
|
1008
|
+
* Resolve how to launch the daemon: `node <entry> __daemon-run`, matching the
|
|
1009
|
+
* exact form that works under a direct `__daemon-run`.
|
|
967
1010
|
*
|
|
968
1011
|
* We spawn the Node runtime (`process.execPath`) with the CLI entry as an
|
|
969
1012
|
* argument rather than executing the entry path directly. Executing the `.js`
|
|
@@ -983,7 +1026,7 @@ export function getDaemonLaunch(agentsBin = getAgentsBinPath()) {
|
|
|
983
1026
|
const { warnings } = validateDaemonBinary(agentsBin);
|
|
984
1027
|
for (const w of warnings)
|
|
985
1028
|
process.stderr.write(`[agents] ${w}\n`);
|
|
986
|
-
return getCliLaunch(['
|
|
1029
|
+
return getCliLaunch(['__daemon-run'], agentsBin);
|
|
987
1030
|
}
|
|
988
1031
|
/**
|
|
989
1032
|
* The directory of the Node runtime that generated this service manifest, kept
|
package/dist/lib/drive-sync.js
CHANGED
|
@@ -13,6 +13,7 @@ import { execFile } from 'child_process';
|
|
|
13
13
|
import { promisify } from 'util';
|
|
14
14
|
import { getDriveDir } from './state.js';
|
|
15
15
|
import { AGENTS } from './agents.js';
|
|
16
|
+
import { sshExec } from './ssh-exec.js';
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
17
18
|
// `remote` flows from disk into rsync/ssh argv. Use a strict regex so a
|
|
18
19
|
// tampered ~/.agents/drive/config.json can't sneak shell metacharacters or
|
|
@@ -92,9 +93,11 @@ export async function push() {
|
|
|
92
93
|
assertValidRemote(config.remote);
|
|
93
94
|
const localDir = getDriveDir() + '/';
|
|
94
95
|
const remoteSpec = `${config.remote}:~/.agents/drive/`;
|
|
95
|
-
// Ensure remote directory exists
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
// Ensure remote directory exists via the shared hardened SSH primitive.
|
|
97
|
+
const mkdir = sshExec(config.remote, 'mkdir -p ~/.agents/drive', { timeoutMs: 30_000 });
|
|
98
|
+
if (mkdir.code !== 0) {
|
|
99
|
+
throw new Error(`Failed to prepare remote drive directory on ${config.remote}${mkdir.stderr.trim() ? `: ${mkdir.stderr.trim()}` : ''}`);
|
|
100
|
+
}
|
|
98
101
|
await execFileAsync('rsync', ['-az', '--exclude=config.json', localDir, remoteSpec]);
|
|
99
102
|
config.lastPush = new Date().toISOString();
|
|
100
103
|
writeDriveConfig(config);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified event reader -- one stream over BOTH operational events
|
|
3
|
+
* (`~/.agents/events.jsonl` via events.ts: secrets, commands, teams, ...) and
|
|
4
|
+
* agent-semantic events (the per-session activity logs via activity.ts: plans,
|
|
5
|
+
* PRs, worktrees, sub-agents, artifacts). They share one {@link EventType}
|
|
6
|
+
* vocabulary and one {@link EventRecord} shape, so `agents events` and any
|
|
7
|
+
* higher-level feature (a session/project summarizer, RSS) read them together.
|
|
8
|
+
*
|
|
9
|
+
* The two write paths stay separate for efficiency -- operational events append
|
|
10
|
+
* to the locked global log (low frequency), agent events append to lock-free
|
|
11
|
+
* per-session shards (high frequency, one writer each). This module is the
|
|
12
|
+
* single READ surface that merges them; nothing here writes.
|
|
13
|
+
*/
|
|
14
|
+
import { type EventRecord, type EventType, type EventLevel } from './events.js';
|
|
15
|
+
export interface UnifiedQuery {
|
|
16
|
+
startDate?: Date;
|
|
17
|
+
endDate?: Date;
|
|
18
|
+
eventTypes?: EventType[];
|
|
19
|
+
level?: EventLevel;
|
|
20
|
+
agent?: string;
|
|
21
|
+
caller?: string;
|
|
22
|
+
command?: string;
|
|
23
|
+
module?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
/** Include agent-semantic activity events. Default true. */
|
|
26
|
+
includeActivity?: boolean;
|
|
27
|
+
/** Override the activity dir (tests). */
|
|
28
|
+
activityRoot?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read a unified, newest-first event stream. Operational events come from
|
|
32
|
+
* events.ts `query()`; agent-semantic events from the activity logs, normalized
|
|
33
|
+
* to the same record shape and filtered identically. `limit` caps the merged
|
|
34
|
+
* result (each source is fetched up to `limit`, so the top-N is exact).
|
|
35
|
+
*/
|
|
36
|
+
export declare function readUnifiedEvents(q?: UnifiedQuery): EventRecord[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified event reader -- one stream over BOTH operational events
|
|
3
|
+
* (`~/.agents/events.jsonl` via events.ts: secrets, commands, teams, ...) and
|
|
4
|
+
* agent-semantic events (the per-session activity logs via activity.ts: plans,
|
|
5
|
+
* PRs, worktrees, sub-agents, artifacts). They share one {@link EventType}
|
|
6
|
+
* vocabulary and one {@link EventRecord} shape, so `agents events` and any
|
|
7
|
+
* higher-level feature (a session/project summarizer, RSS) read them together.
|
|
8
|
+
*
|
|
9
|
+
* The two write paths stay separate for efficiency -- operational events append
|
|
10
|
+
* to the locked global log (low frequency), agent events append to lock-free
|
|
11
|
+
* per-session shards (high frequency, one writer each). This module is the
|
|
12
|
+
* single READ surface that merges them; nothing here writes.
|
|
13
|
+
*/
|
|
14
|
+
import { query, levelFor } from './events.js';
|
|
15
|
+
import { readActivityAsEventRecords } from './activity.js';
|
|
16
|
+
/** Apply the same filters query() applies, to an activity-derived record. */
|
|
17
|
+
function matches(r, q) {
|
|
18
|
+
const ms = Date.parse(r.ts);
|
|
19
|
+
// Mirror query()'s default upper bound (endDate = now) so both sources drop
|
|
20
|
+
// future-dated records identically -- keeps the two in exact filter parity.
|
|
21
|
+
const endMs = (q.endDate ?? new Date()).getTime();
|
|
22
|
+
if (q.startDate && !Number.isNaN(ms) && ms < q.startDate.getTime())
|
|
23
|
+
return false;
|
|
24
|
+
if (!Number.isNaN(ms) && ms > endMs)
|
|
25
|
+
return false;
|
|
26
|
+
if (q.eventTypes && !q.eventTypes.includes(r.event))
|
|
27
|
+
return false;
|
|
28
|
+
if (q.level && (r.level ?? levelFor(r.event)) !== q.level)
|
|
29
|
+
return false;
|
|
30
|
+
if (q.agent && r.agent !== q.agent)
|
|
31
|
+
return false;
|
|
32
|
+
if (q.caller && r.caller !== q.caller)
|
|
33
|
+
return false;
|
|
34
|
+
if (q.command && r.command !== q.command &&
|
|
35
|
+
!(typeof r.command === 'string' && r.command.startsWith(q.command + ' ')))
|
|
36
|
+
return false;
|
|
37
|
+
if (q.module && r.module !== q.module)
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read a unified, newest-first event stream. Operational events come from
|
|
43
|
+
* events.ts `query()`; agent-semantic events from the activity logs, normalized
|
|
44
|
+
* to the same record shape and filtered identically. `limit` caps the merged
|
|
45
|
+
* result (each source is fetched up to `limit`, so the top-N is exact).
|
|
46
|
+
*/
|
|
47
|
+
export function readUnifiedEvents(q = {}) {
|
|
48
|
+
const ops = query({
|
|
49
|
+
startDate: q.startDate,
|
|
50
|
+
endDate: q.endDate,
|
|
51
|
+
eventTypes: q.eventTypes,
|
|
52
|
+
level: q.level,
|
|
53
|
+
agent: q.agent,
|
|
54
|
+
caller: q.caller,
|
|
55
|
+
command: q.command,
|
|
56
|
+
module: q.module,
|
|
57
|
+
limit: q.limit,
|
|
58
|
+
});
|
|
59
|
+
if (q.includeActivity === false)
|
|
60
|
+
return ops;
|
|
61
|
+
const acts = readActivityAsEventRecords({
|
|
62
|
+
sinceMs: q.startDate?.getTime(),
|
|
63
|
+
limit: q.limit,
|
|
64
|
+
root: q.activityRoot,
|
|
65
|
+
}).filter((r) => matches(r, q));
|
|
66
|
+
const merged = [...ops, ...acts].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
67
|
+
return typeof q.limit === 'number' ? merged.slice(0, q.limit) : merged;
|
|
68
|
+
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
13
|
*/
|
|
14
14
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
15
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
|
|
15
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'file.edited' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
16
|
export declare function levelFor(event: EventType): EventLevel;
|
|
17
17
|
export interface EventMeta {
|
|
18
18
|
ts: string;
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -158,6 +158,17 @@ export interface ExecOptions {
|
|
|
158
158
|
* `--interactive` takes precedence over `--headless`; the CLI layer rejects passing both.
|
|
159
159
|
*/
|
|
160
160
|
export declare function resolveInteractive(options: Pick<ExecOptions, 'interactive' | 'headless' | 'prompt'>): boolean;
|
|
161
|
+
/**
|
|
162
|
+
* True when a run resolved to *inferred* interactive intent — no prompt and no
|
|
163
|
+
* explicit `--interactive` — but there is no terminal to host the REPL. Launching
|
|
164
|
+
* would attach a TUI to a dead stdin and hang forever, so the caller should fail
|
|
165
|
+
* fast with the headless alternatives instead (RUSH-1829).
|
|
166
|
+
*
|
|
167
|
+
* An explicit `--interactive` is the caller's deliberate choice and is never
|
|
168
|
+
* blocked (they may be driving a PTY we can't detect). Pure — the TTY state is a
|
|
169
|
+
* parameter so this is unit-testable without touching `process.std*`.
|
|
170
|
+
*/
|
|
171
|
+
export declare function inferredInteractiveWithoutTty(options: Pick<ExecOptions, 'interactive' | 'headless' | 'prompt'>, isTty: boolean): boolean;
|
|
161
172
|
/**
|
|
162
173
|
* Decide whether spawnAgent must capture (PIPE + tee) the child's stdout so the
|
|
163
174
|
* live budget watcher can parse it (issue #346, FIX 3).
|