@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
9
|
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
10
|
+
import { quoteWin32ExecArg } from '../lib/platform/index.js';
|
|
11
|
+
import { type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
10
12
|
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
13
|
+
/**
|
|
14
|
+
* Build the child environment for `agents secrets exec`. Strips
|
|
15
|
+
* loader/interpreter hijack vars (matching agent spawns in exec.ts) and never
|
|
16
|
+
* forwards AGENTS_SECRETS_PASSPHRASE — the master decryption key must not reach
|
|
17
|
+
* the executed command.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildSecretsExecEnv(parentEnv: NodeJS.ProcessEnv, secretEnv: Record<string, string>): NodeJS.ProcessEnv;
|
|
11
20
|
/**
|
|
12
21
|
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
13
22
|
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
@@ -16,25 +25,29 @@ export { SSH_TARGET_RE, assertValidSshTarget };
|
|
|
16
25
|
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
17
26
|
*/
|
|
18
27
|
export declare function bundleEnvToDotenv(env: Record<string, string>): string;
|
|
28
|
+
/** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
|
|
29
|
+
* hint when a `daily` bundle is currently held by the secrets-agent. `held`
|
|
30
|
+
* maps bundle name → expiry epoch-ms (from agentStatus()). */
|
|
31
|
+
export declare function renderPolicyCol(b: SecretsBundle, held?: Map<string, number>): string;
|
|
19
32
|
/** Register the `agents secrets` command tree. */
|
|
20
33
|
export declare function registerSecretsCommands(program: Command): void;
|
|
34
|
+
/** Validate a prompt-policy value, throwing a clear message on a bad one (the
|
|
35
|
+
* caller's try/catch renders it and exits). Accepts the legacy `biometry` /
|
|
36
|
+
* `session` / `none` tokens as aliases for `always` / `daily` / `never` so older
|
|
37
|
+
* flags and scripts keep working. `never`/`none` is the no-biometry-ACL tier —
|
|
38
|
+
* accepted here, then gated behind a loud confirmation in the command layer. */
|
|
39
|
+
export declare function parsePolicyOpt(raw: string | undefined): SecretsPolicy;
|
|
21
40
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
|
|
31
|
-
* BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
|
|
32
|
-
* "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
|
|
33
|
-
* `agents secrets exec` runs a caller-supplied command against a bundle the
|
|
34
|
-
* caller owns, so caller-controlled `%`/`!` is not a privilege boundary. If that
|
|
35
|
-
* ever changes (exec'ing an untrusted command line), route through a shell that
|
|
36
|
-
* disables expansion rather than relying on this quoter. An empty arg becomes
|
|
37
|
-
* `""`. Exported for tests. No-ops on non-Windows (the caller only invokes it
|
|
38
|
-
* under `process.platform === 'win32'`).
|
|
41
|
+
* Gate a create/switch to the `never` prompt-policy behind an explicit,
|
|
42
|
+
* deliberate acknowledgement — it is the least-safe tier (no user-presence
|
|
43
|
+
* check on any read). Returns:
|
|
44
|
+
* - `'ok'` — not `never`, or already acknowledged via `--i-understand`.
|
|
45
|
+
* - `'prompt'` — interactive shell: caller must run a loud confirm prompt.
|
|
46
|
+
* Throws in a non-interactive shell when the flag is absent, so a headless
|
|
47
|
+
* `create --policy never` can't silently downgrade a bundle's protection.
|
|
39
48
|
*/
|
|
40
|
-
export declare function
|
|
49
|
+
export declare function assertNeverPolicyAcknowledged(policy: SecretsPolicy | undefined, opts: {
|
|
50
|
+
iUnderstand?: boolean;
|
|
51
|
+
interactive: boolean;
|
|
52
|
+
}): 'ok' | 'prompt';
|
|
53
|
+
export { quoteWin32ExecArg };
|
package/dist/commands/secrets.js
CHANGED
|
@@ -10,8 +10,10 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
13
|
+
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
14
|
+
import { ensureDaemonStarted } from '../lib/daemon.js';
|
|
13
15
|
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, resolveSshTarget, } from '../lib/secrets/remote.js';
|
|
14
|
-
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
16
|
+
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
15
17
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
16
18
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
17
19
|
import { DEFAULT_TTL_MS, agentLoad, agentLock, agentStatus, ensureAgentRunning, installSecretsAgentService, runAgentLoadFromStdin, runSecretsAgent, secretsAgentServiceInstalled, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
@@ -139,6 +141,31 @@ function readStdinSync() {
|
|
|
139
141
|
// SSH target validation is defined canonically in src/lib/ssh-exec.ts and
|
|
140
142
|
// re-exported here for back-compat with existing importers of these symbols.
|
|
141
143
|
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
144
|
+
/**
|
|
145
|
+
* Build the child environment for `agents secrets exec`. Strips
|
|
146
|
+
* loader/interpreter hijack vars (matching agent spawns in exec.ts) and never
|
|
147
|
+
* forwards AGENTS_SECRETS_PASSPHRASE — the master decryption key must not reach
|
|
148
|
+
* the executed command.
|
|
149
|
+
*/
|
|
150
|
+
export function buildSecretsExecEnv(parentEnv, secretEnv) {
|
|
151
|
+
const env = { ...sanitizeProcessEnv(parentEnv), ...secretEnv };
|
|
152
|
+
delete env.AGENTS_SECRETS_PASSPHRASE;
|
|
153
|
+
return env;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the CLI version for the MCP server's `serverInfo.version`. package.json
|
|
157
|
+
* sits at the repo root — two levels up from both `src/commands/` (bun/tsx dev)
|
|
158
|
+
* and `dist/commands/` (built). Cosmetic only, so any failure falls back cleanly.
|
|
159
|
+
*/
|
|
160
|
+
function getCliVersion() {
|
|
161
|
+
try {
|
|
162
|
+
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
163
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version ?? '0.0.0';
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return '0.0.0';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
142
169
|
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
143
170
|
function shellQuote(s) {
|
|
144
171
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -264,7 +291,10 @@ function compactRemaining(expiresAt) {
|
|
|
264
291
|
/** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
|
|
265
292
|
* hint when a `daily` bundle is currently held by the secrets-agent. `held`
|
|
266
293
|
* maps bundle name → expiry epoch-ms (from agentStatus()). */
|
|
267
|
-
function renderPolicyCol(b, held) {
|
|
294
|
+
export function renderPolicyCol(b, held) {
|
|
295
|
+
// `never` is loud on purpose — it's the only tier with no user-presence gate.
|
|
296
|
+
if (bundlePolicy(b) === 'never')
|
|
297
|
+
return chalk.red.bold('never · NO ACL');
|
|
268
298
|
if (bundlePolicy(b) === 'always')
|
|
269
299
|
return chalk.yellow('always ask');
|
|
270
300
|
const exp = held?.get(b.name);
|
|
@@ -505,7 +535,7 @@ export function registerSecretsCommands(program) {
|
|
|
505
535
|
{ title: 'Agent commands', names: ['start', 'stop', 'unlock', 'lock', 'status', 'policy'] },
|
|
506
536
|
{ title: 'Raw item commands', names: ['get', 'set'] },
|
|
507
537
|
{ title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
|
|
508
|
-
{ title: 'Utilities', names: ['exec', 'generate', 'migrate-acl'] },
|
|
538
|
+
{ title: 'Utilities', names: ['exec', 'mcp', 'generate', 'migrate-acl'] },
|
|
509
539
|
]);
|
|
510
540
|
cmd
|
|
511
541
|
.command('list')
|
|
@@ -588,9 +618,14 @@ export function registerSecretsCommands(program) {
|
|
|
588
618
|
console.log(chalk.yellow('allow_exec: true'));
|
|
589
619
|
if (bundle.backend === 'file')
|
|
590
620
|
console.log(chalk.gray('backend: file (passphrase-encrypted; reads need AGENTS_SECRETS_PASSPHRASE, no Touch ID)'));
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
621
|
+
if (bundlePolicy(bundle) === 'never') {
|
|
622
|
+
console.log(chalk.red.bold('policy: never — NO biometry ACL; reads are silent (no Touch ID, no user-presence check). Automation-only.'));
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
console.log(bundlePolicy(bundle) === 'daily'
|
|
626
|
+
? chalk.gray('policy: daily (ask once, then held ~24h until screen-lock / sleep / logout)')
|
|
627
|
+
: chalk.gray('policy: always (asks for Touch ID every time — never auto-held)'));
|
|
628
|
+
}
|
|
594
629
|
if (bundle.created_at)
|
|
595
630
|
console.log(chalk.gray(`created_at: ${bundle.created_at} (${humanAge(bundle.created_at)})`));
|
|
596
631
|
if (bundle.updated_at)
|
|
@@ -712,8 +747,9 @@ export function registerSecretsCommands(program) {
|
|
|
712
747
|
.description('Create an empty bundle')
|
|
713
748
|
.option('--description <text>', 'Free-form description')
|
|
714
749
|
.option('--allow-exec', 'Allow exec: refs in this bundle (off by default)')
|
|
715
|
-
.option('--policy <policy>', 'prompt policy: daily (default, ask once a day)
|
|
750
|
+
.option('--policy <policy>', 'prompt policy: daily (default, ask once a day), always (ask every time), or never (silent, NO biometry ACL — needs --i-understand)')
|
|
716
751
|
.addOption(new Option('--tier <policy>', 'deprecated alias for --policy').hideHelp())
|
|
752
|
+
.option('--i-understand', 'Confirm creating a "never"-policy bundle (no biometry ACL) without an interactive prompt')
|
|
717
753
|
.option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
|
|
718
754
|
.option('--force', 'Overwrite an existing bundle')
|
|
719
755
|
.action(async (name, opts) => {
|
|
@@ -729,6 +765,13 @@ export function registerSecretsCommands(program) {
|
|
|
729
765
|
console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
|
|
730
766
|
process.exit(1);
|
|
731
767
|
}
|
|
768
|
+
// The `never` tier is the least-safe option — gate it loudly. Throws in a
|
|
769
|
+
// headless shell without --i-understand; prompts otherwise.
|
|
770
|
+
const ack = assertNeverPolicyAcknowledged(policy, { iUnderstand: opts.iUnderstand, interactive: isInteractiveTerminal() });
|
|
771
|
+
if (ack === 'prompt' && !(await confirmNeverPolicyInteractive(resolvedName))) {
|
|
772
|
+
console.error(chalk.yellow('Aborted.'));
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
732
775
|
const bundle = {
|
|
733
776
|
name: resolvedName,
|
|
734
777
|
description: opts.description,
|
|
@@ -738,11 +781,16 @@ export function registerSecretsCommands(program) {
|
|
|
738
781
|
vars: {},
|
|
739
782
|
};
|
|
740
783
|
writeBundle(bundle);
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
784
|
+
const policyTag = bundlePolicy(bundle) === 'daily'
|
|
785
|
+
? 'policy: daily'
|
|
786
|
+
: bundlePolicy(bundle) === 'always'
|
|
787
|
+
? 'policy: always ask'
|
|
788
|
+
: 'policy: never (NO biometry ACL)';
|
|
789
|
+
const tags = [policyTag, backend === 'file' ? 'backend: file' : null].filter(Boolean);
|
|
745
790
|
console.log(chalk.green(`Bundle '${resolvedName}' created (${tags.join(', ')}).`));
|
|
791
|
+
if (bundlePolicy(bundle) === 'never') {
|
|
792
|
+
console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only; rotate anything sensitive out of it.'));
|
|
793
|
+
}
|
|
746
794
|
if (backend === 'file') {
|
|
747
795
|
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE (no Touch ID).'));
|
|
748
796
|
}
|
|
@@ -876,7 +924,7 @@ export function registerSecretsCommands(program) {
|
|
|
876
924
|
secretValue = await promptForSecret(`Enter value for ${resolvedBundleName}.${resolvedKey}`);
|
|
877
925
|
}
|
|
878
926
|
const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
|
|
879
|
-
bundleItemStore(bundle.backend).set(item, secretValue);
|
|
927
|
+
bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' }).set(item, secretValue);
|
|
880
928
|
bundle.vars[resolvedKey] = keychainRef(resolvedKey);
|
|
881
929
|
applyMeta();
|
|
882
930
|
writeBundle(bundle);
|
|
@@ -1122,7 +1170,7 @@ Examples:
|
|
|
1122
1170
|
vars: {},
|
|
1123
1171
|
};
|
|
1124
1172
|
}
|
|
1125
|
-
const store = bundleItemStore(bundle.backend);
|
|
1173
|
+
const store = bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' });
|
|
1126
1174
|
let added = 0;
|
|
1127
1175
|
let skipped = 0;
|
|
1128
1176
|
if (opts.from1password) {
|
|
@@ -1332,6 +1380,8 @@ Examples:
|
|
|
1332
1380
|
.command('exec <bundle> [command...]')
|
|
1333
1381
|
.description('Run a command with the bundle\'s secrets injected into the environment (use --host to resolve the bundle from a remote machine, ephemerally)')
|
|
1334
1382
|
.option('--host <target>', 'Resolve <bundle> on a remote host over SSH and inject it (ephemeral — never stored on this machine)')
|
|
1383
|
+
.option('--keys <keys>', 'Inject only this comma-separated subset of keys (e.g. KEY1,KEY2). Missing keys are an error.')
|
|
1384
|
+
.option('--allow-expired', 'Inject keys even if their expiry date has passed (overrides the pre-run expiry abort).')
|
|
1335
1385
|
.allowUnknownOption()
|
|
1336
1386
|
.action(async (bundleName, commandParts, execOpts) => {
|
|
1337
1387
|
try {
|
|
@@ -1340,28 +1390,42 @@ Examples:
|
|
|
1340
1390
|
process.exit(1);
|
|
1341
1391
|
}
|
|
1342
1392
|
const [cmd, ...args] = commandParts;
|
|
1393
|
+
const keysSubset = execOpts.keys
|
|
1394
|
+
? execOpts.keys.split(',').map((k) => k.trim()).filter(Boolean)
|
|
1395
|
+
: undefined;
|
|
1343
1396
|
let secretEnv;
|
|
1344
1397
|
if (execOpts.host) {
|
|
1398
|
+
// Least-privilege flags do not yet cross the SSH resolver —
|
|
1399
|
+
// silently applying them would inject the full remote env or an
|
|
1400
|
+
// expired key. Fail loud so the user can drop the flag or run
|
|
1401
|
+
// locally instead.
|
|
1402
|
+
const { assertRemoteBundleFlagsUnsupported } = await import('../lib/secrets/bundles.js');
|
|
1403
|
+
assertRemoteBundleFlagsUnsupported(bundleName, execOpts.host, { keys: keysSubset, allowExpired: execOpts.allowExpired }, { keysFlag: '--keys', allowExpiredFlag: '--allow-expired' });
|
|
1345
1404
|
secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName);
|
|
1346
1405
|
}
|
|
1347
1406
|
else {
|
|
1348
1407
|
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
1349
|
-
secretEnv = readAndResolveBundleEnv(bundleName, {
|
|
1408
|
+
secretEnv = readAndResolveBundleEnv(bundleName, {
|
|
1409
|
+
caller: `command ${cmd}`,
|
|
1410
|
+
keys: keysSubset,
|
|
1411
|
+
allowExpired: execOpts.allowExpired,
|
|
1412
|
+
}).env;
|
|
1350
1413
|
}
|
|
1351
1414
|
const { spawn } = await import('child_process');
|
|
1352
1415
|
// On Windows, spawn without a shell ENOENTs for `.cmd`/`.bat` launchers
|
|
1353
1416
|
// (npm, yarn, most JS CLIs) and shell built-ins, so we set shell:true.
|
|
1354
1417
|
// With shell:true Node hands cmd.exe a single command line with NO quoting
|
|
1355
|
-
// of its own
|
|
1356
|
-
//
|
|
1418
|
+
// of its own. Compose that line ourselves (composeWin32CommandLine quotes
|
|
1419
|
+
// every token) and pass an EMPTY args array so Node never concatenates the
|
|
1420
|
+
// user-supplied args unescaped (DEP0190 + injection). See that helper for
|
|
1357
1421
|
// the cmd.exe %VAR%/!VAR! expansion caveat.
|
|
1358
1422
|
const useShell = process.platform === 'win32';
|
|
1359
|
-
const spawnCmd = useShell ?
|
|
1360
|
-
const spawnArgs = useShell ?
|
|
1423
|
+
const spawnCmd = useShell ? composeWin32CommandLine(cmd, args) : cmd;
|
|
1424
|
+
const spawnArgs = useShell ? [] : args;
|
|
1361
1425
|
const proc = spawn(spawnCmd, spawnArgs, {
|
|
1362
1426
|
stdio: 'inherit',
|
|
1363
1427
|
shell: useShell,
|
|
1364
|
-
env:
|
|
1428
|
+
env: buildSecretsExecEnv(process.env, secretEnv),
|
|
1365
1429
|
});
|
|
1366
1430
|
proc.on('close', (code) => process.exit(code ?? 0));
|
|
1367
1431
|
proc.on('error', (err) => {
|
|
@@ -1376,6 +1440,17 @@ Examples:
|
|
|
1376
1440
|
process.exit(1);
|
|
1377
1441
|
}
|
|
1378
1442
|
});
|
|
1443
|
+
cmd
|
|
1444
|
+
.command('mcp')
|
|
1445
|
+
.description('Run a stdio MCP server exposing get_secret(bundle, key) — hand credentials to an MCP-speaking agent by name at call time, never through the child process environment')
|
|
1446
|
+
.action(async () => {
|
|
1447
|
+
// JIT credential delivery (#333): unlike `secrets exec`, which bakes every
|
|
1448
|
+
// resolved value into the child's env, this speaks MCP over stdio so a
|
|
1449
|
+
// framework requests one secret at a time and the raw value never enters
|
|
1450
|
+
// process.env. stdout is the JSON-RPC channel — nothing else may print there.
|
|
1451
|
+
const { runSecretsMcpServer } = await import('../lib/secrets/mcp.js');
|
|
1452
|
+
await runSecretsMcpServer({ version: getCliVersion() });
|
|
1453
|
+
});
|
|
1379
1454
|
cmd
|
|
1380
1455
|
.command('generate [length]')
|
|
1381
1456
|
.description('Generate a random password')
|
|
@@ -1478,6 +1553,12 @@ Examples:
|
|
|
1478
1553
|
console.error(chalk.red('Could not start the secrets-agent.'));
|
|
1479
1554
|
process.exit(1);
|
|
1480
1555
|
}
|
|
1556
|
+
// #415: the daemon should be always-on for any background need, not only
|
|
1557
|
+
// after `routines add`. A user who only ever unlocks secrets still gets
|
|
1558
|
+
// the daemon installed + running here. `ensureAgentRunning` above only
|
|
1559
|
+
// brings up the standalone secrets broker, not the daemon. Idempotent
|
|
1560
|
+
// (single-instance start lock, #414) and best-effort — never blocks unlock.
|
|
1561
|
+
ensureDaemonStarted();
|
|
1481
1562
|
let loaded = 0;
|
|
1482
1563
|
for (const name of targets) {
|
|
1483
1564
|
try {
|
|
@@ -1547,8 +1628,9 @@ Examples:
|
|
|
1547
1628
|
cmd
|
|
1548
1629
|
.command('policy <bundle> [policy]')
|
|
1549
1630
|
.alias('tier')
|
|
1550
|
-
.description("Show or set a bundle's prompt policy: daily (default, ask once a day)
|
|
1551
|
-
.
|
|
1631
|
+
.description("Show or set a bundle's prompt policy: daily (default, ask once a day), always (ask every time), or never (silent, NO biometry ACL).")
|
|
1632
|
+
.option('--i-understand', 'Confirm switching to the "never" policy (no biometry ACL) without an interactive prompt')
|
|
1633
|
+
.action(async (bundleName, policyArg, opts) => {
|
|
1552
1634
|
try {
|
|
1553
1635
|
const bundle = readBundle(bundleName);
|
|
1554
1636
|
if (policyArg === undefined) {
|
|
@@ -1556,15 +1638,24 @@ Examples:
|
|
|
1556
1638
|
return;
|
|
1557
1639
|
}
|
|
1558
1640
|
const next = parsePolicyOpt(policyArg);
|
|
1641
|
+
// Switching to `never` drops the biometry ACL — gate it exactly like create.
|
|
1642
|
+
const ack = assertNeverPolicyAcknowledged(next, { iUnderstand: opts.iUnderstand, interactive: isInteractiveTerminal() });
|
|
1643
|
+
if (ack === 'prompt' && !(await confirmNeverPolicyInteractive(bundle.name))) {
|
|
1644
|
+
console.error(chalk.yellow('Aborted.'));
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1559
1647
|
bundle.policy = next;
|
|
1560
1648
|
writeBundle(bundle);
|
|
1561
1649
|
console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
|
|
1562
1650
|
if (next === 'daily') {
|
|
1563
1651
|
console.log(chalk.gray('Held by the secrets-agent for ~24h after one unlock (auto-cache is on by default; disable with `secrets.agent.auto: false` in agents.yaml).'));
|
|
1564
1652
|
}
|
|
1565
|
-
else {
|
|
1653
|
+
else if (next === 'always') {
|
|
1566
1654
|
console.log(chalk.gray('Asks for Touch ID every time — never auto-held.'));
|
|
1567
1655
|
}
|
|
1656
|
+
else {
|
|
1657
|
+
console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only.'));
|
|
1658
|
+
}
|
|
1568
1659
|
}
|
|
1569
1660
|
catch (err) {
|
|
1570
1661
|
console.error(chalk.red(err.message));
|
|
@@ -1613,23 +1704,52 @@ Examples:
|
|
|
1613
1704
|
registerSecretsSyncCommands(cmd);
|
|
1614
1705
|
registerSecretsMigrateAclCommand(cmd);
|
|
1615
1706
|
}
|
|
1616
|
-
/** Validate a prompt-policy value,
|
|
1617
|
-
* Accepts the legacy `biometry
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
function parsePolicyOpt(raw) {
|
|
1707
|
+
/** Validate a prompt-policy value, throwing a clear message on a bad one (the
|
|
1708
|
+
* caller's try/catch renders it and exits). Accepts the legacy `biometry` /
|
|
1709
|
+
* `session` / `none` tokens as aliases for `always` / `daily` / `never` so older
|
|
1710
|
+
* flags and scripts keep working. `never`/`none` is the no-biometry-ACL tier —
|
|
1711
|
+
* accepted here, then gated behind a loud confirmation in the command layer. */
|
|
1712
|
+
export function parsePolicyOpt(raw) {
|
|
1622
1713
|
const v = (raw ?? 'always').toLowerCase();
|
|
1623
1714
|
if (v === 'always' || v === 'biometry')
|
|
1624
1715
|
return 'always';
|
|
1625
1716
|
if (v === 'daily' || v === 'session')
|
|
1626
1717
|
return 'daily';
|
|
1627
|
-
if (v === 'never' || v === 'none')
|
|
1628
|
-
|
|
1629
|
-
|
|
1718
|
+
if (v === 'never' || v === 'none')
|
|
1719
|
+
return 'never';
|
|
1720
|
+
throw new Error(`Invalid policy '${raw}'. Use 'always', 'daily', or 'never'.`);
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Gate a create/switch to the `never` prompt-policy behind an explicit,
|
|
1724
|
+
* deliberate acknowledgement — it is the least-safe tier (no user-presence
|
|
1725
|
+
* check on any read). Returns:
|
|
1726
|
+
* - `'ok'` — not `never`, or already acknowledged via `--i-understand`.
|
|
1727
|
+
* - `'prompt'` — interactive shell: caller must run a loud confirm prompt.
|
|
1728
|
+
* Throws in a non-interactive shell when the flag is absent, so a headless
|
|
1729
|
+
* `create --policy never` can't silently downgrade a bundle's protection.
|
|
1730
|
+
*/
|
|
1731
|
+
export function assertNeverPolicyAcknowledged(policy, opts) {
|
|
1732
|
+
if (policy !== 'never')
|
|
1733
|
+
return 'ok';
|
|
1734
|
+
if (opts.iUnderstand)
|
|
1735
|
+
return 'ok';
|
|
1736
|
+
if (!opts.interactive) {
|
|
1737
|
+
throw new Error("Refusing to set the 'never' prompt-policy without confirmation. This tier stores " +
|
|
1738
|
+
'the bundle WITHOUT the biometry access control: every read is silent, with no Touch ID ' +
|
|
1739
|
+
'and no user-presence check — any code running as you can read it. Re-run with ' +
|
|
1740
|
+
'--i-understand to confirm you want an unprotected, automation-only bundle.');
|
|
1630
1741
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1742
|
+
return 'prompt';
|
|
1743
|
+
}
|
|
1744
|
+
/** Loud confirm prompt shown before creating/switching to `never` in a TTY.
|
|
1745
|
+
* Returns true to proceed. Kept separate from assertNeverPolicyAcknowledged so
|
|
1746
|
+
* the decision logic stays pure and unit-testable. */
|
|
1747
|
+
async function confirmNeverPolicyInteractive(bundleName) {
|
|
1748
|
+
console.error(chalk.red.bold('WARNING: policy "never" stores this bundle with NO biometry ACL.'));
|
|
1749
|
+
console.error(chalk.red(`Reads of '${bundleName}' will be fully silent — no Touch ID, no user-presence check.`));
|
|
1750
|
+
console.error(chalk.yellow('Use it only for low-sensitivity, automation-only credentials.'));
|
|
1751
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
1752
|
+
return confirm({ message: `Store '${bundleName}' WITHOUT biometry protection?`, default: false });
|
|
1633
1753
|
}
|
|
1634
1754
|
/** Validate a --backend value, exiting with a clear message on a bad one. */
|
|
1635
1755
|
function parseBackendOpt(raw) {
|
|
@@ -1653,48 +1773,9 @@ function humanRemaining(expiresAt) {
|
|
|
1653
1773
|
const days = Math.round(hours / 24);
|
|
1654
1774
|
return `locks in ${days} day${days === 1 ? '' : 's'}`;
|
|
1655
1775
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
* NO quoting of its own, so an unquoted arg with a space is split into several
|
|
1660
|
-
* args, and a cmd metacharacter (`&|<>()^`) would be interpreted by the shell.
|
|
1661
|
-
* We wrap any arg with whitespace, a quote, or a metacharacter in double quotes
|
|
1662
|
-
* and escape embedded quotes / trailing backslashes per the CommandLineToArgvW
|
|
1663
|
-
* rules, so the *child's* argv parse reconstructs the original argument.
|
|
1664
|
-
*
|
|
1665
|
-
* CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
|
|
1666
|
-
* BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
|
|
1667
|
-
* "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
|
|
1668
|
-
* `agents secrets exec` runs a caller-supplied command against a bundle the
|
|
1669
|
-
* caller owns, so caller-controlled `%`/`!` is not a privilege boundary. If that
|
|
1670
|
-
* ever changes (exec'ing an untrusted command line), route through a shell that
|
|
1671
|
-
* disables expansion rather than relying on this quoter. An empty arg becomes
|
|
1672
|
-
* `""`. Exported for tests. No-ops on non-Windows (the caller only invokes it
|
|
1673
|
-
* under `process.platform === 'win32'`).
|
|
1674
|
-
*/
|
|
1675
|
-
export function quoteWin32ExecArg(arg) {
|
|
1676
|
-
if (arg.length > 0 && !/[\s"&|<>()^]/.test(arg))
|
|
1677
|
-
return arg;
|
|
1678
|
-
let result = '"';
|
|
1679
|
-
let backslashes = 0;
|
|
1680
|
-
for (const ch of arg) {
|
|
1681
|
-
if (ch === '\\') {
|
|
1682
|
-
backslashes += 1;
|
|
1683
|
-
continue;
|
|
1684
|
-
}
|
|
1685
|
-
if (ch === '"') {
|
|
1686
|
-
// Double the run of backslashes, then escape this quote.
|
|
1687
|
-
result += '\\'.repeat(backslashes * 2 + 1) + '"';
|
|
1688
|
-
backslashes = 0;
|
|
1689
|
-
continue;
|
|
1690
|
-
}
|
|
1691
|
-
result += '\\'.repeat(backslashes) + ch;
|
|
1692
|
-
backslashes = 0;
|
|
1693
|
-
}
|
|
1694
|
-
// Trailing backslashes precede the closing quote → must be doubled.
|
|
1695
|
-
result += '\\'.repeat(backslashes * 2) + '"';
|
|
1696
|
-
return result;
|
|
1697
|
-
}
|
|
1776
|
+
// `quoteWin32ExecArg` now lives in lib/platform/exec.ts (shared with the agent
|
|
1777
|
+
// run/shim spawn paths); re-exported here for the colocated secrets tests.
|
|
1778
|
+
export { quoteWin32ExecArg };
|
|
1698
1779
|
/**
|
|
1699
1780
|
* Copy text to the system clipboard, cross-platform.
|
|
1700
1781
|
* macOS: `pbcopy`. Windows: `clip`. Linux: tries `wl-copy` (Wayland), then
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents serve` — read-only local web companion.
|
|
3
|
+
*
|
|
4
|
+
* Boots a localhost-only (127.0.0.1) HTTP + SSE server that renders a live
|
|
5
|
+
* dashboard of team status + per-worktree diffs, scheduled routines, and cloud
|
|
6
|
+
* tasks. Everything is a viewer over data other commands already own — there
|
|
7
|
+
* are no mutation endpoints. See src/lib/serve/.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
export declare function registerServeCommand(program: Command): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { startServeServer, DEFAULT_SERVE_PORT, SERVE_HOST } from '../lib/serve/server.js';
|
|
3
|
+
export function registerServeCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('serve')
|
|
6
|
+
.description('Read-only local web companion: team diffs, routines, and cloud status (binds 127.0.0.1 only).')
|
|
7
|
+
.option('-p, --port <n>', `Port to bind on ${SERVE_HOST}`, String(DEFAULT_SERVE_PORT))
|
|
8
|
+
.option('--interval <ms>', 'SSE refresh cadence in milliseconds', '3000')
|
|
9
|
+
.action(async (opts) => {
|
|
10
|
+
const port = parseInt(opts.port ?? '', 10) || DEFAULT_SERVE_PORT;
|
|
11
|
+
const intervalMs = parseInt(opts.interval ?? '', 10) || 3000;
|
|
12
|
+
try {
|
|
13
|
+
const { server, port: bound } = await startServeServer(port, {
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
intervalMs,
|
|
16
|
+
});
|
|
17
|
+
const url = `http://${SERVE_HOST}:${bound}`;
|
|
18
|
+
console.log(`${chalk.green('agents serve')} ${chalk.dim('→')} ${chalk.cyan(url)}`);
|
|
19
|
+
console.log(chalk.dim('read-only · localhost only · Ctrl-C to stop'));
|
|
20
|
+
const shutdown = () => {
|
|
21
|
+
server.close(() => process.exit(0));
|
|
22
|
+
};
|
|
23
|
+
process.on('SIGINT', shutdown);
|
|
24
|
+
process.on('SIGTERM', shutdown);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const msg = String(err?.message ?? err);
|
|
28
|
+
if (msg.includes('EADDRINUSE')) {
|
|
29
|
+
console.error(chalk.red(`Port ${port} is already in use. Pass --port <n> to pick another.`));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.error(chalk.red(`Could not start serve: ${msg}`));
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions inject <sessionId> <text>` — deliver text into the terminal a
|
|
3
|
+
* running session lives in. The CLI face of the Terminal Engine's Gap 2 primitive
|
|
4
|
+
* (`injectIntoTerminal`, src/lib/terminal/inject.ts), so a native watchdog
|
|
5
|
+
* (RUSH-1415) can shell out to nudge a stalled agent with "continue".
|
|
6
|
+
*
|
|
7
|
+
* Resolution: find the active session by id, map its `provenance.reply` rail
|
|
8
|
+
* (provenance.ts:47) to an engine `InjectTarget` and inject. Today only tmux rails
|
|
9
|
+
* are externally addressable; `--pane`/`--pty` target a backend directly when the
|
|
10
|
+
* handle is already known (skipping the session lookup).
|
|
11
|
+
*/
|
|
12
|
+
import type { Command } from 'commander';
|
|
13
|
+
/** Attach the `inject` subcommand to an existing `sessions` command. */
|
|
14
|
+
export declare function registerSessionsInjectCommand(sessionsCmd: Command): void;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions inject <sessionId> <text>` — deliver text into the terminal a
|
|
3
|
+
* running session lives in. The CLI face of the Terminal Engine's Gap 2 primitive
|
|
4
|
+
* (`injectIntoTerminal`, src/lib/terminal/inject.ts), so a native watchdog
|
|
5
|
+
* (RUSH-1415) can shell out to nudge a stalled agent with "continue".
|
|
6
|
+
*
|
|
7
|
+
* Resolution: find the active session by id, map its `provenance.reply` rail
|
|
8
|
+
* (provenance.ts:47) to an engine `InjectTarget` and inject. Today only tmux rails
|
|
9
|
+
* are externally addressable; `--pane`/`--pty` target a backend directly when the
|
|
10
|
+
* handle is already known (skipping the session lookup).
|
|
11
|
+
*/
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { getActiveSessions } from '../lib/session/active.js';
|
|
14
|
+
import { injectTargetFromReplyRail } from '../lib/session/inject.js';
|
|
15
|
+
import { injectIntoTerminal } from '../lib/terminal/index.js';
|
|
16
|
+
import { setHelpSections } from '../lib/help.js';
|
|
17
|
+
/** Resolve a session id (short or full) to an addressable terminal target. */
|
|
18
|
+
async function resolveTarget(sessionId) {
|
|
19
|
+
const sessions = await getActiveSessions();
|
|
20
|
+
const match = sessions.find((s) => s.sessionId === sessionId || (s.sessionId != null && s.sessionId.startsWith(sessionId)));
|
|
21
|
+
if (!match)
|
|
22
|
+
return { target: null, reason: `No active session matches "${sessionId}".` };
|
|
23
|
+
const rail = match.provenance?.reply;
|
|
24
|
+
if (!rail) {
|
|
25
|
+
return {
|
|
26
|
+
target: null,
|
|
27
|
+
reason: `Session "${sessionId}" has no addressable reply rail (not running under tmux). Relaunch it under a tmux/pty rail to inject.`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const target = injectTargetFromReplyRail(rail);
|
|
31
|
+
if (!target)
|
|
32
|
+
return { target: null, reason: `Session "${sessionId}" reply rail is not injectable.` };
|
|
33
|
+
return { target };
|
|
34
|
+
}
|
|
35
|
+
async function runInject(sessionId, text, options) {
|
|
36
|
+
// Direct-target shortcuts skip the session lookup — the watchdog often already
|
|
37
|
+
// holds the pane id or pty session it wants to type into.
|
|
38
|
+
let target = null;
|
|
39
|
+
if (options.pty) {
|
|
40
|
+
target = { backend: 'pty', id: options.pty };
|
|
41
|
+
}
|
|
42
|
+
else if (options.pane) {
|
|
43
|
+
target = { backend: 'tmux', pane: options.pane, socket: options.socket };
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const resolved = await resolveTarget(sessionId);
|
|
47
|
+
if (!resolved.target) {
|
|
48
|
+
if (options.json)
|
|
49
|
+
console.log(JSON.stringify({ ok: false, error: resolved.reason }));
|
|
50
|
+
else
|
|
51
|
+
console.error(chalk.red(resolved.reason));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
target = resolved.target;
|
|
55
|
+
}
|
|
56
|
+
const res = await injectIntoTerminal(target, text, {
|
|
57
|
+
enter: options.enter !== false,
|
|
58
|
+
combined: options.combined,
|
|
59
|
+
socket: options.socket,
|
|
60
|
+
host: options.host,
|
|
61
|
+
});
|
|
62
|
+
if (options.json) {
|
|
63
|
+
console.log(JSON.stringify(res));
|
|
64
|
+
}
|
|
65
|
+
else if (res.ok) {
|
|
66
|
+
console.log(chalk.green(`Injected into ${res.backend} (${res.writes} write${res.writes === 1 ? '' : 's'}).`));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.error(chalk.red(res.error ?? 'injection failed'));
|
|
70
|
+
}
|
|
71
|
+
if (!res.ok)
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
/** Attach the `inject` subcommand to an existing `sessions` command. */
|
|
75
|
+
export function registerSessionsInjectCommand(sessionsCmd) {
|
|
76
|
+
const injectCmd = sessionsCmd
|
|
77
|
+
.command('inject <sessionId> <text>')
|
|
78
|
+
.description('Deliver text (+ Enter) into the terminal a running session lives in — nudge a stalled agent.')
|
|
79
|
+
.option('--pane <id>', 'Target a tmux pane id directly (e.g. %3), skipping session lookup')
|
|
80
|
+
.option('--pty <id>', 'Target an agents-pty session id directly, skipping session lookup')
|
|
81
|
+
.option('--socket <path>', 'tmux socket path (defaults to the session/shared socket)')
|
|
82
|
+
.option('--host <target>', 'Deliver on a remote host over SSH (tmux/AppleScript backends)')
|
|
83
|
+
.option('--no-enter', 'Send only the text, without a trailing Enter')
|
|
84
|
+
.option('--combined', 'Fuse text + Enter into ONE write (default: two writes, Ink-TUI safe)')
|
|
85
|
+
.option('--json', 'Output the InjectResult as JSON');
|
|
86
|
+
setHelpSections(injectCmd, {
|
|
87
|
+
examples: `
|
|
88
|
+
# Nudge a stalled agent by session id (resolves its tmux pane)
|
|
89
|
+
agents sessions inject a1b2c3d4 "continue"
|
|
90
|
+
|
|
91
|
+
# Target a tmux pane directly (what a watchdog already holds)
|
|
92
|
+
agents sessions inject _ "continue" --pane %3 --socket /tmp/agents/tmux.sock
|
|
93
|
+
|
|
94
|
+
# Type into an agents-pty session without submitting
|
|
95
|
+
agents sessions inject _ "ls" --pty $SID --no-enter
|
|
96
|
+
`,
|
|
97
|
+
notes: `
|
|
98
|
+
- Ink-TUI Enter semantics: by default the text and Enter are two separate
|
|
99
|
+
writes, which is what Claude's Ink TUI needs. --combined fuses them.
|
|
100
|
+
- Only sessions running under a tmux pane are addressable by id today
|
|
101
|
+
(provenance.ts reply rails). Use --pane/--pty for direct targeting.
|
|
102
|
+
- Built on the Terminal Engine (src/lib/terminal): --host runs the tmux /
|
|
103
|
+
AppleScript spec over the same SSH transport the launch engine uses.
|
|
104
|
+
`,
|
|
105
|
+
});
|
|
106
|
+
// The parent `sessions` command also defines --json, so it binds there;
|
|
107
|
+
// optsWithGlobals() merges parent + subcommand options so --json is honored.
|
|
108
|
+
injectCmd.action(async (sessionId, text, _options, command) => {
|
|
109
|
+
await runInject(sessionId, text, command.optsWithGlobals());
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -5,6 +5,8 @@ export interface PickedSession {
|
|
|
5
5
|
}
|
|
6
6
|
export interface SessionPickerConfig {
|
|
7
7
|
message: string;
|
|
8
|
+
/** Dim hint line shown under the header (filters/flags tip). */
|
|
9
|
+
subtitle?: string;
|
|
8
10
|
sessions: SessionMeta[];
|
|
9
11
|
filter: (query: string) => SessionMeta[];
|
|
10
12
|
labelFor: (s: SessionMeta, query: string) => string;
|