@phnx-labs/agents-cli 1.20.87 → 1.20.89
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 +323 -0
- package/README.md +12 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/commands.js +7 -7
- package/dist/commands/doctor.d.ts +0 -19
- package/dist/commands/doctor.js +219 -305
- package/dist/commands/exec.js +7 -19
- package/dist/commands/factory.js +26 -2
- package/dist/commands/funnel.js +16 -1
- package/dist/commands/inspect.js +3 -5
- package/dist/commands/menubar.js +117 -34
- package/dist/commands/routines.js +25 -3
- package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
- package/dist/commands/secrets-rotate-passphrase.js +96 -0
- package/dist/commands/secrets.js +2 -0
- package/dist/commands/sessions.d.ts +7 -1
- package/dist/commands/sessions.js +40 -12
- package/dist/commands/ssh.js +3 -3
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/commands/webhook.js +7 -2
- package/dist/lib/agents.d.ts +31 -1
- package/dist/lib/agents.js +55 -0
- package/dist/lib/command-skills.d.ts +10 -0
- package/dist/lib/command-skills.js +14 -0
- package/dist/lib/commands.js +28 -2
- package/dist/lib/daemon.d.ts +29 -0
- package/dist/lib/daemon.js +75 -6
- package/dist/lib/devices/doctor-findings.d.ts +167 -0
- package/dist/lib/devices/doctor-findings.js +893 -0
- package/dist/lib/devices/fleet-divergence.d.ts +22 -0
- package/dist/lib/devices/fleet-divergence.js +34 -10
- package/dist/lib/devices/fleet-inventory.d.ts +17 -6
- package/dist/lib/devices/fleet-inventory.js +56 -8
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +14 -3
- package/dist/lib/exec.js +41 -8
- package/dist/lib/factory/snapshot.d.ts +78 -0
- package/dist/lib/factory/snapshot.js +209 -0
- package/dist/lib/fs-atomic.d.ts +14 -1
- package/dist/lib/fs-atomic.js +35 -3
- package/dist/lib/funnel.d.ts +1 -0
- package/dist/lib/funnel.js +8 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/menubar/install-menubar.d.ts +53 -2
- package/dist/lib/menubar/install-menubar.js +183 -28
- package/dist/lib/platform/process.d.ts +2 -0
- package/dist/lib/platform/process.js +5 -3
- package/dist/lib/project-resources.js +34 -20
- package/dist/lib/resources.d.ts +8 -0
- package/dist/lib/resources.js +34 -1
- package/dist/lib/routines-placement.d.ts +2 -1
- package/dist/lib/routines-placement.js +8 -4
- package/dist/lib/routines.d.ts +57 -1
- package/dist/lib/routines.js +74 -1
- package/dist/lib/runner.d.ts +16 -1
- package/dist/lib/runner.js +58 -16
- package/dist/lib/sandbox.d.ts +2 -0
- package/dist/lib/sandbox.js +38 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +9 -34
- package/dist/lib/secrets/filestore.d.ts +152 -34
- package/dist/lib/secrets/filestore.js +676 -123
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -6
- package/dist/lib/secrets/rc-hygiene.js +0 -24
- package/dist/lib/session/active.d.ts +6 -6
- package/dist/lib/session/active.js +6 -6
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +137 -1
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +76 -37
- package/dist/lib/session/remote-active.d.ts +4 -1
- package/dist/lib/session/remote-active.js +8 -2
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/viewing-in.d.ts +31 -0
- package/dist/lib/session/viewing-in.js +47 -0
- package/dist/lib/staleness/detectors/commands.js +14 -5
- package/dist/lib/staleness/types.d.ts +2 -0
- package/dist/lib/staleness/writers/commands.js +13 -7
- package/dist/lib/state.d.ts +17 -0
- package/dist/lib/state.js +30 -2
- package/dist/lib/triggers/handlers.d.ts +95 -0
- package/dist/lib/triggers/handlers.js +384 -0
- package/dist/lib/triggers/webhook.d.ts +10 -2
- package/dist/lib/triggers/webhook.js +65 -11
- package/dist/lib/usage.d.ts +72 -1
- package/dist/lib/usage.js +21 -27
- package/dist/lib/versions.js +30 -13
- package/package.json +1 -1
package/dist/commands/exec.js
CHANGED
|
@@ -1667,7 +1667,7 @@ export function registerRunCommand(program) {
|
|
|
1667
1667
|
const { pickSessionInteractive } = await import('./sessions.js');
|
|
1668
1668
|
const { buildContinuePrompt } = await import('../lib/loop.js');
|
|
1669
1669
|
// Freshen the index for this agent before any lookup (incremental, cached).
|
|
1670
|
-
// AgentId is wider than SessionAgentId (
|
|
1670
|
+
// AgentId is wider than SessionAgentId (amp/kiro/goose/copilot keep no transcripts);
|
|
1671
1671
|
// those simply yield no matches and fall through to the not-found error.
|
|
1672
1672
|
const sessionAgent = agent;
|
|
1673
1673
|
await discoverSessions({ agent: sessionAgent, version });
|
|
@@ -1899,8 +1899,8 @@ export function registerRunCommand(program) {
|
|
|
1899
1899
|
// safest native mode (modes[0], typically edit). That covers both the
|
|
1900
1900
|
// implicit default and an explicit `--mode plan`, so multi-agent
|
|
1901
1901
|
// scripts can pass a uniform plan flag without per-agent branching.
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1902
|
+
// Mode degradation is never silent: buildExecCommand emits one stderr
|
|
1903
|
+
// warning for the requested-to-resolved transition unless --quiet is set.
|
|
1904
1904
|
// `skip` still hard-fails when unsupported — pretending we bypassed
|
|
1905
1905
|
// permissions would be unsafe.
|
|
1906
1906
|
const modeIsDefault = modeSource === 'default';
|
|
@@ -1913,20 +1913,7 @@ export function registerRunCommand(program) {
|
|
|
1913
1913
|
console.error(chalk.red(err.message));
|
|
1914
1914
|
process.exit(1);
|
|
1915
1915
|
}
|
|
1916
|
-
|
|
1917
|
-
mode = resolvedMode;
|
|
1918
|
-
if (!options.quiet) {
|
|
1919
|
-
if (requestedMode === 'plan' && !modeIsDefault) {
|
|
1920
|
-
process.stderr.write(chalk.yellow(`[agents] ${agent} has no read-only 'plan' mode; using '${mode}' (writable) instead. Pass --mode ${mode} to silence this.\n`));
|
|
1921
|
-
}
|
|
1922
|
-
else {
|
|
1923
|
-
process.stderr.write(chalk.gray(`[agents] ${agent} has no '${requestedMode}' mode; using '${mode}'\n`));
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
else {
|
|
1928
|
-
mode = resolvedMode;
|
|
1929
|
-
}
|
|
1916
|
+
mode = resolvedMode;
|
|
1930
1917
|
// Fail fast on the headless-plan stall footgun: a slash command run
|
|
1931
1918
|
// headless under the implicit default 'plan' mode hangs forever at
|
|
1932
1919
|
// ExitPlanMode (no TTY to approve the plan). Tell the user how to fix it
|
|
@@ -1935,7 +1922,7 @@ export function registerRunCommand(program) {
|
|
|
1935
1922
|
const stallCmd = headlessPlanStallCommand({
|
|
1936
1923
|
prompt,
|
|
1937
1924
|
interactive: options.interactive,
|
|
1938
|
-
mode,
|
|
1925
|
+
mode: resolvedMode,
|
|
1939
1926
|
modeIsDefault,
|
|
1940
1927
|
});
|
|
1941
1928
|
if (stallCmd) {
|
|
@@ -2029,7 +2016,7 @@ export function registerRunCommand(program) {
|
|
|
2029
2016
|
version,
|
|
2030
2017
|
prompt,
|
|
2031
2018
|
interactive: options.interactive || forceInteractive,
|
|
2032
|
-
mode,
|
|
2019
|
+
mode: requestedMode,
|
|
2033
2020
|
effort,
|
|
2034
2021
|
cwd: options.cwd,
|
|
2035
2022
|
model,
|
|
@@ -2040,6 +2027,7 @@ export function registerRunCommand(program) {
|
|
|
2040
2027
|
name: options.name,
|
|
2041
2028
|
resume: resumeNative,
|
|
2042
2029
|
verbose: options.verbose,
|
|
2030
|
+
modeWarningState: { quiet: options.quiet },
|
|
2043
2031
|
// --raw, --no-tmux (commander negation → options.tmux === false), and
|
|
2044
2032
|
// --disable-tmux all bypass the interactive tmux wrapper. AGENTS_NO_TMUX=1
|
|
2045
2033
|
// does the same via the env check in exec.ts.
|
package/dist/commands/factory.js
CHANGED
|
@@ -6,6 +6,7 @@ import { homedir } from 'os';
|
|
|
6
6
|
import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
|
|
7
7
|
import { insertTask } from '../lib/cloud/store.js';
|
|
8
8
|
import { emit } from '../lib/events.js';
|
|
9
|
+
import { buildFactorySnapshot } from '../lib/factory/snapshot.js';
|
|
9
10
|
function requireFactoryUrl() {
|
|
10
11
|
const url = process.env.FACTORY_FLOOR_URL;
|
|
11
12
|
if (!url) {
|
|
@@ -51,14 +52,28 @@ export function registerFactoryCommands(program) {
|
|
|
51
52
|
Examples:
|
|
52
53
|
agents factory submit PROJ-123
|
|
53
54
|
agents factory submit https://linear.app/example/issue/PROJ-123
|
|
55
|
+
agents factory snapshot --json
|
|
54
56
|
`);
|
|
55
|
-
factory.hook('preAction', () => {
|
|
56
|
-
|
|
57
|
+
factory.hook('preAction', (_thisCommand, actionCommand) => {
|
|
58
|
+
// Foreman must be able to read its tick input before any beta-gated action.
|
|
59
|
+
if (enabled || actionCommand.name() === 'snapshot')
|
|
57
60
|
return;
|
|
58
61
|
console.error(chalk.red('agents factory is in beta.'));
|
|
59
62
|
console.error(chalk.gray(betaEnableHint('factory')));
|
|
60
63
|
process.exit(1);
|
|
61
64
|
});
|
|
65
|
+
factory
|
|
66
|
+
.command('snapshot')
|
|
67
|
+
.description('Read the complete Software Factory state without dispatching or changing it.')
|
|
68
|
+
.option('--json', 'Output the stable machine-readable snapshot')
|
|
69
|
+
.action(async (opts) => {
|
|
70
|
+
const snapshot = await buildFactorySnapshot();
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
renderSnapshot(snapshot);
|
|
76
|
+
});
|
|
62
77
|
factory
|
|
63
78
|
.command('submit <linear-ref>')
|
|
64
79
|
.description('Submit a Linear issue (PROJ-123 or URL) to the Software Factory.')
|
|
@@ -93,3 +108,12 @@ Examples:
|
|
|
93
108
|
console.log(` tail output agents cloud logs ${result.cloud_execution_id}`);
|
|
94
109
|
});
|
|
95
110
|
}
|
|
111
|
+
function renderSnapshot(snapshot) {
|
|
112
|
+
console.log(chalk.bold(`Factory snapshot ${snapshot.generatedAt}`));
|
|
113
|
+
console.log(` sessions ${snapshot.sessions.length}`);
|
|
114
|
+
console.log(` open PRs ${snapshot.prs.length}`);
|
|
115
|
+
console.log(` devices ${snapshot.devices.length}`);
|
|
116
|
+
for (const [project, queue] of Object.entries(snapshot.queues)) {
|
|
117
|
+
console.log(` ${project.padEnd(12)} todo ${queue.todo} · in progress ${queue.inProgress} · blocked ${queue.blocked}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
package/dist/commands/funnel.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { buildFunnelStatusCommand, buildFunnelUpCommand, parseFunnelPort } from '../lib/funnel.js';
|
|
2
|
+
import { buildFunnelDownCommand, buildFunnelStatusCommand, buildFunnelUpCommand, parseFunnelPort } from '../lib/funnel.js';
|
|
3
3
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
4
4
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
5
5
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
@@ -59,4 +59,19 @@ export function registerFunnelCommand(program) {
|
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
|
+
funnel
|
|
63
|
+
.command('down <host>')
|
|
64
|
+
.description('Disable Tailscale Funnel exposure for a public HTTPS port.')
|
|
65
|
+
.option('--port <n>', 'Public Funnel port: 443, 8443, or 10000', '443')
|
|
66
|
+
.action(async (host, opts) => {
|
|
67
|
+
try {
|
|
68
|
+
const publicPort = parseFunnelPort(opts.port ?? '443');
|
|
69
|
+
await runOnHost(host, buildFunnelDownCommand(publicPort));
|
|
70
|
+
console.log(chalk.green(`Funnel disabled on ${host}: public :${publicPort}`));
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(chalk.red(err.message));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
62
77
|
}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -32,6 +32,7 @@ import { listMcpServerConfigs, discoverMcpConfigsFromRepo } from '../lib/mcp.js'
|
|
|
32
32
|
import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups } from '../lib/plugins.js';
|
|
33
33
|
import { PLUGIN_GROUP_COLORS } from './plugins.js';
|
|
34
34
|
import { countSessionsInScope } from '../lib/session/discover.js';
|
|
35
|
+
import { isSessionTrackedAgent } from '../lib/session/types.js';
|
|
35
36
|
import { damerauLevenshtein } from '../lib/fuzzy.js';
|
|
36
37
|
import { terminalWidth, truncateToWidth, stringWidth, stripAnsi } from '../lib/session/width.js';
|
|
37
38
|
/** Resource kinds the inspect command can drill into. */
|
|
@@ -1256,14 +1257,11 @@ function safeStat(p) {
|
|
|
1256
1257
|
return null;
|
|
1257
1258
|
}
|
|
1258
1259
|
}
|
|
1259
|
-
const SESSION_AGENTS = new Set([
|
|
1260
|
-
'claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid',
|
|
1261
|
-
]);
|
|
1262
1260
|
function safeCountSessions(agent) {
|
|
1263
|
-
if (!
|
|
1261
|
+
if (!isSessionTrackedAgent(agent))
|
|
1264
1262
|
return 0;
|
|
1265
1263
|
try {
|
|
1266
|
-
return countSessionsInScope({ agent
|
|
1264
|
+
return countSessionsInScope({ agent });
|
|
1267
1265
|
}
|
|
1268
1266
|
catch {
|
|
1269
1267
|
return 0;
|
package/dist/commands/menubar.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* user; these commands are the manual override.
|
|
8
8
|
*/
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import {
|
|
10
|
+
import { setHelpSections } from '../lib/help.js';
|
|
11
|
+
import { enableMenubarService, disableMenubarService, getMenubarStatus, runMenubarSetup, } from '../lib/menubar/install-menubar.js';
|
|
11
12
|
function notMac() {
|
|
12
13
|
if (process.platform !== 'darwin') {
|
|
13
14
|
console.log(chalk.yellow('The menu bar helper is macOS only.'));
|
|
@@ -15,10 +16,122 @@ function notMac() {
|
|
|
15
16
|
}
|
|
16
17
|
return false;
|
|
17
18
|
}
|
|
19
|
+
/** Shared status readout — `status`, bare `menubar`, and `setup --check` all end here. */
|
|
20
|
+
function printStatus(s, opts = {}) {
|
|
21
|
+
const yn = (b) => (b ? chalk.green('yes') : chalk.gray('no'));
|
|
22
|
+
console.log(chalk.bold('Menu bar helper\n'));
|
|
23
|
+
console.log(` running ${yn(s.running)}`);
|
|
24
|
+
console.log(` service installed ${yn(s.serviceInstalled)}`);
|
|
25
|
+
if (opts.brief) {
|
|
26
|
+
console.log(chalk.gray('\n setup | enable | disable | status'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
|
|
30
|
+
console.log(` installed version ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
|
|
31
|
+
console.log(` current version ${chalk.gray(s.currentVersion)}`);
|
|
32
|
+
console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
|
|
33
|
+
console.log(` disabled by user ${yn(s.disabledByUser)}`);
|
|
34
|
+
// Two copies of the INSTALLED bundle is the duplicate the user sees as two
|
|
35
|
+
// agents marks in the menu bar. It used to read as a healthy `running: yes`.
|
|
36
|
+
if (s.instances.length > 1) {
|
|
37
|
+
console.log(chalk.yellow(`\n ${s.instances.length} copies of the installed helper are running — that is the duplicate menu-bar icon:`));
|
|
38
|
+
for (const p of s.instances)
|
|
39
|
+
console.log(chalk.gray(` ${p.pid} ${p.executable}`));
|
|
40
|
+
console.log(chalk.gray(' Fix it with `agents menubar setup`.'));
|
|
41
|
+
}
|
|
42
|
+
if (s.foreignInstances.length > 0) {
|
|
43
|
+
// RegisterEventHotKey is first-come, so the helper that registered the
|
|
44
|
+
// chord first owns Cmd-Shift-V/O. A process list cannot say which that
|
|
45
|
+
// was — only that a rival exists — so report the conflict, not a winner.
|
|
46
|
+
// The loser has no other symptom: its chords simply never fire.
|
|
47
|
+
const n = s.foreignInstances.length;
|
|
48
|
+
console.log(chalk.yellow(`\n ${n} other helper process${n === 1 ? '' : 'es'} running — ${n === 1 ? 'it' : 'they'} may hold Cmd-Shift-V/O instead of the installed one:`));
|
|
49
|
+
for (const p of s.foreignInstances)
|
|
50
|
+
console.log(chalk.gray(` ${p.pid} ${p.executable}`));
|
|
51
|
+
console.log(chalk.gray(' End them with `agents menubar setup`.'));
|
|
52
|
+
}
|
|
53
|
+
if (s.stale) {
|
|
54
|
+
console.log(chalk.yellow('\n Installed helper is stale — runs on next `agents` startup, or `agents menubar setup` now.'));
|
|
55
|
+
}
|
|
56
|
+
else if (!s.serviceInstalled && !s.disabledByUser) {
|
|
57
|
+
console.log(chalk.gray('\n Set it up with `agents menubar setup`.'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function printSetupResult(r) {
|
|
61
|
+
console.log(chalk.bold('Menu bar setup\n'));
|
|
62
|
+
for (const step of r.steps) {
|
|
63
|
+
const mark = step.outcome === 'failed' ? chalk.red('✗')
|
|
64
|
+
: step.outcome === 'changed' ? chalk.green('+') : chalk.green('✓');
|
|
65
|
+
console.log(` ${mark} ${step.name.padEnd(15)} ${chalk.gray(step.detail)}`);
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
if (r.configured) {
|
|
69
|
+
console.log(chalk.green('Menu bar configured.') + chalk.gray(' One agents mark, started at login.'));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(chalk.red('Menu bar not fully configured.') + chalk.gray(' See the failed step above.'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
18
75
|
export function registerMenubarCommands(program) {
|
|
19
76
|
const menubar = program
|
|
20
77
|
.command('menubar')
|
|
21
78
|
.description('Manage the macOS menu-bar helper (running sessions, agents awaiting input, routines)');
|
|
79
|
+
// `setup` is the one command that gets a machine to the intended state:
|
|
80
|
+
// exactly one status item, started at login. `enable` stays the narrow
|
|
81
|
+
// install+start; setup adds duplicate cleanup and verifies the end state.
|
|
82
|
+
const setup = menubar
|
|
83
|
+
.command('setup')
|
|
84
|
+
.description('Configure the menu bar end-to-end: one instance, started at login')
|
|
85
|
+
.option('--check', 'Report the current state, change nothing')
|
|
86
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
87
|
+
.action((options) => {
|
|
88
|
+
if (options.check) {
|
|
89
|
+
const s = getMenubarStatus();
|
|
90
|
+
if (options.json) {
|
|
91
|
+
process.stdout.write(JSON.stringify(s) + '\n');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (notMac())
|
|
95
|
+
return;
|
|
96
|
+
printStatus(s);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!options.json && notMac())
|
|
100
|
+
return;
|
|
101
|
+
const r = runMenubarSetup();
|
|
102
|
+
if (options.json) {
|
|
103
|
+
process.stdout.write(JSON.stringify(r) + '\n');
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
printSetupResult(r);
|
|
107
|
+
}
|
|
108
|
+
if (!r.configured)
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
});
|
|
111
|
+
setHelpSections(setup, {
|
|
112
|
+
examples: `
|
|
113
|
+
# Configure the menu bar end-to-end (idempotent — safe to re-run)
|
|
114
|
+
agents menubar setup
|
|
115
|
+
|
|
116
|
+
# Two agents marks in the menu bar? This ends the duplicate.
|
|
117
|
+
agents menubar setup
|
|
118
|
+
|
|
119
|
+
# See the current state without changing anything
|
|
120
|
+
agents menubar setup --check
|
|
121
|
+
`,
|
|
122
|
+
notes: `
|
|
123
|
+
Configures, in order: every running helper ended, the helper bundle at
|
|
124
|
+
~/Library/Application Support/agents-cli, its code signature, the launchd
|
|
125
|
+
login item (com.phnx-labs.agents-menubar — RunAtLoad + KeepAlive), then
|
|
126
|
+
verifies exactly one helper came back up.
|
|
127
|
+
|
|
128
|
+
Every running helper is ended and launchd restarts one, so the survivor is
|
|
129
|
+
always the login-managed copy. Exits nonzero if it cannot reach that state.
|
|
130
|
+
|
|
131
|
+
Setup clears a previous \`agents menubar disable\`. To turn the menu bar off
|
|
132
|
+
again, run \`agents menubar disable\`.
|
|
133
|
+
`,
|
|
134
|
+
});
|
|
22
135
|
menubar
|
|
23
136
|
.command('enable')
|
|
24
137
|
.description('Install and start the menu-bar helper (launches at login)')
|
|
@@ -40,7 +153,7 @@ export function registerMenubarCommands(program) {
|
|
|
40
153
|
if (notMac())
|
|
41
154
|
return;
|
|
42
155
|
disableMenubarService();
|
|
43
|
-
console.log(chalk.green('Menu bar helper disabled.') + chalk.gray(' Re-enable any time with `agents menubar
|
|
156
|
+
console.log(chalk.green('Menu bar helper disabled.') + chalk.gray(' Re-enable any time with `agents menubar setup`.'));
|
|
44
157
|
});
|
|
45
158
|
menubar
|
|
46
159
|
.command('status')
|
|
@@ -56,33 +169,7 @@ export function registerMenubarCommands(program) {
|
|
|
56
169
|
console.log(chalk.yellow('The menu bar helper is macOS only.'));
|
|
57
170
|
return;
|
|
58
171
|
}
|
|
59
|
-
|
|
60
|
-
console.log(chalk.bold('Menu bar helper\n'));
|
|
61
|
-
console.log(` running ${yn(s.running)}`);
|
|
62
|
-
console.log(` service installed ${yn(s.serviceInstalled)}`);
|
|
63
|
-
console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
|
|
64
|
-
console.log(` installed version ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
|
|
65
|
-
console.log(` current version ${chalk.gray(s.currentVersion)}`);
|
|
66
|
-
console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
|
|
67
|
-
console.log(` disabled by user ${yn(s.disabledByUser)}`);
|
|
68
|
-
if (s.foreignInstances.length > 0) {
|
|
69
|
-
// RegisterEventHotKey is first-come, so the helper that registered the
|
|
70
|
-
// chord first owns Cmd-Shift-V/O. A process list cannot say which that
|
|
71
|
-
// was — only that a rival exists — so report the conflict, not a
|
|
72
|
-
// winner. The loser
|
|
73
|
-
// has no other symptom: its chords simply never fire.
|
|
74
|
-
const n = s.foreignInstances.length;
|
|
75
|
-
console.log(chalk.yellow(`\n ${n} other helper process${n === 1 ? '' : 'es'} running — ${n === 1 ? 'it' : 'they'} may hold Cmd-Shift-V/O instead of the installed one:`));
|
|
76
|
-
for (const p of s.foreignInstances)
|
|
77
|
-
console.log(chalk.gray(` ${p.pid} ${p.executable}`));
|
|
78
|
-
console.log(chalk.gray(' End it, then `agents menubar enable` to restart the installed helper.'));
|
|
79
|
-
}
|
|
80
|
-
if (s.stale) {
|
|
81
|
-
console.log(chalk.yellow('\n Installed helper is stale — runs on next `agents` startup, or `agents menubar enable` now.'));
|
|
82
|
-
}
|
|
83
|
-
else if (!s.serviceInstalled && !s.disabledByUser) {
|
|
84
|
-
console.log(chalk.gray('\n Enable it with `agents menubar enable`.'));
|
|
85
|
-
}
|
|
172
|
+
printStatus(s);
|
|
86
173
|
});
|
|
87
174
|
// Bare `agents menubar` -> status.
|
|
88
175
|
menubar.action(() => {
|
|
@@ -91,10 +178,6 @@ export function registerMenubarCommands(program) {
|
|
|
91
178
|
console.log(chalk.yellow('The menu bar helper is macOS only.'));
|
|
92
179
|
return;
|
|
93
180
|
}
|
|
94
|
-
|
|
95
|
-
console.log(chalk.bold('Menu bar helper\n'));
|
|
96
|
-
console.log(` running ${yn(s.running)}`);
|
|
97
|
-
console.log(` service installed ${yn(s.serviceInstalled)}`);
|
|
98
|
-
console.log(chalk.gray('\n enable | disable | status'));
|
|
181
|
+
printStatus(s, { brief: true });
|
|
99
182
|
});
|
|
100
183
|
}
|
|
@@ -19,7 +19,7 @@ import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js'
|
|
|
19
19
|
import { getRoutinesDir } from '../lib/state.js';
|
|
20
20
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
21
21
|
import { safeJoin } from '../lib/paths.js';
|
|
22
|
-
import { executeJob, executeJobDetached, monitorRunningJobs } from '../lib/runner.js';
|
|
22
|
+
import { executeJob, executeJobDetached, monitorRunningJobs, ROUTINE_AGENT_IDS } from '../lib/runner.js';
|
|
23
23
|
import { JobScheduler } from '../lib/scheduler.js';
|
|
24
24
|
import { detectOverdueJobs } from '../lib/overdue.js';
|
|
25
25
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
@@ -70,6 +70,8 @@ function fireConditionLabel(job) {
|
|
|
70
70
|
job.trigger.action ? `action=${job.trigger.action}` : null,
|
|
71
71
|
job.trigger.teamKey ? `team=${job.trigger.teamKey}` : null,
|
|
72
72
|
job.trigger.label ? `label=${job.trigger.label}` : null,
|
|
73
|
+
job.trigger.stateTo ? `stateTo=${job.trigger.stateTo}` : null,
|
|
74
|
+
job.trigger.stateFrom ? `stateFrom=${job.trigger.stateFrom}` : null,
|
|
73
75
|
].filter(Boolean).join(', ');
|
|
74
76
|
return `on linear:${job.trigger.event}${filters ? ` (${filters})` : ''}`;
|
|
75
77
|
}
|
|
@@ -261,6 +263,10 @@ function parseRoutineTrigger(options) {
|
|
|
261
263
|
trigger.teamKey = options.teamKey;
|
|
262
264
|
if (typeof options.label === 'string')
|
|
263
265
|
trigger.label = options.label;
|
|
266
|
+
if (typeof options.stateTo === 'string')
|
|
267
|
+
trigger.stateTo = options.stateTo;
|
|
268
|
+
if (typeof options.stateFrom === 'string')
|
|
269
|
+
trigger.stateFrom = options.stateFrom;
|
|
264
270
|
return trigger;
|
|
265
271
|
}
|
|
266
272
|
throw new Error('--on source must be github or linear');
|
|
@@ -572,7 +578,7 @@ export function registerRoutinesCommands(program) {
|
|
|
572
578
|
.command('add [nameOrPath]')
|
|
573
579
|
.description('Create a new routine from a YAML file or inline flags. Starts the scheduler automatically if it is not already running.')
|
|
574
580
|
.option('-s, --schedule <cron>', 'Cron schedule in standard format (5 fields: minute hour day month weekday)')
|
|
575
|
-
.option('-a, --agent <agent>',
|
|
581
|
+
.option('-a, --agent <agent>', `Which agent runs this routine: ${ROUTINE_AGENT_IDS.join(', ')}`)
|
|
576
582
|
.option('--workflow <name>', 'Run an installed workflow (~/.agents/workflows/<name>) via `agents run`. Mutually exclusive with --agent.')
|
|
577
583
|
.option('--command <sh>', 'Run a plain shell command directly (no agent, no auth, no sandbox) — for deterministic housekeeping routines. Mutually exclusive with --agent and --workflow; --prompt is not used.')
|
|
578
584
|
.option('-p, --prompt <prompt>', 'Task instruction for the agent')
|
|
@@ -591,6 +597,8 @@ export function registerRoutinesCommands(program) {
|
|
|
591
597
|
.option('--action <name>', 'Webhook action filter for --on triggers (GitHub: labeled/opened; Linear: update)')
|
|
592
598
|
.option('--team-key <key>', 'Linear team key filter for --on linear:<event> (e.g. RUSH)')
|
|
593
599
|
.option('--label <name>', 'Label filter for --on triggers (GitHub label name or Linear issue label)')
|
|
600
|
+
.option('--state-to <name>', 'Linear current-state filter for --on linear:<event> (e.g. Plan)')
|
|
601
|
+
.option('--state-from <name>', 'Linear previous-state filter for --on linear:<event> (e.g. Triage)')
|
|
594
602
|
.option('--end-at <iso>', 'Stop firing on or after this ISO 8601 timestamp (e.g., "2026-12-31T23:59:00Z"); routine auto-disables.')
|
|
595
603
|
.option('--disabled', 'Create the routine but keep it paused (enable later with resume)')
|
|
596
604
|
.option('--resume <sessionId>', 'At fire time, resume this existing session id (via `agents run <agent> --resume`) instead of starting fresh — the actual session reopens with full context and the prompt becomes its next turn. Powers self-scheduled wake-ups (e.g. /hibernate). Requires --agent claude or codex; runs un-sandboxed (the session store lives in the real home, not the job overlay).')
|
|
@@ -884,12 +892,26 @@ export function registerRoutinesCommands(program) {
|
|
|
884
892
|
routinesCmd
|
|
885
893
|
.command('edit [name]')
|
|
886
894
|
.description('Open a routine in $EDITOR. Creates a new YAML template if the routine does not exist.')
|
|
887
|
-
.
|
|
895
|
+
.option('--state-to <name>', 'Update the Linear current-state filter before opening the editor')
|
|
896
|
+
.option('--state-from <name>', 'Update the Linear previous-state filter before opening the editor')
|
|
897
|
+
.action(async (name, options) => {
|
|
888
898
|
if (!name) {
|
|
889
899
|
name = await pickJob('Select job to edit', undefined, ['agents routines edit <name>']) ?? undefined;
|
|
890
900
|
if (!name)
|
|
891
901
|
return;
|
|
892
902
|
}
|
|
903
|
+
const existing = readJob(name);
|
|
904
|
+
if (existing && (options.stateTo !== undefined || options.stateFrom !== undefined)) {
|
|
905
|
+
if (!existing.trigger || existing.trigger.type !== 'linear_event') {
|
|
906
|
+
console.error(chalk.red(`'${name}' does not have a Linear trigger; --state-to/--state-from only apply to linear triggers`));
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
909
|
+
if (options.stateTo !== undefined)
|
|
910
|
+
existing.trigger.stateTo = options.stateTo || undefined;
|
|
911
|
+
if (options.stateFrom !== undefined)
|
|
912
|
+
existing.trigger.stateFrom = options.stateFrom || undefined;
|
|
913
|
+
writeJob(existing);
|
|
914
|
+
}
|
|
893
915
|
const jobPath = getJobPath(name);
|
|
894
916
|
if (!jobPath) {
|
|
895
917
|
// Job doesn't exist - create a new one
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents secrets rotate-passphrase` — rotate the encrypted file store's
|
|
3
|
+
* machine-local master passphrase (RUSH-1975).
|
|
4
|
+
*
|
|
5
|
+
* Re-encrypts every `<item>.enc` under a freshly generated key and rewrites the
|
|
6
|
+
* 0600 key file in place, atomically: the new store is staged, verified
|
|
7
|
+
* (round-trip + count), fsync'd, then swapped by directory rename, so a crash
|
|
8
|
+
* leaves the old store intact and readable. Headless-safe and Linux-first — the
|
|
9
|
+
* remediation path for a compromised passphrase (RUSH-1968), where the two
|
|
10
|
+
* supported alternatives are unacceptable (a hand-rolled non-atomic script, or
|
|
11
|
+
* export-to-plaintext which is the exposure being fixed).
|
|
12
|
+
*
|
|
13
|
+
* Dry-run by default, matching `import-keyring`; `--commit` performs the swap.
|
|
14
|
+
*/
|
|
15
|
+
import type { Command } from 'commander';
|
|
16
|
+
/** Register `agents secrets rotate-passphrase` on the parent secrets Command. */
|
|
17
|
+
export declare function registerSecretsRotatePassphraseCommand(secrets: Command): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents secrets rotate-passphrase` — rotate the encrypted file store's
|
|
3
|
+
* machine-local master passphrase (RUSH-1975).
|
|
4
|
+
*
|
|
5
|
+
* Re-encrypts every `<item>.enc` under a freshly generated key and rewrites the
|
|
6
|
+
* 0600 key file in place, atomically: the new store is staged, verified
|
|
7
|
+
* (round-trip + count), fsync'd, then swapped by directory rename, so a crash
|
|
8
|
+
* leaves the old store intact and readable. Headless-safe and Linux-first — the
|
|
9
|
+
* remediation path for a compromised passphrase (RUSH-1968), where the two
|
|
10
|
+
* supported alternatives are unacceptable (a hand-rolled non-atomic script, or
|
|
11
|
+
* export-to-plaintext which is the exposure being fixed).
|
|
12
|
+
*
|
|
13
|
+
* Dry-run by default, matching `import-keyring`; `--commit` performs the swap.
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { machinePassphraseExists, rotatePassphrase } from '../lib/secrets/filestore.js';
|
|
17
|
+
import { agentStatus } from '../lib/secrets/agent.js';
|
|
18
|
+
/** Register `agents secrets rotate-passphrase` on the parent secrets Command. */
|
|
19
|
+
export function registerSecretsRotatePassphraseCommand(secrets) {
|
|
20
|
+
secrets
|
|
21
|
+
.command('rotate-passphrase')
|
|
22
|
+
.description('Re-key the encrypted file store under a new machine-local passphrase (atomic, headless-safe). Dry-run by default.')
|
|
23
|
+
.option('--commit', 'Perform the rotation (default is dry-run reporting only)')
|
|
24
|
+
.option('--dry-run', 'Report bundle count and round-trip result without re-keying (the default). Still heals an interrupted rotation — that is the one thing it writes.')
|
|
25
|
+
.option('--force', 'Override the safety refusals (held broker unlocks, or a passphrase exported in the environment)')
|
|
26
|
+
.addHelpText('after', `
|
|
27
|
+
Rotates the auto-provisioned file-store key at ~/.agents/.secrets-key/passphrase:
|
|
28
|
+
decrypts every item under the current key, re-encrypts under a new one, verifies
|
|
29
|
+
every item round-trips, then swaps the store and key file atomically. A crash
|
|
30
|
+
before the swap leaves the old store readable with the old key; a crash inside the
|
|
31
|
+
swap self-heals on the next rotate-passphrase run (not on an ordinary get). No
|
|
32
|
+
plaintext secret value or passphrase is ever written to disk, argv, or a log.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
# Report what would rotate (no writes)
|
|
36
|
+
agents secrets rotate-passphrase
|
|
37
|
+
|
|
38
|
+
# Perform the rotation
|
|
39
|
+
agents secrets rotate-passphrase --commit`)
|
|
40
|
+
.action(async (opts) => {
|
|
41
|
+
try {
|
|
42
|
+
if (opts.commit && opts.dryRun) {
|
|
43
|
+
throw new Error('--commit and --dry-run are mutually exclusive.');
|
|
44
|
+
}
|
|
45
|
+
const dryRun = !opts.commit;
|
|
46
|
+
if (!machinePassphraseExists()) {
|
|
47
|
+
throw new Error('No machine-local passphrase is provisioned on this box, so there is nothing to rotate. ' +
|
|
48
|
+
'This command re-keys the file store\'s auto-provisioned key at ~/.agents/.secrets-key/passphrase.');
|
|
49
|
+
}
|
|
50
|
+
// Guard 1: refuse while the secrets-agent holds live unlocks (macOS), so a
|
|
51
|
+
// concurrent read cannot land against a half-swapped store. --force overrides.
|
|
52
|
+
if (!dryRun && !opts.force) {
|
|
53
|
+
const held = await agentStatus();
|
|
54
|
+
if (held.length > 0) {
|
|
55
|
+
throw new Error(`The secrets-agent is holding ${held.length} unlocked bundle(s) (${held.map((e) => e.name).join(', ')}). ` +
|
|
56
|
+
'Lock them first (`agents secrets lock --all`) so no concurrent read races the rotation, or pass --force.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Guard 2: a passphrase exported in the environment shadows the on-disk key
|
|
60
|
+
// file with a now-stale value after the rotation, breaking every read. This
|
|
61
|
+
// is the exact RUSH-1968 footgun — refuse the commit and point at the fix.
|
|
62
|
+
if (!dryRun && !opts.force && (process.env.AGENTS_SECRETS_PASSPHRASE ?? '').length > 0) {
|
|
63
|
+
throw new Error('AGENTS_SECRETS_PASSPHRASE is set in this environment; it would shadow the rotated key file ' +
|
|
64
|
+
'with a stale value and break every read. Unset it (this is the RUSH-1968 fix) before rotating, or pass --force.');
|
|
65
|
+
}
|
|
66
|
+
const report = rotatePassphrase({ dryRun });
|
|
67
|
+
console.log(`${chalk.bold(String(report.bundleCount))} item(s) decrypt under the current key and ` +
|
|
68
|
+
`${report.roundTripOk ? chalk.green('round-trip cleanly') : chalk.red('failed to round-trip')} under a new key.`);
|
|
69
|
+
if (report.skipped.length > 0) {
|
|
70
|
+
console.log(chalk.gray(`skipped ${report.skipped.length} orphan file(s) (not re-keyed):`));
|
|
71
|
+
for (const s of report.skipped)
|
|
72
|
+
console.log(chalk.gray(` - ${s}`));
|
|
73
|
+
}
|
|
74
|
+
if (report.dryRun) {
|
|
75
|
+
if (report.recoveredInterruptedRotation) {
|
|
76
|
+
// Be precise: this run DID write. Recovery is deliberately not gated on
|
|
77
|
+
// --commit, because healing is how a crashed store becomes readable
|
|
78
|
+
// again without re-keying it.
|
|
79
|
+
console.log(chalk.yellow('Recovered an interrupted rotation: the store was healed back to a single ' +
|
|
80
|
+
'readable state. That is the only thing this dry run wrote — no re-keying happened.'));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(chalk.gray('Dry-run: nothing written.'));
|
|
84
|
+
}
|
|
85
|
+
console.log(chalk.gray(`Pass --commit to re-encrypt the store and swap the key file (${report.keyFilePath}).`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log(chalk.green(`Rotated: re-encrypted ${report.bundleCount} item(s) and rewrote ${report.keyFilePath} (mode 0600).`));
|
|
89
|
+
console.log(chalk.gray('The previous key and ciphertext were removed. Any process still holding the old passphrase in its environment must be restarted.'));
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.error(chalk.red(err.message));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -38,6 +38,7 @@ import { getVaultSession, vaultExists } from '../lib/secrets/vault.js';
|
|
|
38
38
|
import { registerSecretsSyncCommands } from './secrets-sync.js';
|
|
39
39
|
import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
|
|
40
40
|
import { registerSecretsImportKeyringCommand } from './secrets-import.js';
|
|
41
|
+
import { registerSecretsRotatePassphraseCommand } from './secrets-rotate-passphrase.js';
|
|
41
42
|
/** Prompt the user for a secret value with masked input. Requires an interactive TTY. */
|
|
42
43
|
async function promptForSecret(message) {
|
|
43
44
|
if (!isInteractiveTerminal()) {
|
|
@@ -2419,6 +2420,7 @@ Examples:
|
|
|
2419
2420
|
registerSecretsSyncCommands(cmd);
|
|
2420
2421
|
registerSecretsMigrateAclCommand(cmd);
|
|
2421
2422
|
registerSecretsImportKeyringCommand(cmd);
|
|
2423
|
+
registerSecretsRotatePassphraseCommand(cmd);
|
|
2422
2424
|
}
|
|
2423
2425
|
/** Validate a prompt-policy value, throwing a clear message on a bad one (the
|
|
2424
2426
|
* caller's try/catch renders it and exits). Accepts the legacy `biometry` /
|
|
@@ -136,11 +136,17 @@ export declare function linkCwdCell(s: Pick<SessionMeta, 'cwd' | '_remote'>, lab
|
|
|
136
136
|
* (null when unknown) alongside the raw fields, so every active row is joinable.
|
|
137
137
|
* `project` uses the same derivation SessionMeta does — basename(cwd) (see
|
|
138
138
|
* discover.ts) — so the active view and the history view join identically.
|
|
139
|
+
*
|
|
140
|
+
* `viewingIn` flattens to the same display string the row renderer prints —
|
|
141
|
+
* `'codium tab 3'` / `'detached'` / null — so a consumer can tell a watched
|
|
142
|
+
* session from an orphaned one (its terminal died, the agent is still running)
|
|
143
|
+
* without re-implementing the tmux client lookup.
|
|
139
144
|
*/
|
|
140
|
-
export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<ActiveSession & {
|
|
145
|
+
export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<Omit<ActiveSession, 'viewingIn'> & {
|
|
141
146
|
ticketId: string | null;
|
|
142
147
|
project: string | null;
|
|
143
148
|
prLink: string | null;
|
|
149
|
+
viewingIn: string | null;
|
|
144
150
|
}>;
|
|
145
151
|
/**
|
|
146
152
|
* Compact owner display for the `--active` owner column: the local-part of a
|