@phnx-labs/agents-cli 1.20.78 → 1.20.83
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 +595 -0
- package/README.md +26 -26
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +66 -0
- package/dist/commands/doctor.js +596 -33
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +24 -5
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/menubar.js +12 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +187 -11
- package/dist/commands/routines.d.ts +8 -1
- package/dist/commands/routines.js +241 -77
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +100 -32
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.js +32 -3
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +115 -0
- package/dist/commands/sessions.js +189 -35
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +52 -8
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -2
- package/dist/lib/agents.js +72 -63
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +18 -0
- package/dist/lib/daemon.js +47 -6
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.d.ts +7 -0
- package/dist/lib/exec.js +69 -11
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +14 -14
- package/dist/lib/git.js +41 -44
- package/dist/lib/hooks.d.ts +71 -0
- package/dist/lib/hooks.js +245 -95
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +24 -0
- package/dist/lib/menubar/install-menubar.js +52 -2
- package/dist/lib/menubar/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +33 -0
- package/dist/lib/routines.js +122 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +49 -10
- package/dist/lib/scheduler.js +16 -4
- 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/agent.d.ts +16 -0
- package/dist/lib/secrets/agent.js +105 -3
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +11 -8
- package/dist/lib/secrets/bundles.js +31 -23
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +128 -26
- package/dist/lib/session/active.js +192 -53
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +4 -1
- package/dist/lib/session/db.js +65 -8
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +13 -0
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/state.js +57 -1
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
|
@@ -24,6 +24,9 @@ import { dispatchAgentsCommand, withActorEnv } from './dispatch.js';
|
|
|
24
24
|
import { stripRoutingFlags, buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, } from './remote-cmd.js';
|
|
25
25
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
26
26
|
import { machineId } from '../session/sync/config.js';
|
|
27
|
+
import { loadDevices } from '../devices/registry.js';
|
|
28
|
+
import { fanOutDevices, planFleetTargets, runLocalCommand, runOnDevice, } from '../devices/fleet.js';
|
|
29
|
+
import { platformGroupLabel } from '../devices/health-report.js';
|
|
27
30
|
/**
|
|
28
31
|
* First-class groups that run transparently on a remote via SSH when
|
|
29
32
|
* `--host`/`--device` is present. Keep both canonical names and aliases
|
|
@@ -102,6 +105,7 @@ const OWN_HOST_COMMANDS = new Set([
|
|
|
102
105
|
'harnesses',
|
|
103
106
|
'sessions',
|
|
104
107
|
'feed',
|
|
108
|
+
'activity', // fans `--host`/`--device`/`--devices-all` out itself (feed-style)
|
|
105
109
|
'computer',
|
|
106
110
|
'secrets',
|
|
107
111
|
'logs',
|
|
@@ -112,8 +116,15 @@ const OWN_HOST_COMMANDS = new Set([
|
|
|
112
116
|
'apply', // `--device` scopes the fleet reconcile to one device (it targets devices itself)
|
|
113
117
|
'monitors', // `--device` names the OWNER machine (pin-to-one), not a routing target
|
|
114
118
|
]);
|
|
115
|
-
/** `--no-tty` is stripped like the routing flags but carries no value.
|
|
116
|
-
|
|
119
|
+
/** `--no-tty` is stripped like the routing flags but carries no value. The plural
|
|
120
|
+
* fleet flags are stripped only when we handle the `all` sentinel ourselves;
|
|
121
|
+
* otherwise they fall through to command-level aggregators. */
|
|
122
|
+
const STRIP_SPECS = [
|
|
123
|
+
...HOST_ROUTING_SPECS,
|
|
124
|
+
{ long: 'no-tty', takesValue: false },
|
|
125
|
+
{ long: 'hosts', takesValue: true },
|
|
126
|
+
{ long: 'devices', takesValue: true },
|
|
127
|
+
];
|
|
117
128
|
/** Pull the value of `--host`/`-H`/`--remote-cwd` (any form) out of an argv. */
|
|
118
129
|
export function flagValue(args, long, short) {
|
|
119
130
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -155,6 +166,201 @@ async function resolveTargetHost(name, any) {
|
|
|
155
166
|
assertValidSshTarget(name); // rejects injection / flag-smuggling before it reaches ssh
|
|
156
167
|
return syntheticHost(name);
|
|
157
168
|
}
|
|
169
|
+
/** Detect the `all` sentinel on any routing flag. */
|
|
170
|
+
function isFleetAllSentinel(hostFlag, deviceFlag, hostsFlag, devicesFlag) {
|
|
171
|
+
return (hostFlag?.toLowerCase() === 'all' ||
|
|
172
|
+
deviceFlag?.toLowerCase() === 'all' ||
|
|
173
|
+
hostsFlag?.toLowerCase() === 'all' ||
|
|
174
|
+
devicesFlag?.toLowerCase() === 'all');
|
|
175
|
+
}
|
|
176
|
+
/** Strip routing flags from the argv and ensure the per-device call emits JSON. */
|
|
177
|
+
function buildFleetForwardedArgs(allArgs) {
|
|
178
|
+
const stripped = stripRoutingFlags(allArgs, STRIP_SPECS);
|
|
179
|
+
if (!stripped.includes('--json'))
|
|
180
|
+
stripped.push('--json');
|
|
181
|
+
return stripped;
|
|
182
|
+
}
|
|
183
|
+
/** Parse stdout as JSON; on failure return an object describing the error. */
|
|
184
|
+
function safeJsonParse(stdout) {
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(stdout);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return { parseError: 'invalid JSON', snippet: stdout.trim().slice(0, 200) };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** One-line summary of a per-device `agents view [agent] --json` payload. */
|
|
193
|
+
function summarizeViewResult(forwarded, json) {
|
|
194
|
+
// After routing flags are stripped, the agent argument is the first token that
|
|
195
|
+
// is not a flag (e.g. `kimi` in `agents view kimi --json`).
|
|
196
|
+
const agentArg = forwarded.find((a, i) => i > 0 && !a.startsWith('-'));
|
|
197
|
+
// `agents view --json` returns an array; `agents view <agent> --json` returns
|
|
198
|
+
// a single object. Normalize to the per-agent shape.
|
|
199
|
+
const agent = agentArg
|
|
200
|
+
? (Array.isArray(json) ? json[0] : json)
|
|
201
|
+
: undefined;
|
|
202
|
+
if (!agentArg) {
|
|
203
|
+
const rows = Array.isArray(json) ? json : [];
|
|
204
|
+
const count = rows.reduce((n, r) => n + (r.versions?.length ?? 0), 0);
|
|
205
|
+
return count === 0 ? 'no agents installed' : `${count} version${count === 1 ? '' : 's'}`;
|
|
206
|
+
}
|
|
207
|
+
if (!agent || !Array.isArray(agent.versions) || agent.versions.length === 0) {
|
|
208
|
+
return 'not installed';
|
|
209
|
+
}
|
|
210
|
+
const v = agent.versions.find((x) => x.isDefault) ?? agent.versions[0];
|
|
211
|
+
const parts = [chalk.cyan(String(v.version))];
|
|
212
|
+
if (v.signedIn) {
|
|
213
|
+
parts.push(chalk.green('active'));
|
|
214
|
+
if (v.email)
|
|
215
|
+
parts.push(chalk.gray(String(v.email)));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
parts.push(chalk.gray('signed out'));
|
|
219
|
+
}
|
|
220
|
+
return parts.join(' · ');
|
|
221
|
+
}
|
|
222
|
+
function formatCompactUsd(usd) {
|
|
223
|
+
if (usd >= 1000)
|
|
224
|
+
return `$${(usd / 1000).toFixed(1)}k`;
|
|
225
|
+
if (usd >= 1)
|
|
226
|
+
return `$${usd.toFixed(2)}`;
|
|
227
|
+
return `$${usd.toFixed(3)}`;
|
|
228
|
+
}
|
|
229
|
+
function formatCompactTokens(n) {
|
|
230
|
+
const abs = Math.abs(n);
|
|
231
|
+
if (abs >= 1e9)
|
|
232
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
233
|
+
if (abs >= 1e6)
|
|
234
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
235
|
+
if (abs >= 1e3)
|
|
236
|
+
return `${(n / 1e3).toFixed(1)}K`;
|
|
237
|
+
return String(n);
|
|
238
|
+
}
|
|
239
|
+
/** One-line summary of a per-device `agents output --json` payload. */
|
|
240
|
+
function summarizeOutputResult(json) {
|
|
241
|
+
const p = json;
|
|
242
|
+
const burn = p?.burn;
|
|
243
|
+
if (!burn || typeof burn !== 'object')
|
|
244
|
+
return 'ok';
|
|
245
|
+
const parts = [];
|
|
246
|
+
if (typeof burn.costUsd === 'number')
|
|
247
|
+
parts.push(`${formatCompactUsd(burn.costUsd)} burned`);
|
|
248
|
+
if (typeof burn.outputTokens === 'number')
|
|
249
|
+
parts.push(`${formatCompactTokens(burn.outputTokens)} output tokens`);
|
|
250
|
+
const commits = p?.output?.commits;
|
|
251
|
+
if (typeof commits === 'number' && commits > 0)
|
|
252
|
+
parts.push(`${commits} commits`);
|
|
253
|
+
return parts.length ? parts.join(' · ') : 'ok';
|
|
254
|
+
}
|
|
255
|
+
/** Best-effort summary of any per-device JSON payload. */
|
|
256
|
+
function summarizeResult(command, forwarded, json) {
|
|
257
|
+
if (command === 'view')
|
|
258
|
+
return summarizeViewResult(forwarded, json);
|
|
259
|
+
if (command === 'output')
|
|
260
|
+
return summarizeOutputResult(json);
|
|
261
|
+
return 'ok';
|
|
262
|
+
}
|
|
263
|
+
const GROUP_ORDER = ['macOS', 'Linux', 'Windows', 'Other'];
|
|
264
|
+
/** Render the grouped-by-OS fleet roster from per-device results. */
|
|
265
|
+
function renderFleetRoster(command, forwarded, results, self) {
|
|
266
|
+
const agentArg = forwarded[1];
|
|
267
|
+
const installed = results.filter((r) => r.status === 'ok').length;
|
|
268
|
+
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
269
|
+
const failed = results.filter((r) => r.status === 'failed').length;
|
|
270
|
+
const title = agentArg ? `${command} ${agentArg}` : command;
|
|
271
|
+
const summaryParts = [];
|
|
272
|
+
if (installed)
|
|
273
|
+
summaryParts.push(`${installed} installed`);
|
|
274
|
+
if (failed)
|
|
275
|
+
summaryParts.push(`${failed} unreachable`);
|
|
276
|
+
if (skipped)
|
|
277
|
+
summaryParts.push(`${skipped} skipped`);
|
|
278
|
+
console.log(chalk.bold(title) + chalk.gray(` · ${results.length} device${results.length === 1 ? '' : 's'}`));
|
|
279
|
+
console.log('');
|
|
280
|
+
const nameW = Math.max(6, ...results.map((r) => r.name.length));
|
|
281
|
+
const grouped = new Map();
|
|
282
|
+
for (const r of results) {
|
|
283
|
+
const g = platformGroupLabel(r.device.platform);
|
|
284
|
+
(grouped.get(g) ?? grouped.set(g, []).get(g)).push(r);
|
|
285
|
+
}
|
|
286
|
+
for (const group of GROUP_ORDER) {
|
|
287
|
+
const members = grouped.get(group);
|
|
288
|
+
if (!members || members.length === 0)
|
|
289
|
+
continue;
|
|
290
|
+
members.sort((a, b) => {
|
|
291
|
+
const aSelf = a.name.toLowerCase() === self.toLowerCase();
|
|
292
|
+
const bSelf = b.name.toLowerCase() === self.toLowerCase();
|
|
293
|
+
return (aSelf ? -1 : bSelf ? 1 : 0) || a.name.localeCompare(b.name);
|
|
294
|
+
});
|
|
295
|
+
console.log(chalk.bold(group));
|
|
296
|
+
for (const r of members) {
|
|
297
|
+
const isSelf = r.name.toLowerCase() === self.toLowerCase();
|
|
298
|
+
const prefix = isSelf ? chalk.cyan('▸') : ' ';
|
|
299
|
+
let glyph;
|
|
300
|
+
let text;
|
|
301
|
+
if (r.status === 'skipped') {
|
|
302
|
+
glyph = chalk.gray('○');
|
|
303
|
+
text = chalk.gray(String(r.reason ?? 'skipped'));
|
|
304
|
+
}
|
|
305
|
+
else if (r.status === 'failed') {
|
|
306
|
+
glyph = chalk.red('✕');
|
|
307
|
+
text = chalk.red(String(r.error ?? 'unreachable').split('\n')[0].slice(0, 80));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
glyph = chalk.green('●');
|
|
311
|
+
text = summarizeResult(command, forwarded, r.value);
|
|
312
|
+
}
|
|
313
|
+
const selfNote = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
314
|
+
console.log(` ${prefix} ${r.name.padEnd(nameW)} ${glyph} ${text}${selfNote}`);
|
|
315
|
+
}
|
|
316
|
+
console.log('');
|
|
317
|
+
}
|
|
318
|
+
if (summaryParts.length) {
|
|
319
|
+
console.log(chalk.gray(summaryParts.join(' · ')));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Run `agents <command> …` across every registered device and render the roster. */
|
|
323
|
+
export async function runFleetPassthrough(command, allArgs, spec, opts = {}) {
|
|
324
|
+
const self = opts.self ?? machineId();
|
|
325
|
+
const registry = await (opts.loadDevices ?? loadDevices)();
|
|
326
|
+
const planned = planFleetTargets(registry);
|
|
327
|
+
const targets = planned.map((t) => ({
|
|
328
|
+
name: t.device.name,
|
|
329
|
+
device: t.device,
|
|
330
|
+
skip: t.skip,
|
|
331
|
+
}));
|
|
332
|
+
const forwarded = buildFleetForwardedArgs(allArgs);
|
|
333
|
+
const runner = opts.runner ?? runOnDevice;
|
|
334
|
+
const localRunner = opts.localRunner ?? runLocalCommand;
|
|
335
|
+
const results = await fanOutDevices(targets, async (target) => {
|
|
336
|
+
const cmd = ['agents', ...forwarded];
|
|
337
|
+
const isSelf = target.device.name.toLowerCase() === self.toLowerCase();
|
|
338
|
+
const res = isSelf ? localRunner(cmd) : runner(target.device, cmd);
|
|
339
|
+
if (res.code !== 0) {
|
|
340
|
+
const detail = (res.stderr || res.stdout || 'unreachable').trim().slice(0, 200);
|
|
341
|
+
throw new Error(detail || 'unreachable');
|
|
342
|
+
}
|
|
343
|
+
return safeJsonParse(res.stdout);
|
|
344
|
+
}, { perDeviceTimeoutMs: 120_000 });
|
|
345
|
+
// Map fan-out results back to typed results with device attached for rendering.
|
|
346
|
+
const typedResults = results.map((r, i) => ({
|
|
347
|
+
...r,
|
|
348
|
+
device: targets[i].device,
|
|
349
|
+
}));
|
|
350
|
+
if (allArgs.includes('--json')) {
|
|
351
|
+
const out = {};
|
|
352
|
+
for (const r of typedResults) {
|
|
353
|
+
out[r.name] = r.status === 'ok' ? r.value : { error: r.error ?? r.reason ?? 'unknown' };
|
|
354
|
+
}
|
|
355
|
+
console.log(JSON.stringify(out, null, 2));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
renderFleetRoster(command, forwarded, typedResults, self);
|
|
359
|
+
}
|
|
360
|
+
const anyFailed = typedResults.some((r) => r.status === 'failed');
|
|
361
|
+
process.exitCode = anyFailed ? 1 : 0;
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
158
364
|
/**
|
|
159
365
|
* Route `agents <command> … --host <name>` to a remote if the command is
|
|
160
366
|
* host-routable and a `--host` (or its `--device` alias) was given. Returns
|
|
@@ -167,11 +373,16 @@ async function resolveTargetHost(name, any) {
|
|
|
167
373
|
* @param command the resolved subcommand name (`process.argv`'s first non-flag).
|
|
168
374
|
* @param allArgs `process.argv.slice(2)` — the command name followed by its args.
|
|
169
375
|
*/
|
|
170
|
-
export async function maybeRunOnHost(command, allArgs) {
|
|
376
|
+
export async function maybeRunOnHost(command, allArgs, opts) {
|
|
171
377
|
const hostFlag = flagValue(allArgs, 'host', 'H');
|
|
172
378
|
const deviceFlag = flagValue(allArgs, 'device');
|
|
379
|
+
const hostsFlag = flagValue(allArgs, 'hosts');
|
|
380
|
+
const devicesFlag = flagValue(allArgs, 'devices');
|
|
173
381
|
const hostName = hostFlag ?? deviceFlag;
|
|
174
|
-
|
|
382
|
+
const fleetAll = isFleetAllSentinel(hostFlag, deviceFlag, hostsFlag, devicesFlag);
|
|
383
|
+
// Proceed when any routing flag is present, including the plural fleet flags
|
|
384
|
+
// that may carry the `all` sentinel.
|
|
385
|
+
if (!hostName && !hostsFlag && !devicesFlag)
|
|
175
386
|
return false;
|
|
176
387
|
// Commands with their own richer --host semantics must reach local commander
|
|
177
388
|
// BEFORE any single-target conflict gate. sessions/feed merge --host and
|
|
@@ -193,14 +404,18 @@ export async function maybeRunOnHost(command, allArgs) {
|
|
|
193
404
|
return false;
|
|
194
405
|
}
|
|
195
406
|
}
|
|
196
|
-
// `--hosts`
|
|
197
|
-
//
|
|
198
|
-
// a
|
|
199
|
-
|
|
200
|
-
if (allArgs.includes('--hosts'))
|
|
201
|
-
return false;
|
|
202
|
-
if (allArgs.includes('--devices') && command !== 'routines')
|
|
407
|
+
// `--hosts` / `--devices` are command-level fleet flags unless their value is
|
|
408
|
+
// the `all` sentinel, which this module fans out generically. On `routines`,
|
|
409
|
+
// a non-all `--devices` value is placement (which devices may run the routine).
|
|
410
|
+
if (allArgs.includes('--hosts') && hostsFlag?.toLowerCase() !== 'all')
|
|
203
411
|
return false;
|
|
412
|
+
if (allArgs.includes('--devices')) {
|
|
413
|
+
if (devicesFlag === undefined)
|
|
414
|
+
return false; // malformed, let commander error
|
|
415
|
+
const isAll = devicesFlag.toLowerCase() === 'all';
|
|
416
|
+
if (!isAll && command !== 'routines')
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
204
419
|
const spec = REMOTE_PASSTHROUGH[command];
|
|
205
420
|
if (!spec) {
|
|
206
421
|
// Flag was accepted (no raw commander "unknown option") but this group has
|
|
@@ -210,13 +425,23 @@ export async function maybeRunOnHost(command, allArgs) {
|
|
|
210
425
|
process.exitCode = 1;
|
|
211
426
|
return true;
|
|
212
427
|
}
|
|
213
|
-
//
|
|
214
|
-
//
|
|
428
|
+
// Reject a conflicting --host/--device pair before either the fleet fan-out
|
|
429
|
+
// or single-target path runs. Equal values (e.g. both `all`) are allowed.
|
|
215
430
|
if (hostFlag && deviceFlag && hostFlag !== deviceFlag) {
|
|
216
431
|
console.error(chalk.red('Conflicting --host/--device values — pass just one.'));
|
|
217
432
|
process.exitCode = 1;
|
|
218
433
|
return true;
|
|
219
434
|
}
|
|
435
|
+
// Generic fleet fan-out for the `all` sentinel — before single-host resolution
|
|
436
|
+
// so `all` is never treated as a literal hostname.
|
|
437
|
+
if (fleetAll) {
|
|
438
|
+
return runFleetPassthrough(command, allArgs, spec, opts);
|
|
439
|
+
}
|
|
440
|
+
// After the bailouts and fleet fan-out above, the only remaining path is a
|
|
441
|
+
// single-target --host/--device. Guard for the type checker: plural non-all
|
|
442
|
+
// flags and bare flags were already handled.
|
|
443
|
+
if (!hostName)
|
|
444
|
+
return false;
|
|
220
445
|
// Running against your own machine is just a local run — skip the SSH round-trip.
|
|
221
446
|
// `machineId()` is the same self-identifier the device registry and session
|
|
222
447
|
// sync use (lowercased short hostname); compare case-insensitively.
|
|
@@ -49,6 +49,8 @@ export interface HostPromptRun {
|
|
|
49
49
|
sessionId?: string;
|
|
50
50
|
/** Working directory on the host, already made remote-portable by the caller. */
|
|
51
51
|
remoteCwd?: string;
|
|
52
|
+
/** `remoteCwd` was derived from the local cwd — mirror it, don't fail on it. */
|
|
53
|
+
mirrorCwd?: boolean;
|
|
52
54
|
/** Stream progress and block until completion (default true). */
|
|
53
55
|
follow?: boolean;
|
|
54
56
|
timeoutMs?: number;
|
package/dist/lib/hq/floor.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ActiveSession } from '../session/active.js';
|
|
|
2
2
|
import type { OpenBlock } from '../feed.js';
|
|
3
3
|
import type { AgentStatusDetail, TaskInfo } from '../teams/api.js';
|
|
4
4
|
export type HqRoomKind = 'team' | 'machine' | 'lobby';
|
|
5
|
-
export type HqAgentMood = 'working' | 'waiting' | 'blocked' | 'celebrating' | 'idle';
|
|
5
|
+
export type HqAgentMood = 'working' | 'waiting' | 'blocked' | 'celebrating' | 'done' | 'idle';
|
|
6
6
|
export type HqAmbientKind = 'needs_input' | 'error' | 'pr' | 'team_active' | 'idle_scene';
|
|
7
7
|
export interface HqAction {
|
|
8
8
|
id: string;
|
package/dist/lib/hq/floor.js
CHANGED
|
@@ -28,6 +28,10 @@ function teammateName(session, teammatesById) {
|
|
|
28
28
|
return session.agentId;
|
|
29
29
|
}
|
|
30
30
|
function moodForSession(session, hasOpenBlock) {
|
|
31
|
+
if (session.status === 'abandoned')
|
|
32
|
+
return 'blocked';
|
|
33
|
+
if (session.status === 'closed')
|
|
34
|
+
return 'done';
|
|
31
35
|
if (session.status === 'input_required' || session.activity === 'waiting_input' || hasOpenBlock)
|
|
32
36
|
return 'waiting';
|
|
33
37
|
if (session.rateLimited)
|
|
@@ -175,6 +179,7 @@ export function buildHqFloor(input) {
|
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
181
|
for (const room of rooms.values()) {
|
|
182
|
+
const roomAgents = agents.filter((agent) => agent.roomId === room.id);
|
|
178
183
|
if (room.kind === 'team' && room.counts.running > 0) {
|
|
179
184
|
ambientEvents.push({
|
|
180
185
|
id: `team-active:${room.id}`,
|
|
@@ -184,7 +189,7 @@ export function buildHqFloor(input) {
|
|
|
184
189
|
intensity: 'low',
|
|
185
190
|
});
|
|
186
191
|
}
|
|
187
|
-
if (
|
|
192
|
+
if (roomAgents.length > 0 && roomAgents.every((agent) => agent.mood === 'idle')) {
|
|
188
193
|
ambientEvents.push({
|
|
189
194
|
id: `idle:${room.id}`,
|
|
190
195
|
kind: 'idle_scene',
|
package/dist/lib/mcp.js
CHANGED
|
@@ -18,7 +18,6 @@ import { getBinaryPath, getVersionHomePath } from './versions.js';
|
|
|
18
18
|
import { IS_WINDOWS, execFileShellSpec } from './platform/index.js';
|
|
19
19
|
import { AGENTS, getMcpConfigPathForHome, getProjectMcpConfigPath } from './agents.js';
|
|
20
20
|
import { isCapable } from './capabilities.js';
|
|
21
|
-
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
22
21
|
/**
|
|
23
22
|
* Parse an MCP server config from a YAML file.
|
|
24
23
|
*/
|
|
@@ -401,7 +400,7 @@ export async function registerMcpCommandToTargets(targets, name, commandSpec, sc
|
|
|
401
400
|
function registerMcpCommand(agentId, name, commandSpec, scope, transport, options = {}) {
|
|
402
401
|
try {
|
|
403
402
|
validateMcpServerName(name);
|
|
404
|
-
if (agentId === 'hermes'
|
|
403
|
+
if (agentId === 'hermes') {
|
|
405
404
|
const server = {
|
|
406
405
|
name,
|
|
407
406
|
path: '',
|
|
@@ -413,12 +412,7 @@ function registerMcpCommand(agentId, name, commandSpec, scope, transport, option
|
|
|
413
412
|
: { command: commandSpec.command, args: commandSpec.args }),
|
|
414
413
|
},
|
|
415
414
|
};
|
|
416
|
-
|
|
417
|
-
installMcpToHermesConfig(server, options.home || os.homedir());
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
installMcpToForgeConfig(server, options.home || os.homedir());
|
|
421
|
-
}
|
|
415
|
+
installMcpToHermesConfig(server, options.home || os.homedir());
|
|
422
416
|
return { success: true };
|
|
423
417
|
}
|
|
424
418
|
const bin = options.binary || AGENTS[agentId].cliCommand;
|
|
@@ -436,31 +430,6 @@ function registerMcpCommand(agentId, name, commandSpec, scope, transport, option
|
|
|
436
430
|
return { success: false, error: err.message };
|
|
437
431
|
}
|
|
438
432
|
}
|
|
439
|
-
/**
|
|
440
|
-
* Install MCP server to Gemini config file.
|
|
441
|
-
*/
|
|
442
|
-
function installMcpToGeminiConfig(server, versionHome) {
|
|
443
|
-
const configPath = path.join(versionHome, '.gemini', 'settings.json');
|
|
444
|
-
updateGeminiSettings(configPath, (config) => {
|
|
445
|
-
setGeminiAutoUpdateDisabled(config);
|
|
446
|
-
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
447
|
-
config.mcpServers = {};
|
|
448
|
-
}
|
|
449
|
-
const mcpServers = config.mcpServers;
|
|
450
|
-
if (server.config.transport === 'stdio') {
|
|
451
|
-
mcpServers[server.name] = {
|
|
452
|
-
command: server.config.command,
|
|
453
|
-
args: server.config.args || [],
|
|
454
|
-
env: server.config.env || {},
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
mcpServers[server.name] = {
|
|
459
|
-
url: server.config.url,
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
433
|
/**
|
|
465
434
|
* Install MCP server to Cursor config file.
|
|
466
435
|
*/
|
|
@@ -574,31 +543,6 @@ function installMcpToHermesConfig(server, versionHome) {
|
|
|
574
543
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
575
544
|
fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
|
|
576
545
|
}
|
|
577
|
-
function installMcpToForgeConfig(server, versionHome) {
|
|
578
|
-
const configPath = path.join(versionHome, '.forge', '.mcp.json');
|
|
579
|
-
let config = {};
|
|
580
|
-
if (fs.existsSync(configPath)) {
|
|
581
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
582
|
-
}
|
|
583
|
-
if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
|
|
584
|
-
config.mcpServers = {};
|
|
585
|
-
}
|
|
586
|
-
const mcpServers = config.mcpServers;
|
|
587
|
-
if (server.config.transport === 'stdio') {
|
|
588
|
-
mcpServers[server.name] = {
|
|
589
|
-
command: server.config.command,
|
|
590
|
-
args: server.config.args || [],
|
|
591
|
-
...(server.config.env && { env: server.config.env }),
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
mcpServers[server.name] = {
|
|
596
|
-
url: server.config.url,
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
600
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
601
|
-
}
|
|
602
546
|
function installMcpToOpenCodeConfig(server, versionHome) {
|
|
603
547
|
const configPath = path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
|
|
604
548
|
let config = {};
|
|
@@ -643,10 +587,8 @@ function writeMcpConfigSupportsAgent(agentId) {
|
|
|
643
587
|
switch (agentId) {
|
|
644
588
|
case 'claude':
|
|
645
589
|
case 'cursor':
|
|
646
|
-
case 'gemini':
|
|
647
590
|
case 'kimi':
|
|
648
591
|
case 'droid':
|
|
649
|
-
case 'forge':
|
|
650
592
|
case 'openclaw':
|
|
651
593
|
case 'codex':
|
|
652
594
|
case 'grok':
|
|
@@ -672,10 +614,8 @@ export function writeMcpConfig(agentId, configPath, servers, mode = 'overwrite')
|
|
|
672
614
|
switch (agentId) {
|
|
673
615
|
case 'claude':
|
|
674
616
|
case 'cursor':
|
|
675
|
-
case 'gemini':
|
|
676
617
|
case 'kimi':
|
|
677
|
-
case 'droid':
|
|
678
|
-
case 'forge': {
|
|
618
|
+
case 'droid': {
|
|
679
619
|
let config = {};
|
|
680
620
|
if (fs.existsSync(configPath)) {
|
|
681
621
|
try {
|
|
@@ -888,10 +828,6 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
|
|
|
888
828
|
installMcpViaCodex(binaryPath, server, versionHome);
|
|
889
829
|
handled = true;
|
|
890
830
|
}
|
|
891
|
-
else if (agentId === 'gemini') {
|
|
892
|
-
installMcpToGeminiConfig(server, versionHome);
|
|
893
|
-
handled = true;
|
|
894
|
-
}
|
|
895
831
|
else if (agentId === 'cursor') {
|
|
896
832
|
installMcpToCursorConfig(server, versionHome);
|
|
897
833
|
handled = true;
|
|
@@ -943,10 +879,6 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
|
|
|
943
879
|
installMcpToHermesConfig(server, versionHome);
|
|
944
880
|
handled = true;
|
|
945
881
|
}
|
|
946
|
-
else if (agentId === 'forge') {
|
|
947
|
-
installMcpToForgeConfig(server, versionHome);
|
|
948
|
-
handled = true;
|
|
949
|
-
}
|
|
950
882
|
// Project-layer servers also get merged into the agent's project-level
|
|
951
883
|
// config (e.g., .mcp.json, .codex/config.toml) so the CLI discovers them
|
|
952
884
|
// when run inside the repo.
|
|
Binary file
|
|
@@ -124,6 +124,28 @@ export declare function disableMenubarService(): void;
|
|
|
124
124
|
* Best-effort — never throws into startup.
|
|
125
125
|
*/
|
|
126
126
|
export declare function installMenubarLaunchAgentOnUpgrade(): void;
|
|
127
|
+
/** A live MenubarHelper process: its pid and the executable it is running. */
|
|
128
|
+
export interface MenubarProcess {
|
|
129
|
+
pid: number;
|
|
130
|
+
executable: string;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Split the live MenubarHelper processes into the installed bundle's own
|
|
134
|
+
* (`running`) and every other copy (`foreign`).
|
|
135
|
+
*
|
|
136
|
+
* `pgrep -f MenubarHelper` conflated the two, so a stray dev build could hold
|
|
137
|
+
* the global Cmd-Shift-V chord (RegisterEventHotKey is first-come) while status
|
|
138
|
+
* still reported a healthy `running: yes` — the paste was dead and nothing said
|
|
139
|
+
* so. A foreign copy is the thing to look for, so name it.
|
|
140
|
+
*
|
|
141
|
+
* Identity comes from `comm` (the resolved executable), never from a substring
|
|
142
|
+
* of the command line: matching the latter flags any shell that merely mentions
|
|
143
|
+
* MenubarHelper. `command` is consulted only to drop `--notify` one-shots.
|
|
144
|
+
*/
|
|
145
|
+
export declare function classifyMenubarProcesses(commOutput: string, commandOutput: string, installedExec: string): {
|
|
146
|
+
running: boolean;
|
|
147
|
+
foreign: MenubarProcess[];
|
|
148
|
+
};
|
|
127
149
|
export interface MenubarStatus {
|
|
128
150
|
platform: string;
|
|
129
151
|
source: string | null;
|
|
@@ -133,6 +155,8 @@ export interface MenubarStatus {
|
|
|
133
155
|
stale: boolean;
|
|
134
156
|
serviceInstalled: boolean;
|
|
135
157
|
running: boolean;
|
|
158
|
+
/** Live MenubarHelper processes that are NOT the installed bundle. */
|
|
159
|
+
foreignInstances: MenubarProcess[];
|
|
136
160
|
disabledByUser: boolean;
|
|
137
161
|
}
|
|
138
162
|
export declare function getMenubarStatus(): MenubarStatus;
|
|
@@ -441,12 +441,61 @@ export function installMenubarLaunchAgentOnUpgrade() {
|
|
|
441
441
|
/* never block startup on the menu bar */
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
|
+
/** Parse `ps -axo pid=,<field>=` into pid -> field. The field is the rest of the
|
|
445
|
+
* line, so a path containing spaces (App Support does) survives intact. */
|
|
446
|
+
function parsePsLines(psOutput) {
|
|
447
|
+
const out = new Map();
|
|
448
|
+
for (const line of psOutput.split('\n')) {
|
|
449
|
+
const m = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
450
|
+
if (m)
|
|
451
|
+
out.set(Number(m[1]), m[2]);
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Split the live MenubarHelper processes into the installed bundle's own
|
|
457
|
+
* (`running`) and every other copy (`foreign`).
|
|
458
|
+
*
|
|
459
|
+
* `pgrep -f MenubarHelper` conflated the two, so a stray dev build could hold
|
|
460
|
+
* the global Cmd-Shift-V chord (RegisterEventHotKey is first-come) while status
|
|
461
|
+
* still reported a healthy `running: yes` — the paste was dead and nothing said
|
|
462
|
+
* so. A foreign copy is the thing to look for, so name it.
|
|
463
|
+
*
|
|
464
|
+
* Identity comes from `comm` (the resolved executable), never from a substring
|
|
465
|
+
* of the command line: matching the latter flags any shell that merely mentions
|
|
466
|
+
* MenubarHelper. `command` is consulted only to drop `--notify` one-shots.
|
|
467
|
+
*/
|
|
468
|
+
export function classifyMenubarProcesses(commOutput, commandOutput, installedExec) {
|
|
469
|
+
const commands = parsePsLines(commandOutput);
|
|
470
|
+
const foreign = [];
|
|
471
|
+
let running = false;
|
|
472
|
+
for (const [pid, executable] of parsePsLines(commOutput)) {
|
|
473
|
+
if (path.basename(executable) !== 'MenubarHelper')
|
|
474
|
+
continue;
|
|
475
|
+
// `--notify` is a one-shot that posts a notification and exits; it runs the
|
|
476
|
+
// installed binary but never claims the status item or the chords.
|
|
477
|
+
if ((commands.get(pid) || '').includes('--notify'))
|
|
478
|
+
continue;
|
|
479
|
+
if (executable === installedExec)
|
|
480
|
+
running = true;
|
|
481
|
+
else
|
|
482
|
+
foreign.push({ pid, executable });
|
|
483
|
+
}
|
|
484
|
+
return { running, foreign };
|
|
485
|
+
}
|
|
444
486
|
export function getMenubarStatus() {
|
|
445
487
|
const dest = installedAppPath();
|
|
446
488
|
let running = false;
|
|
489
|
+
let foreignInstances = [];
|
|
447
490
|
if (onDarwin()) {
|
|
448
|
-
const
|
|
449
|
-
|
|
491
|
+
const ps = (format) => spawnSync('ps', ['-axo', format], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8' });
|
|
492
|
+
const comm = ps('pid=,comm=');
|
|
493
|
+
const command = ps('pid=,command=');
|
|
494
|
+
if (comm.status === 0 && command.status === 0) {
|
|
495
|
+
const c = classifyMenubarProcesses(comm.stdout || '', command.stdout || '', installedExecutablePath());
|
|
496
|
+
running = c.running;
|
|
497
|
+
foreignInstances = c.foreign;
|
|
498
|
+
}
|
|
450
499
|
}
|
|
451
500
|
const serviceInstalled = menubarServiceInstalled();
|
|
452
501
|
return {
|
|
@@ -458,6 +507,7 @@ export function getMenubarStatus() {
|
|
|
458
507
|
stale: onDarwin() && serviceInstalled && menubarSetupStale(),
|
|
459
508
|
serviceInstalled,
|
|
460
509
|
running,
|
|
510
|
+
foreignInstances,
|
|
461
511
|
disabledByUser: menubarDisabledByUser(),
|
|
462
512
|
};
|
|
463
513
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* 'error' listener — without it Node re-throws ENOENT as an uncaught exception and
|
|
17
17
|
* takes the whole daemon down (the exact crash overdue.test.ts guards).
|
|
18
18
|
*/
|
|
19
|
+
import { type ChildProcess } from 'child_process';
|
|
19
20
|
export interface DesktopNotification {
|
|
20
21
|
/** Bold first line. */
|
|
21
22
|
title: string;
|
|
@@ -36,6 +37,20 @@ export interface DesktopNotification {
|
|
|
36
37
|
export declare function buildMenubarNotifyArgs(n: DesktopNotification): string[];
|
|
37
38
|
/** AppleScript for the osascript degradation path. Exported for tests. */
|
|
38
39
|
export declare function buildOsascriptNotifyArgs(n: DesktopNotification): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Spawn one detached, best-effort notifier process with a bounded lifetime.
|
|
42
|
+
*
|
|
43
|
+
* The child is detached + unref'd so it never blocks the daemon, but — unlike a
|
|
44
|
+
* pure fire-and-forget — it is supervised: a watchdog SIGKILLs it after
|
|
45
|
+
* `timeoutMs` so a stalled notifier can never linger (the pile-up this fixes).
|
|
46
|
+
* The common path (a sub-second post + self-exit) clears the watchdog on the
|
|
47
|
+
* child's own 'exit', so the kill only ever fires for a genuinely hung child.
|
|
48
|
+
*
|
|
49
|
+
* `timeoutMs` is injectable so the bounded-lifetime behaviour is testable against
|
|
50
|
+
* a real long-running child without a multi-second wait. Returns the child so a
|
|
51
|
+
* caller/test can observe it; callers in this module ignore it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function spawnDetachedQuiet(command: string, args: string[], timeoutMs?: number): ChildProcess;
|
|
39
54
|
/**
|
|
40
55
|
* Fire a native desktop notification, branded via the MenubarHelper companion on
|
|
41
56
|
* macOS. Best-effort — any failure is swallowed so a notification hiccup never
|