@phnx-labs/agents-cli 1.21.0 → 1.21.2
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 +198 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.js +5 -2
- package/dist/commands/feed.js +63 -40
- package/dist/commands/hooks.js +9 -45
- package/dist/commands/menubar.js +24 -24
- package/dist/commands/message.js +23 -3
- package/dist/commands/monitors.js +8 -6
- package/dist/commands/perf.d.ts +13 -0
- package/dist/commands/perf.js +80 -23
- package/dist/commands/projects.d.ts +11 -0
- package/dist/commands/projects.js +153 -21
- package/dist/commands/routines.js +46 -1
- package/dist/commands/ssh.js +69 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +158 -0
- package/dist/commands/usage.d.ts +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +90 -45
- package/dist/index.js +14 -1
- package/dist/lib/activity.d.ts +6 -0
- package/dist/lib/activity.js +3 -0
- package/dist/lib/agents.js +2 -2
- package/dist/lib/analytics/dashboard.d.ts +11 -0
- package/dist/lib/analytics/dashboard.js +31 -0
- package/dist/lib/analytics/recipes.d.ts +32 -0
- package/dist/lib/analytics/recipes.js +316 -0
- package/dist/lib/analytics/usage-db.d.ts +84 -0
- package/dist/lib/analytics/usage-db.js +301 -0
- package/dist/lib/browser/service.js +18 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
- package/dist/lib/channels/resolve.d.ts +18 -1
- package/dist/lib/channels/resolve.js +15 -5
- package/dist/lib/cli-resources.d.ts +20 -0
- package/dist/lib/cli-resources.js +48 -1
- package/dist/lib/daemon.js +51 -14
- package/dist/lib/devices/health-report.d.ts +5 -0
- package/dist/lib/devices/health-report.js +3 -0
- package/dist/lib/feed-broadcast.d.ts +93 -12
- package/dist/lib/feed-broadcast.js +235 -30
- package/dist/lib/feed-post.d.ts +16 -2
- package/dist/lib/feed-post.js +35 -7
- package/dist/lib/feed.js +1 -1
- package/dist/lib/fleet-cache.d.ts +37 -0
- package/dist/lib/fleet-cache.js +40 -0
- package/dist/lib/fleet-status.d.ts +53 -0
- package/dist/lib/fleet-status.js +120 -0
- package/dist/lib/friction-heuristics.d.ts +32 -0
- package/dist/lib/friction-heuristics.js +47 -0
- package/dist/lib/hooks/cache.js +28 -6
- package/dist/lib/hooks/profile.d.ts +8 -0
- package/dist/lib/hooks/profile.js +14 -4
- package/dist/lib/hooks.d.ts +16 -1
- package/dist/lib/hooks.js +133 -18
- package/dist/lib/linear-cache.d.ts +63 -0
- package/dist/lib/linear-cache.js +146 -0
- package/dist/lib/linear-project-counts.d.ts +35 -5
- package/dist/lib/linear-project-counts.js +61 -16
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +7 -0
- package/dist/lib/menubar/install-menubar.js +36 -6
- package/dist/lib/monitors/config.d.ts +1 -1
- package/dist/lib/monitors/dispatch.d.ts +6 -2
- package/dist/lib/monitors/dispatch.js +10 -15
- package/dist/lib/notify.d.ts +42 -6
- package/dist/lib/notify.js +41 -32
- package/dist/lib/perf/db.d.ts +6 -1
- package/dist/lib/perf/db.js +35 -5
- package/dist/lib/perf/types.d.ts +10 -0
- package/dist/lib/project-doctor.d.ts +36 -0
- package/dist/lib/project-doctor.js +45 -0
- package/dist/lib/project-import.d.ts +11 -1
- package/dist/lib/project-import.js +17 -3
- package/dist/lib/project-status.d.ts +25 -5
- package/dist/lib/project-status.js +48 -6
- package/dist/lib/rotate.d.ts +27 -0
- package/dist/lib/rotate.js +44 -17
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +39 -0
- package/dist/lib/runner.js +34 -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/usage-db.d.ts +3 -63
- package/dist/lib/secrets/usage-db.js +46 -186
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +14 -3
- package/dist/lib/session/discover.d.ts +3 -0
- package/dist/lib/session/discover.js +8 -0
- package/dist/lib/session/types.d.ts +1 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +31 -3
- package/dist/lib/state.js +53 -10
- package/dist/lib/types.d.ts +14 -4
- package/dist/lib/usage-refresh.d.ts +106 -0
- package/dist/lib/usage-refresh.js +238 -0
- package/dist/lib/usage.d.ts +152 -17
- package/dist/lib/usage.js +393 -79
- package/package.json +1 -1
|
@@ -346,6 +346,15 @@ export class BrowserService {
|
|
|
346
346
|
conn.tasks.set(taskName, task);
|
|
347
347
|
await this.saveTaskState(effectiveProfileName, conn.tasks);
|
|
348
348
|
emit('browser.launch', { profile: effectiveProfileName, task: taskName, pid: conn.pid });
|
|
349
|
+
void import('../analytics/usage-db.js').then(({ recordUsage }) => {
|
|
350
|
+
recordUsage({
|
|
351
|
+
kind: 'browser',
|
|
352
|
+
name: effectiveProfileName,
|
|
353
|
+
event: 'launch',
|
|
354
|
+
source: 'browser',
|
|
355
|
+
meta: { task: taskName },
|
|
356
|
+
});
|
|
357
|
+
}).catch(() => { });
|
|
349
358
|
// If URL provided, create tab directly (no about:blank)
|
|
350
359
|
let tabId;
|
|
351
360
|
if (opts.url && !conn.electron) {
|
|
@@ -412,6 +421,15 @@ export class BrowserService {
|
|
|
412
421
|
conn.tasks.delete(taskName);
|
|
413
422
|
await this.saveTaskState(profileName, conn.tasks);
|
|
414
423
|
emit('browser.close', { profile: profileName, task: taskName });
|
|
424
|
+
void import('../analytics/usage-db.js').then(({ recordUsage }) => {
|
|
425
|
+
recordUsage({
|
|
426
|
+
kind: 'browser',
|
|
427
|
+
name: profileName,
|
|
428
|
+
event: 'close',
|
|
429
|
+
source: 'browser',
|
|
430
|
+
meta: { task: taskName },
|
|
431
|
+
});
|
|
432
|
+
}).catch(() => { });
|
|
415
433
|
if (conn.forkedFrom && conn.tasks.size === 0) {
|
|
416
434
|
conn.cdp.close();
|
|
417
435
|
killChrome(conn.pid);
|
|
@@ -24,7 +24,7 @@ export const openclawTelegramProvider = {
|
|
|
24
24
|
return { ok: false, channel: name, id: opts.target, error: 'openclaw CLI not found on PATH' };
|
|
25
25
|
}
|
|
26
26
|
try {
|
|
27
|
-
await execFileAsync('openclaw', buildOpenClawNotifyArgs(text, {
|
|
27
|
+
await execFileAsync('openclaw', buildOpenClawNotifyArgs(text, { target: opts.target }));
|
|
28
28
|
return { ok: true, channel: name, id: opts.target };
|
|
29
29
|
}
|
|
30
30
|
catch (err) {
|
|
@@ -4,8 +4,25 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Default is name-identity (`--channel slack` -> `slack` provider) — NOT a
|
|
6
6
|
* fallback to a different transport. Only telegram is dual-homed (rush vs
|
|
7
|
-
* openclaw-telegram); config picks.
|
|
7
|
+
* openclaw-telegram); config picks.
|
|
8
|
+
*
|
|
9
|
+
* Two entry points, deliberately: `lookupTransport` *returns* the failure, for
|
|
10
|
+
* long-lived callers (the monitor daemon, the feed-dispatch loop) that must
|
|
11
|
+
* survive a bad channel name; `resolveTransport` `die()`s on it, for the
|
|
12
|
+
* interactive `agents send` / `agents notify` command path where exiting with a
|
|
13
|
+
* loud message is the right answer. Never give a daemon the dying one.
|
|
8
14
|
*/
|
|
9
15
|
import type { Meta } from '../types.js';
|
|
10
16
|
import { type ChannelProvider } from './registry.js';
|
|
17
|
+
export interface TransportLookup {
|
|
18
|
+
/** Provider name after applying the `notify.transports` mapping. */
|
|
19
|
+
providerName: string;
|
|
20
|
+
/** Registered provider, or undefined when `providerName` resolves to nothing. */
|
|
21
|
+
provider?: ChannelProvider;
|
|
22
|
+
/** Why resolution failed — set exactly when `provider` is undefined. */
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Resolve a channel to its provider, returning the failure instead of exiting. */
|
|
26
|
+
export declare function lookupTransport(channel: string, meta: Meta): TransportLookup;
|
|
27
|
+
/** Interactive-command resolution: an unregistered provider dies loud. */
|
|
11
28
|
export declare function resolveTransport(channel: string, meta: Meta): ChannelProvider;
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { die } from '../format.js';
|
|
2
2
|
import { resolveChannelProvider, listChannelProviders } from './registry.js';
|
|
3
|
-
|
|
3
|
+
/** Resolve a channel to its provider, returning the failure instead of exiting. */
|
|
4
|
+
export function lookupTransport(channel, meta) {
|
|
4
5
|
const providerName = meta.notify?.transports?.[channel] ?? channel;
|
|
5
6
|
const provider = resolveChannelProvider(providerName);
|
|
6
|
-
if (
|
|
7
|
-
|
|
7
|
+
if (provider)
|
|
8
|
+
return { providerName, provider };
|
|
9
|
+
return {
|
|
10
|
+
providerName,
|
|
11
|
+
error: `No channel provider '${providerName}'` +
|
|
8
12
|
(providerName === channel ? '' : ` (mapped from channel '${channel}' via notify.transports)`) +
|
|
9
|
-
`. Registered: ${listChannelProviders().join(', ')}
|
|
10
|
-
}
|
|
13
|
+
`. Registered: ${listChannelProviders().join(', ')}.`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Interactive-command resolution: an unregistered provider dies loud. */
|
|
17
|
+
export function resolveTransport(channel, meta) {
|
|
18
|
+
const { provider, error } = lookupTransport(channel, meta);
|
|
19
|
+
if (!provider)
|
|
20
|
+
die(error);
|
|
11
21
|
return provider;
|
|
12
22
|
}
|
|
@@ -90,6 +90,15 @@ export declare function hasCommand(cmd: string): boolean;
|
|
|
90
90
|
* shell, never interpolates strings into a command line.
|
|
91
91
|
*/
|
|
92
92
|
export declare function isCliInstalled(manifest: CliManifest): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Async, non-blocking sibling of {@link isCliInstalled}. Same dispatch and
|
|
95
|
+
* Windows-shim retry, but over `execFile` so many manifests can be checked
|
|
96
|
+
* concurrently — the fix for RUSH-2136, where `agents doctor --json` ran a dozen+
|
|
97
|
+
* blocking 10s-timeout `spawnSync` checks SERIALLY (measured ~136s on an idle
|
|
98
|
+
* box). `execFile`'s `timeout` still SIGKILLs a wedged check, so a single
|
|
99
|
+
* hanging probe can't stall the whole set past 10s.
|
|
100
|
+
*/
|
|
101
|
+
export declare function isCliInstalledAsync(manifest: CliManifest): Promise<boolean>;
|
|
93
102
|
/**
|
|
94
103
|
* Pick the first install method whose required host tool is available.
|
|
95
104
|
* Returns null when none of the declared methods can run on this host.
|
|
@@ -151,3 +160,14 @@ export declare function listCliStatus(cwd?: string): {
|
|
|
151
160
|
statuses: CliStatus[];
|
|
152
161
|
errors: CliManifestError[];
|
|
153
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* Async sibling of {@link listCliStatus} that probes every manifest CONCURRENTLY
|
|
165
|
+
* (RUSH-2136). The sync version runs each blocking `spawnSync` check one after
|
|
166
|
+
* another, so a dozen host CLIs whose checks are slow serialize into a
|
|
167
|
+
* multi-minute stall on `agents doctor --json`. This awaits them in parallel, so
|
|
168
|
+
* total wall time is the slowest single check (bounded at 10s), not their sum.
|
|
169
|
+
*/
|
|
170
|
+
export declare function listCliStatusAsync(cwd?: string): Promise<{
|
|
171
|
+
statuses: CliStatus[];
|
|
172
|
+
errors: CliManifestError[];
|
|
173
|
+
}>;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import * as fs from 'fs';
|
|
19
19
|
import * as os from 'os';
|
|
20
20
|
import * as path from 'path';
|
|
21
|
-
import { spawnSync } from 'child_process';
|
|
21
|
+
import { spawnSync, execFile } from 'child_process';
|
|
22
22
|
import * as yaml from 'yaml';
|
|
23
23
|
import { listResources, resolveResource } from './resources.js';
|
|
24
24
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
@@ -304,6 +304,38 @@ export function isCliInstalled(manifest) {
|
|
|
304
304
|
}
|
|
305
305
|
return false;
|
|
306
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Async, non-blocking sibling of {@link isCliInstalled}. Same dispatch and
|
|
309
|
+
* Windows-shim retry, but over `execFile` so many manifests can be checked
|
|
310
|
+
* concurrently — the fix for RUSH-2136, where `agents doctor --json` ran a dozen+
|
|
311
|
+
* blocking 10s-timeout `spawnSync` checks SERIALLY (measured ~136s on an idle
|
|
312
|
+
* box). `execFile`'s `timeout` still SIGKILLs a wedged check, so a single
|
|
313
|
+
* hanging probe can't stall the whole set past 10s.
|
|
314
|
+
*/
|
|
315
|
+
export function isCliInstalledAsync(manifest) {
|
|
316
|
+
const c = manifest.check;
|
|
317
|
+
if (c.kind === 'which') {
|
|
318
|
+
cmdExistsCache.delete(c.cmd);
|
|
319
|
+
return Promise.resolve(hasCommand(c.cmd));
|
|
320
|
+
}
|
|
321
|
+
return new Promise((resolve) => {
|
|
322
|
+
execFile(c.cmd, c.args, { timeout: 10_000 }, (err) => {
|
|
323
|
+
if (!err)
|
|
324
|
+
return resolve(true);
|
|
325
|
+
// A spawn failure (as opposed to a non-zero exit) surfaces as a string
|
|
326
|
+
// errno code (ENOENT/EINVAL); a non-zero exit surfaces as a numeric code.
|
|
327
|
+
// On Windows a `.cmd`/`.bat` shim spawn-fails without a shell — retry once
|
|
328
|
+
// through the shell, exactly as the sync path does.
|
|
329
|
+
const spawnFailed = typeof err.code === 'string';
|
|
330
|
+
if (process.platform === 'win32' && spawnFailed) {
|
|
331
|
+
const line = composeWin32CommandLine(c.cmd, c.args);
|
|
332
|
+
execFile(line, { timeout: 10_000, shell: true }, (retryErr) => resolve(!retryErr));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
resolve(false);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
307
339
|
// ─── Method selection ────────────────────────────────────────────────────────
|
|
308
340
|
/**
|
|
309
341
|
* Pick the first install method whose required host tool is available.
|
|
@@ -550,3 +582,18 @@ export function listCliStatus(cwd) {
|
|
|
550
582
|
}));
|
|
551
583
|
return { statuses, errors };
|
|
552
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Async sibling of {@link listCliStatus} that probes every manifest CONCURRENTLY
|
|
587
|
+
* (RUSH-2136). The sync version runs each blocking `spawnSync` check one after
|
|
588
|
+
* another, so a dozen host CLIs whose checks are slow serialize into a
|
|
589
|
+
* multi-minute stall on `agents doctor --json`. This awaits them in parallel, so
|
|
590
|
+
* total wall time is the slowest single check (bounded at 10s), not their sum.
|
|
591
|
+
*/
|
|
592
|
+
export async function listCliStatusAsync(cwd) {
|
|
593
|
+
const { manifests, errors } = listCliManifests(cwd);
|
|
594
|
+
const statuses = await Promise.all(manifests.map(async (manifest) => ({
|
|
595
|
+
manifest,
|
|
596
|
+
installed: await isCliInstalledAsync(manifest),
|
|
597
|
+
})));
|
|
598
|
+
return { statuses, errors };
|
|
599
|
+
}
|
package/dist/lib/daemon.js
CHANGED
|
@@ -803,13 +803,17 @@ export async function runDaemon() {
|
|
|
803
803
|
};
|
|
804
804
|
const launchHealthInterval = setInterval(() => { void runLaunchHealthCheck(); }, 6 * 60 * 60_000);
|
|
805
805
|
const launchHealthKickoff = setTimeout(() => { void runLaunchHealthCheck(); }, 90_000);
|
|
806
|
-
// Fleet cache warm:
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
806
|
+
// Fleet cache warm: publish THIS host's row for the caches `agents fleet
|
|
807
|
+
// status` / `agents devices list` read. PUBLISH-OWN / READ-UNION (RUSH-2061):
|
|
808
|
+
// each daemon probes only ITSELF and never SSHes another box, so the fleet no
|
|
809
|
+
// longer pays N² SSH resource probes every 3 minutes (N daemons × N devices) —
|
|
810
|
+
// the source of the fan-out storm AND the orphaned-probe pile-up. Two cheap
|
|
811
|
+
// self-only refreshes: (1) this host's auth-health verdicts (also feeds the
|
|
812
|
+
// `doctor --json` Auth rollup other hosts read), and (2) its fleet-status row
|
|
813
|
+
// (local resource probe + live-agent workload). Cross-host rows are unioned on
|
|
814
|
+
// demand by the reader (`agents fleet status`), not pushed by every daemon.
|
|
815
|
+
// Best-effort + overlap-guarded like the probes above; ~every 3 min, once ~60s
|
|
816
|
+
// after startup.
|
|
813
817
|
let warmingFleetCache = false;
|
|
814
818
|
const runFleetCacheWarm = async () => {
|
|
815
819
|
if (warmingFleetCache)
|
|
@@ -822,13 +826,9 @@ export async function runDaemon() {
|
|
|
822
826
|
const { getCliVersion } = await import('./version.js');
|
|
823
827
|
const authRows = await probeLocalFleetAuth({ cliVersion: getCliVersion() });
|
|
824
828
|
writeFleetAuthRows(self, authRows);
|
|
825
|
-
const {
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
const reg = await loadDevices();
|
|
829
|
-
const probeable = planFleetTargets(reg).filter((t) => !t.skip).map((t) => t.device);
|
|
830
|
-
const res = await loadFleetStats(probeable, { forceRefresh: true, selfName: self });
|
|
831
|
-
log('INFO', `fleet cache warm: ${authRows.length} auth row(s), ${res.stats.size} device stat(s)`);
|
|
829
|
+
const { publishLocalFleetStatus } = await import('./fleet-status.js');
|
|
830
|
+
const row = await publishLocalFleetStatus(self);
|
|
831
|
+
log('INFO', `fleet cache warm: ${authRows.length} auth row(s), ${row.agents.running} running agent(s) on ${self}`);
|
|
832
832
|
}
|
|
833
833
|
catch (err) {
|
|
834
834
|
log('ERROR', `fleet cache warm failed: ${err.message}`);
|
|
@@ -839,6 +839,41 @@ export async function runDaemon() {
|
|
|
839
839
|
};
|
|
840
840
|
const fleetCacheInterval = setInterval(() => { void runFleetCacheWarm(); }, 3 * 60_000);
|
|
841
841
|
const fleetCacheKickoff = setTimeout(() => { void runFleetCacheWarm(); }, 60_000);
|
|
842
|
+
// Adaptive usage refresh: keep the usage cache the `agents run` router reads
|
|
843
|
+
// (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
844
|
+
// This host is the sole writer for its own local accounts. The tick wakes at
|
|
845
|
+
// the 90s floor, but per-account cadence gates the actual live fetches: an
|
|
846
|
+
// account racing toward its 5h cap is polled sooner (down to 90s), an idle one
|
|
847
|
+
// rarely (up to 15min), capped at ~6 provider calls/account/hour and skipped
|
|
848
|
+
// entirely while its provider is under a 429 backoff. Overlap-guarded like the
|
|
849
|
+
// probes above; a box signed into no networked-usage account is a clean no-op.
|
|
850
|
+
let refreshingUsage = false;
|
|
851
|
+
const runUsageRefreshTick = async () => {
|
|
852
|
+
if (refreshingUsage)
|
|
853
|
+
return;
|
|
854
|
+
refreshingUsage = true;
|
|
855
|
+
try {
|
|
856
|
+
const { runUsageRefresh, buildLocalUsageAccounts } = await import('./usage-refresh.js');
|
|
857
|
+
const { writeClaudeUsageCache } = await import('./usage.js');
|
|
858
|
+
const { usageRateLimitedUntil } = await import('./usage-backoff.js');
|
|
859
|
+
const r = await runUsageRefresh({
|
|
860
|
+
listAccounts: buildLocalUsageAccounts,
|
|
861
|
+
writeUsageCache: writeClaudeUsageCache,
|
|
862
|
+
backoffUntil: usageRateLimitedUntil,
|
|
863
|
+
});
|
|
864
|
+
if (r.refreshed > 0 || r.failed > 0) {
|
|
865
|
+
log('INFO', `usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
log('ERROR', `usage refresh failed: ${err.message}`);
|
|
870
|
+
}
|
|
871
|
+
finally {
|
|
872
|
+
refreshingUsage = false;
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
const usageRefreshInterval = setInterval(() => { void runUsageRefreshTick(); }, 90_000);
|
|
876
|
+
const usageRefreshKickoff = setTimeout(() => { void runUsageRefreshTick(); }, 30_000);
|
|
842
877
|
// RUSH-1817: the startup host decision above is one-shot. If a standalone
|
|
843
878
|
// broker answered agentPing() at daemon start, the daemon declined to host —
|
|
844
879
|
// but should that standalone later die or crash-loop, nothing takes over and
|
|
@@ -928,6 +963,8 @@ export async function runDaemon() {
|
|
|
928
963
|
clearTimeout(launchHealthKickoff);
|
|
929
964
|
clearInterval(fleetCacheInterval);
|
|
930
965
|
clearTimeout(fleetCacheKickoff);
|
|
966
|
+
clearInterval(usageRefreshInterval);
|
|
967
|
+
clearTimeout(usageRefreshKickoff);
|
|
931
968
|
clearInterval(brokerSelfHealInterval);
|
|
932
969
|
hostedBroker?.close();
|
|
933
970
|
removeDaemonPid();
|
|
@@ -2,6 +2,7 @@ import { type DeviceStats } from './health.js';
|
|
|
2
2
|
import { type HostAuthSummary } from '../auth-health.js';
|
|
3
3
|
import type { OnlineState } from './reachability.js';
|
|
4
4
|
import { type FleetInventory } from './fleet-divergence.js';
|
|
5
|
+
import type { FleetAgentCounts } from '../fleet-status.js';
|
|
5
6
|
export interface FleetCliStatus {
|
|
6
7
|
installed: boolean;
|
|
7
8
|
path: string | null;
|
|
@@ -46,6 +47,10 @@ export interface FleetHealthRow {
|
|
|
46
47
|
* divergence detection (RUSH-2027). Undefined for an unreachable box or an
|
|
47
48
|
* older CLI that doesn't emit it. */
|
|
48
49
|
inventory?: FleetInventory;
|
|
50
|
+
/** This host's live agent workload (running-agent count + per-context / per-
|
|
51
|
+
* agent breakdown), from the fleet-status mirror / read-union (RUSH-2061).
|
|
52
|
+
* Undefined when no row has been published for the host yet. */
|
|
53
|
+
agents?: FleetAgentCounts;
|
|
49
54
|
}
|
|
50
55
|
export interface FleetWarning {
|
|
51
56
|
kind: 'unreachable' | 'drift' | 'cli' | 'version-skew' | 'divergence';
|
|
@@ -445,6 +445,9 @@ export function renderFleetSummary(report, opts = {}) {
|
|
|
445
445
|
else if (isGenuinelyOffline(row) && row.lastSeen) {
|
|
446
446
|
marks.push(chalk.gray(`last seen ${formatCheckedAge(Date.parse(row.lastSeen), now)}`));
|
|
447
447
|
}
|
|
448
|
+
if (row.agents && row.agents.running > 0) {
|
|
449
|
+
marks.push(chalk.cyan(`${row.agents.running} agent${row.agents.running === 1 ? '' : 's'}`));
|
|
450
|
+
}
|
|
448
451
|
if (isSelf)
|
|
449
452
|
marks.push(chalk.cyan('← this machine'));
|
|
450
453
|
const note = marks.length ? ` ${marks.join(' ')}` : '';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Meta } from './types.js';
|
|
1
2
|
/** How loudly a post asks to be heard. Ordered — `important` implies milestone. */
|
|
2
3
|
export type FeedPostLevel = 'milestone' | 'important';
|
|
3
4
|
/** Parse a `--level` value; anything unrecognized is a usage error, not a default. */
|
|
@@ -6,9 +7,20 @@ export interface FeedSinkConfig {
|
|
|
6
7
|
/**
|
|
7
8
|
* argv to run, with `{placeholder}` tokens substituted. First element is the
|
|
8
9
|
* program; it is spawned directly (no shell), so quoting is not a concern and
|
|
9
|
-
* post text can never become shell syntax.
|
|
10
|
+
* post text can never become shell syntax. Mutually exclusive with `channel`
|
|
11
|
+
* — a sink is one shape or the other.
|
|
10
12
|
*/
|
|
11
|
-
command
|
|
13
|
+
command?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* In-process delivery through the same channel-provider registry `agents
|
|
16
|
+
* send`/`agents notify` use — the composed `{message}` body, no argv, no
|
|
17
|
+
* spawn. `'owner'` is the address alias (expands to `notify.owner.{channel,to}`
|
|
18
|
+
* in agents.yaml, same as `agents notify`); any other value is a registered
|
|
19
|
+
* channel name (or a `notify.transports` mapping) and requires `to`.
|
|
20
|
+
*/
|
|
21
|
+
channel?: string;
|
|
22
|
+
/** Recipient for a `channel` sink. Required unless `channel` is the `owner` alias. */
|
|
23
|
+
to?: string;
|
|
12
24
|
/** Lowest post level that reaches this sink. Defaults to `milestone` (all posts). */
|
|
13
25
|
minLevel?: FeedPostLevel;
|
|
14
26
|
}
|
|
@@ -16,7 +28,9 @@ export interface FeedSinkConfig {
|
|
|
16
28
|
export type FeedBroadcastConfig = Record<string, FeedSinkConfig>;
|
|
17
29
|
/** Everything a template may interpolate. Absent values skip templates that need them. */
|
|
18
30
|
export interface FeedBroadcastContext {
|
|
19
|
-
/**
|
|
31
|
+
/** Short subject line (~4–5 words). Phone line 1. */
|
|
32
|
+
title?: string;
|
|
33
|
+
/** The post body, verbatim. Phone line after the blank line. */
|
|
20
34
|
text: string;
|
|
21
35
|
level: FeedPostLevel;
|
|
22
36
|
/** Tracker id for the work, e.g. `RUSH-2081`. */
|
|
@@ -63,6 +77,8 @@ export declare function blockBroadcastContext(block: {
|
|
|
63
77
|
}, extras?: {
|
|
64
78
|
project?: string;
|
|
65
79
|
agent?: string;
|
|
80
|
+
title?: string;
|
|
81
|
+
body?: string;
|
|
66
82
|
}): FeedBroadcastContext;
|
|
67
83
|
/**
|
|
68
84
|
* Why a declared block reached nobody, or undefined when it got through.
|
|
@@ -78,7 +94,14 @@ export declare function blockBroadcastContext(block: {
|
|
|
78
94
|
export declare function blockDeliveryFailure(blocked: boolean, outcomes: SinkOutcome[]): string | undefined;
|
|
79
95
|
export interface PlannedSink {
|
|
80
96
|
name: string;
|
|
81
|
-
|
|
97
|
+
/** Command sink: argv to spawn (mutually exclusive with `channel`). */
|
|
98
|
+
argv?: string[];
|
|
99
|
+
/** Channel sink: provider channel name, or the `owner` alias. */
|
|
100
|
+
channel?: string;
|
|
101
|
+
/** Channel sink recipient. Unset for the `owner` alias — resolved at delivery. */
|
|
102
|
+
to?: string;
|
|
103
|
+
/** Channel sink body — the composed `{message}` for this post. */
|
|
104
|
+
text?: string;
|
|
82
105
|
}
|
|
83
106
|
export interface SinkOutcome {
|
|
84
107
|
name: string;
|
|
@@ -87,10 +110,42 @@ export interface SinkOutcome {
|
|
|
87
110
|
error?: string;
|
|
88
111
|
}
|
|
89
112
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
* Short host label for a phone line — strip user@ and domain so
|
|
114
|
+
* `muqsit@mac-mini.tailnet.ts.net` reads as `mac-mini`.
|
|
115
|
+
*/
|
|
116
|
+
export declare function shortHost(host: string | undefined): string | undefined;
|
|
117
|
+
/** First 8 hex chars of a session id for the footer (readable, not a full uuid). */
|
|
118
|
+
export declare function shortSessionChunk(session: string | undefined): string | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* Scrub em/en dashes from outbound phone copy (house rule + iMessage readability).
|
|
121
|
+
* Collapses whitespace; does not invent meaning.
|
|
122
|
+
*/
|
|
123
|
+
export declare function scrubOutboundDashes(text: string): string;
|
|
124
|
+
/**
|
|
125
|
+
* Footer like "Sent from my iPhone" — who posted, a session crumb, which box.
|
|
126
|
+
*
|
|
127
|
+
* Sent from grok/a02da0e2 on mac-mini
|
|
128
|
+
*
|
|
129
|
+
* Agent name first; session chunk for disambiguation when many groks run;
|
|
130
|
+
* host last. Skip the uninformative default label `agent`.
|
|
131
|
+
*/
|
|
132
|
+
export declare function composeBroadcastFooter(ctx: FeedBroadcastContext): string | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Human-facing body for a messaging sink (`{message}`).
|
|
135
|
+
*
|
|
136
|
+
* ```
|
|
137
|
+
* Title in a few words
|
|
138
|
+
*
|
|
139
|
+
* Body of what happened or the ask.
|
|
140
|
+
*
|
|
141
|
+
* Sent from grok/a02da0e2 on mac-mini
|
|
142
|
+
* agents focus a02da0e2 (blocks only)
|
|
143
|
+
* https://… (optional attach URL)
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* Title first (scannable subject). Blank line. Body. Footer provenance so a
|
|
147
|
+
* fleet of agents is attributable without crowding the ask. Prefer `{message}`
|
|
148
|
+
* over bare `{text}` in messaging sinks.
|
|
94
149
|
*/
|
|
95
150
|
export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): string;
|
|
96
151
|
/**
|
|
@@ -103,11 +158,37 @@ export declare function renderSinkArgv(template: string[], ctx: FeedBroadcastCon
|
|
|
103
158
|
/**
|
|
104
159
|
* Which sinks this post reaches, in config order. Pure — the dry-run listing and
|
|
105
160
|
* the real fan-out plan through here, so what `--dry-run` shows is what runs.
|
|
161
|
+
*
|
|
162
|
+
* A `channel:` sink is gated by the same `minLevel` rule as a `command:` sink —
|
|
163
|
+
* one level check for both shapes, so a dry-run plan is truthful regardless of
|
|
164
|
+
* which shape an operator's sink uses.
|
|
106
165
|
*/
|
|
107
166
|
export declare function planFeedBroadcast(config: FeedBroadcastConfig | undefined, ctx: FeedBroadcastContext): PlannedSink[];
|
|
108
167
|
/**
|
|
109
|
-
*
|
|
110
|
-
* that
|
|
111
|
-
*
|
|
168
|
+
* The effective sink config for a post: the operator's `feed.broadcast`, or —
|
|
169
|
+
* when that is unset or empty — an implicit fallback straight to
|
|
170
|
+
* `notify.owner`, for a post worth interrupting someone over.
|
|
171
|
+
*
|
|
172
|
+
* Before this, `broadcastPostedEvent`/`broadcastBlock` returned early the
|
|
173
|
+
* moment `feed.broadcast` was empty, even when `notify.owner` was fully
|
|
174
|
+
* configured — so the common case (an operator who set up owner notifications
|
|
175
|
+
* but never wrote a `feed.broadcast` block) produced a `--blocked` post that
|
|
176
|
+
* looked recorded and reached nobody. `agents notify` already treats
|
|
177
|
+
* `notify.owner` as the default human destination; this makes an important
|
|
178
|
+
* feed post/block use that same default instead of requiring a second,
|
|
179
|
+
* redundant config block that says the same thing.
|
|
180
|
+
*
|
|
181
|
+
* The fallback only fires for `important` — a routine `milestone` post stays
|
|
182
|
+
* record-only, matching the `minLevel` contract every declared sink already
|
|
183
|
+
* follows. An operator-declared `feed.broadcast` (any non-empty config)
|
|
184
|
+
* always wins outright; the fallback never layers on top of it.
|
|
185
|
+
*/
|
|
186
|
+
export declare function effectiveBroadcastConfig(config: FeedBroadcastConfig | undefined, level: FeedPostLevel, meta: Meta): FeedBroadcastConfig | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* Run the planned sinks. A `command:` sink is a direct spawn with a bounded
|
|
189
|
+
* lifetime; a `channel:` sink delivers in-process. Either way a sink that
|
|
190
|
+
* fails or is not installed/registered is reported, never thrown — the post
|
|
191
|
+
* is already written and must not be undone by a mirror that could not be
|
|
192
|
+
* reached.
|
|
112
193
|
*/
|
|
113
|
-
export declare function runFeedBroadcast(planned: PlannedSink[], timeoutMs?: number): SinkOutcome[]
|
|
194
|
+
export declare function runFeedBroadcast(planned: PlannedSink[], meta: Meta, timeoutMs?: number): Promise<SinkOutcome[]>;
|