@phnx-labs/agents-cli 1.22.8 → 1.22.9
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 +53 -0
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +41 -9
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -1
- package/dist/commands/harness.js +316 -76
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/sessions-picker.js +3 -3
- package/dist/commands/sessions-render.d.ts +12 -0
- package/dist/commands/sessions-render.js +124 -0
- package/dist/commands/sessions.js +28 -1
- package/dist/commands/ssh.js +26 -3
- package/dist/commands/teams.js +12 -1
- package/dist/index.js +9 -2
- package/dist/lib/crabbox/cli.d.ts +35 -0
- package/dist/lib/crabbox/cli.js +46 -0
- package/dist/lib/crabbox/config.d.ts +21 -0
- package/dist/lib/crabbox/config.js +43 -0
- package/dist/lib/crabbox/lease.d.ts +15 -5
- package/dist/lib/crabbox/lease.js +57 -17
- package/dist/lib/daemon.js +12 -11
- package/dist/lib/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.js +18 -1
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/observe-aliases.d.ts +30 -0
- package/dist/lib/observe-aliases.js +56 -0
- package/dist/lib/redact.js +4 -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/session/parse.d.ts +5 -1
- package/dist/lib/session/parse.js +23 -13
- package/dist/lib/session/prompt.js +5 -0
- package/dist/lib/session/render.d.ts +3 -0
- package/dist/lib/session/render.js +33 -10
- package/dist/lib/startup/command-registry.js +15 -1
- package/dist/lib/usage-refresh.d.ts +19 -11
- package/dist/lib/usage-refresh.js +75 -43
- package/dist/lib/usage.d.ts +12 -0
- package/dist/lib/usage.js +37 -6
- package/package.json +1 -1
package/dist/lib/daemon.js
CHANGED
|
@@ -888,14 +888,14 @@ export async function runDaemon() {
|
|
|
888
888
|
};
|
|
889
889
|
const fleetCacheInterval = setInterval(() => { void runFleetCacheWarm(); }, 3 * 60_000);
|
|
890
890
|
const fleetCacheKickoff = setTimeout(() => { void runFleetCacheWarm(); }, 60_000);
|
|
891
|
-
//
|
|
891
|
+
// Usage refresh: keep the usage cache the `agents run` router reads
|
|
892
892
|
// (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
893
|
-
// This host is the sole writer for its own local accounts. The tick wakes
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
893
|
+
// This host is the sole writer for its own local accounts. The tick wakes
|
|
894
|
+
// every 60s (USAGE_REFRESH_TICK_MS) to consider due accounts; each account
|
|
895
|
+
// is scheduled at a fixed 5-minute cadence (REFRESH_INTERVAL_MS), capped at
|
|
896
|
+
// ~12 provider calls/account/hour, skipped under 429 backoff, and fetched
|
|
897
|
+
// with fileOnly credentials so a background tick never pops macOS Touch ID.
|
|
898
|
+
// Overlap-guarded: a slow pass cannot stack concurrent refresh loops.
|
|
899
899
|
let refreshingUsage = false;
|
|
900
900
|
const runUsageRefreshTick = async () => {
|
|
901
901
|
if (refreshingUsage)
|
|
@@ -910,9 +910,9 @@ export async function runDaemon() {
|
|
|
910
910
|
writeUsageCache: writeClaudeUsageCache,
|
|
911
911
|
backoffUntil: usageRateLimitedUntil,
|
|
912
912
|
});
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
913
|
+
// Always log a compact summary so "is refresh working?" is greppable even
|
|
914
|
+
// when every account was not-due (proves the tick ran).
|
|
915
|
+
log('INFO', `usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped`);
|
|
916
916
|
}
|
|
917
917
|
catch (err) {
|
|
918
918
|
log('ERROR', `usage refresh failed: ${err.message}`);
|
|
@@ -921,7 +921,8 @@ export async function runDaemon() {
|
|
|
921
921
|
refreshingUsage = false;
|
|
922
922
|
}
|
|
923
923
|
};
|
|
924
|
-
|
|
924
|
+
// 60s wake matches USAGE_REFRESH_TICK_MS in usage-refresh.ts (keep in sync).
|
|
925
|
+
const usageRefreshInterval = setInterval(() => { void runUsageRefreshTick(); }, 60_000);
|
|
925
926
|
const usageRefreshKickoff = setTimeout(() => { void runUsageRefreshTick(); }, 30_000);
|
|
926
927
|
// RUSH-1817: the startup host decision above is one-shot. If a standalone
|
|
927
928
|
// broker answered agentPing() at daemon start, the daemon declined to host —
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { splitUserHost } from '../hosts/registry.js';
|
|
1
|
+
import { splitUserHost, type MatchHostOptions } from '../hosts/registry.js';
|
|
2
2
|
import { type DeviceProfile } from './registry.js';
|
|
3
3
|
export { splitUserHost };
|
|
4
4
|
/** A dialable peer: the ssh target, the machine id used to tag its rows, a
|
|
@@ -21,9 +21,10 @@ export interface ResolvedExplicitTargetSet {
|
|
|
21
21
|
* `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
|
|
22
22
|
* unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
|
|
23
23
|
* ssh` has never dialed — returns undefined so the caller reports "Unknown
|
|
24
|
-
* device" rather than dialing a literal.
|
|
24
|
+
* device" rather than dialing a literal. `token` may also be the `auto`
|
|
25
|
+
* affinity sentinel (RUSH-2185); `opts.resolveAuto` overrides the pick (tests).
|
|
25
26
|
*/
|
|
26
|
-
export declare function resolveDeviceTarget(token: string): Promise<DeviceProfile | undefined>;
|
|
27
|
+
export declare function resolveDeviceTarget(token: string, opts?: Pick<MatchHostOptions, 'resolveAuto'>): Promise<DeviceProfile | undefined>;
|
|
27
28
|
/**
|
|
28
29
|
* Resolve an explicit `--host`/`--device` list to dialable targets. A token that
|
|
29
30
|
* fails the injection guard or names nothing reachable is skipped with a stderr
|
|
@@ -71,10 +71,11 @@ async function toResolvedTarget(token) {
|
|
|
71
71
|
* `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
|
|
72
72
|
* unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
|
|
73
73
|
* ssh` has never dialed — returns undefined so the caller reports "Unknown
|
|
74
|
-
* device" rather than dialing a literal.
|
|
74
|
+
* device" rather than dialing a literal. `token` may also be the `auto`
|
|
75
|
+
* affinity sentinel (RUSH-2185); `opts.resolveAuto` overrides the pick (tests).
|
|
75
76
|
*/
|
|
76
|
-
export async function resolveDeviceTarget(token) {
|
|
77
|
-
const host = await matchHost(token, { allowBareLiteral: true });
|
|
77
|
+
export async function resolveDeviceTarget(token, opts = {}) {
|
|
78
|
+
const host = await matchHost(token, { allowBareLiteral: true, ...opts });
|
|
78
79
|
if (!host)
|
|
79
80
|
return undefined;
|
|
80
81
|
if (host.device) {
|
|
@@ -24,6 +24,7 @@ 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 { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
27
28
|
import { loadDevices } from '../devices/registry.js';
|
|
28
29
|
import { isSelfHost } from '../devices/self-host.js';
|
|
29
30
|
import { fanOutDevices, planFleetTargets, runLocalCommand, runOnDevice, } from '../devices/fleet.js';
|
|
@@ -396,7 +397,7 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
396
397
|
const deviceFlag = flagValue(allArgs, 'device');
|
|
397
398
|
const hostsFlag = flagValue(allArgs, 'hosts');
|
|
398
399
|
const devicesFlag = flagValue(allArgs, 'devices');
|
|
399
|
-
|
|
400
|
+
let hostName = hostFlag ?? deviceFlag;
|
|
400
401
|
const fleetAll = isFleetAllSentinel(hostFlag, deviceFlag, hostsFlag, devicesFlag);
|
|
401
402
|
// Proceed when any routing flag is present, including the plural fleet flags
|
|
402
403
|
// that may carry the `all` sentinel.
|
|
@@ -460,6 +461,22 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
460
461
|
// flags and bare flags were already handled.
|
|
461
462
|
if (!hostName)
|
|
462
463
|
return false;
|
|
464
|
+
// `auto` is the same affinity sentinel `agents run --device auto` resolves
|
|
465
|
+
// (RUSH-2185) — pick the concrete target up front via resolveDeviceAffinity so
|
|
466
|
+
// the isSelfHost check right below (which compares a literal name, not "auto")
|
|
467
|
+
// still catches a local pick and runs the command locally rather than
|
|
468
|
+
// resolving to a real Host and self-SSHing (or, if this box isn't itself a
|
|
469
|
+
// registered device, dialing a literal, nonexistent host named "auto").
|
|
470
|
+
if (isDeviceAuto(hostName)) {
|
|
471
|
+
const plan = resolveDeviceAffinity({});
|
|
472
|
+
if (!plan.host) {
|
|
473
|
+
const stripped = stripRoutingFlags(allArgs, STRIP_SPECS);
|
|
474
|
+
process.argv = [process.argv[0], process.argv[1], ...stripped];
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
process.stderr.write(chalk.gray(`[agents] device=auto → ${plan.host}\n`));
|
|
478
|
+
hostName = plan.host;
|
|
479
|
+
}
|
|
463
480
|
// Running against your own machine is just a local run — skip the SSH round-trip.
|
|
464
481
|
// Match EVERY identity the box answers to (short id, loopback, tailscale
|
|
465
482
|
// dnsName), not just machineId() — a `--host <self-dnsName>` used to slip past a
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import type { Host, HostProvider, HostProviderId } from './types.js';
|
|
20
20
|
import { DeviceOffloadUnsupportedError } from './types.js';
|
|
21
21
|
import { type DeviceProfile } from '../devices/registry.js';
|
|
22
|
+
import { type DeviceAffinityPlan } from '../smart-launch.js';
|
|
22
23
|
export { DeviceOffloadUnsupportedError };
|
|
23
24
|
export declare function getProvider(id: HostProviderId): HostProvider;
|
|
24
25
|
export declare function getAllProviders(): HostProvider[];
|
|
@@ -52,6 +53,9 @@ export interface MatchHostOptions {
|
|
|
52
53
|
* "Unknown device" verdict reachable).
|
|
53
54
|
*/
|
|
54
55
|
allowBareLiteral?: boolean;
|
|
56
|
+
/** Override the affinity pick for the `auto` sentinel (tests). Defaults to
|
|
57
|
+
* `resolveDeviceAffinity({})` — the same engine `agents run --device auto` uses. */
|
|
58
|
+
resolveAuto?: () => DeviceAffinityPlan;
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
57
61
|
* The one place a `--host` / `--device` token becomes a resolved host. Reads the
|
|
@@ -25,6 +25,8 @@ import { readMeta } from '../state.js';
|
|
|
25
25
|
import { isSshConfigHost } from './ssh-config.js';
|
|
26
26
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
27
27
|
import { loadDevices, isControlDevice } from '../devices/registry.js';
|
|
28
|
+
import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
29
|
+
import { localMachineId } from '../session/origin-machine.js';
|
|
28
30
|
// Re-export so existing importers (tests, commands) keep their path; the class
|
|
29
31
|
// itself lives in types.ts so providers can throw it without a circular import.
|
|
30
32
|
export { DeviceOffloadUnsupportedError };
|
|
@@ -146,6 +148,20 @@ function literalHost(token, host, user) {
|
|
|
146
148
|
* into a dispatch verdict.
|
|
147
149
|
*/
|
|
148
150
|
export async function matchHost(name, opts = {}) {
|
|
151
|
+
// `auto` is the same affinity sentinel `agents run --device auto` resolves
|
|
152
|
+
// (isDeviceAuto / resolveDeviceAffinity in ../smart-launch.js) — shared here so
|
|
153
|
+
// every caller through this one core (ssh, teams, dispatch/passthrough) picks a
|
|
154
|
+
// device the SAME way `run` does instead of rejecting it as "Unknown device
|
|
155
|
+
// 'auto'" (RUSH-2185). A `null` plan.host means the affinity engine picked this
|
|
156
|
+
// very machine; resolve that as the local device/host entry (if any) rather than
|
|
157
|
+
// returning nothing — callers that already special-case "target is this
|
|
158
|
+
// machine" (teams add/create, the passthrough self-host check) then treat it as
|
|
159
|
+
// local exactly as they would if the user had typed the local name.
|
|
160
|
+
if (isDeviceAuto(name)) {
|
|
161
|
+
const plan = (opts.resolveAuto ?? (() => resolveDeviceAffinity({})))();
|
|
162
|
+
const picked = plan.host ?? normalizeHost(localMachineId());
|
|
163
|
+
return matchHost(picked, opts);
|
|
164
|
+
}
|
|
149
165
|
try {
|
|
150
166
|
assertValidSshTarget(name);
|
|
151
167
|
}
|
|
@@ -113,6 +113,7 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
113
113
|
lease: 'local-only',
|
|
114
114
|
box: 'local-only',
|
|
115
115
|
keepBox: 'local-only',
|
|
116
|
+
fresh: 'local-only', // skips the warm-pool reuse for --lease; the lease path is always local
|
|
116
117
|
reuse: 'local-only', // reuse-picker choice for --lease; the lease path is always local
|
|
117
118
|
bare: 'local-only', // skips the local setup-copy push; lease-only concern
|
|
118
119
|
tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observe-umbrella aliases (Phase 3 surface consolidation).
|
|
3
|
+
*
|
|
4
|
+
* Thin name → real command expansion. No store merge — feed / sessions / events
|
|
5
|
+
* remain the stores; these are doors that point at the right reader.
|
|
6
|
+
*
|
|
7
|
+
* inbox → feed (needs-you default)
|
|
8
|
+
* timeline → feed --filter updates (agent progress stream)
|
|
9
|
+
* roster → sessions --active (live agent roster)
|
|
10
|
+
*
|
|
11
|
+
* `audit` is NOT an alias here — `agents audit` is already the tamper-evident
|
|
12
|
+
* run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
|
|
13
|
+
*/
|
|
14
|
+
export type ObserveAlias = 'inbox' | 'timeline' | 'roster';
|
|
15
|
+
export declare const OBSERVE_ALIASES: readonly ObserveAlias[];
|
|
16
|
+
export interface ObserveExpandResult {
|
|
17
|
+
/** argv for the real command (no program name): e.g. ['feed', '--filter', 'updates'] */
|
|
18
|
+
argv: string[];
|
|
19
|
+
/** One-line note for stderr (optional); empty when silent. */
|
|
20
|
+
note: string;
|
|
21
|
+
}
|
|
22
|
+
/** True when `rest` already carries a `--filter` / `--filter=…` flag. */
|
|
23
|
+
export declare function hasFilterFlag(rest: readonly string[]): boolean;
|
|
24
|
+
/** True when `rest` already carries `--active`. */
|
|
25
|
+
export declare function hasActiveFlag(rest: readonly string[]): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Expand an observe alias + remaining user args into the real command argv.
|
|
28
|
+
* Returns null when `alias` is not an observe alias.
|
|
29
|
+
*/
|
|
30
|
+
export declare function expandObserveAlias(alias: string, rest?: readonly string[]): ObserveExpandResult | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observe-umbrella aliases (Phase 3 surface consolidation).
|
|
3
|
+
*
|
|
4
|
+
* Thin name → real command expansion. No store merge — feed / sessions / events
|
|
5
|
+
* remain the stores; these are doors that point at the right reader.
|
|
6
|
+
*
|
|
7
|
+
* inbox → feed (needs-you default)
|
|
8
|
+
* timeline → feed --filter updates (agent progress stream)
|
|
9
|
+
* roster → sessions --active (live agent roster)
|
|
10
|
+
*
|
|
11
|
+
* `audit` is NOT an alias here — `agents audit` is already the tamper-evident
|
|
12
|
+
* run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
|
|
13
|
+
*/
|
|
14
|
+
export const OBSERVE_ALIASES = ['inbox', 'timeline', 'roster'];
|
|
15
|
+
/** True when `rest` already carries a `--filter` / `--filter=…` flag. */
|
|
16
|
+
export function hasFilterFlag(rest) {
|
|
17
|
+
return rest.some((a) => a === '--filter' || a.startsWith('--filter='));
|
|
18
|
+
}
|
|
19
|
+
/** True when `rest` already carries `--active`. */
|
|
20
|
+
export function hasActiveFlag(rest) {
|
|
21
|
+
return rest.some((a) => a === '--active');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Expand an observe alias + remaining user args into the real command argv.
|
|
25
|
+
* Returns null when `alias` is not an observe alias.
|
|
26
|
+
*/
|
|
27
|
+
export function expandObserveAlias(alias, rest = []) {
|
|
28
|
+
const tail = [...rest];
|
|
29
|
+
switch (alias) {
|
|
30
|
+
case 'inbox':
|
|
31
|
+
return {
|
|
32
|
+
argv: ['feed', ...tail],
|
|
33
|
+
note: 'agents inbox → agents feed (needs-you inbox)',
|
|
34
|
+
};
|
|
35
|
+
case 'timeline': {
|
|
36
|
+
const argv = hasFilterFlag(tail)
|
|
37
|
+
? ['feed', ...tail]
|
|
38
|
+
: ['feed', '--filter', 'updates', ...tail];
|
|
39
|
+
return {
|
|
40
|
+
argv,
|
|
41
|
+
note: 'agents timeline → agents feed --filter updates',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
case 'roster': {
|
|
45
|
+
const argv = hasActiveFlag(tail)
|
|
46
|
+
? ['sessions', ...tail]
|
|
47
|
+
: ['sessions', '--active', ...tail];
|
|
48
|
+
return {
|
|
49
|
+
argv,
|
|
50
|
+
note: 'agents roster → agents sessions --active',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/lib/redact.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Shared redaction helpers for text that may be exported or logged.
|
|
3
3
|
*/
|
|
4
4
|
const SECRET_PATTERNS = [
|
|
5
|
+
// Local home paths identify operators and disclose internal filesystem layout.
|
|
6
|
+
// Keep the useful path suffix while masking the machine-specific home prefix.
|
|
7
|
+
[/(^|[\s,"'`(=:])\/(?:home|Users)\/[^/\s,"'`]+/g, '$1[HOME]'],
|
|
8
|
+
[/(^|[\s,"'`(=])[A-Z]:\\Users\\[^\\\s,"'`]+/gi, '$1[HOME]'],
|
|
5
9
|
[/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED_AWS_KEY]'],
|
|
6
10
|
// GitHub: classic PATs (ghp_), OAuth (gho_), app/refresh/server tokens
|
|
7
11
|
// (ghs_/ghr_), and fine-grained PATs (github_pat_). All share the 36-char
|
|
Binary file
|
|
Binary file
|
|
@@ -31,7 +31,11 @@ export declare function safeReadSessionFile(filePath: string, maxBytes?: number)
|
|
|
31
31
|
/**
|
|
32
32
|
* Auto-detect agent type from file path and parse the session.
|
|
33
33
|
*/
|
|
34
|
-
export
|
|
34
|
+
export interface ParseSessionOptions {
|
|
35
|
+
/** Keep normalized tool results compact by default; renderers can request full output. */
|
|
36
|
+
maxToolOutputChars?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function parseSession(filePath: string, agent?: SessionAgentId, opts?: ParseSessionOptions): SessionEvent[];
|
|
35
39
|
/** Infer the agent type from a session file path using known directory conventions. */
|
|
36
40
|
export declare function detectAgent(filePath: string): SessionAgentId | null;
|
|
37
41
|
/**
|
|
@@ -112,10 +112,12 @@ export function safeReadSessionFile(filePath, maxBytes = SESSION_FILE_MAX_BYTES)
|
|
|
112
112
|
}
|
|
113
113
|
return fs.readFileSync(filePath, 'utf-8');
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
function truncateNormalizedToolOutput(output, maxChars) {
|
|
116
|
+
if (!Number.isFinite(maxChars) || output.length <= maxChars)
|
|
117
|
+
return output;
|
|
118
|
+
return `${output.slice(0, maxChars)}\n\n[Output truncated: ${output.length - maxChars} characters omitted.]`;
|
|
119
|
+
}
|
|
120
|
+
export function parseSession(filePath, agent, opts = {}) {
|
|
119
121
|
const detected = agent || detectAgent(filePath);
|
|
120
122
|
if (!detected) {
|
|
121
123
|
throw new Error(`Cannot detect agent type from path: ${filePath}`);
|
|
@@ -166,7 +168,11 @@ export function parseSession(filePath, agent) {
|
|
|
166
168
|
// from `!`-prefix runs, `<system-reminder>`, etc.) as `_synthetic` so turn
|
|
167
169
|
// slicing and `--include user` count only genuine user intent — one place,
|
|
168
170
|
// every harness, instead of per-consumer regex.
|
|
171
|
+
const maxToolOutputChars = opts.maxToolOutputChars ?? 500;
|
|
169
172
|
for (const e of events) {
|
|
173
|
+
if (e.type === 'tool_result' && e.output) {
|
|
174
|
+
e.output = truncateNormalizedToolOutput(e.output, maxToolOutputChars);
|
|
175
|
+
}
|
|
170
176
|
sanitizeEvent(e);
|
|
171
177
|
if (e.type === 'message' && e.role === 'user' && isSyntheticUserMessage(e.content)) {
|
|
172
178
|
e._synthetic = true;
|
|
@@ -496,7 +502,11 @@ export function parseClaudeContent(content) {
|
|
|
496
502
|
exitCode: structured.exitCode,
|
|
497
503
|
statusCode: structured.statusCode,
|
|
498
504
|
errorCode: structured.errorCode,
|
|
499
|
-
|
|
505
|
+
// Not truncated here: `parseSession` caps every event's `output`
|
|
506
|
+
// centrally via `maxToolOutputChars` (default 500, the same
|
|
507
|
+
// bound this site used to hardcode), so the render path can ask
|
|
508
|
+
// for the full text with `Infinity`.
|
|
509
|
+
output,
|
|
500
510
|
});
|
|
501
511
|
}
|
|
502
512
|
if (toolId)
|
|
@@ -686,7 +696,7 @@ export function parseCodexContent(content) {
|
|
|
686
696
|
exitCode: result.exitCode,
|
|
687
697
|
statusCode: result.statusCode,
|
|
688
698
|
errorCode: result.errorCode,
|
|
689
|
-
output: result.text
|
|
699
|
+
output: result.text,
|
|
690
700
|
});
|
|
691
701
|
if (callId)
|
|
692
702
|
callMap.delete(callId);
|
|
@@ -741,7 +751,7 @@ export function parseCodexContent(content) {
|
|
|
741
751
|
exitCode: result.exitCode,
|
|
742
752
|
statusCode: result.statusCode,
|
|
743
753
|
errorCode: result.errorCode,
|
|
744
|
-
output: result.text
|
|
754
|
+
output: result.text,
|
|
745
755
|
});
|
|
746
756
|
if (callId)
|
|
747
757
|
callMap.delete(callId);
|
|
@@ -865,7 +875,7 @@ export function parseGemini(filePath) {
|
|
|
865
875
|
tool: toolName,
|
|
866
876
|
callId: typeof tc.id === 'string' ? tc.id : undefined,
|
|
867
877
|
success: tc.status === 'success',
|
|
868
|
-
output
|
|
878
|
+
output,
|
|
869
879
|
});
|
|
870
880
|
}
|
|
871
881
|
}
|
|
@@ -1242,7 +1252,7 @@ export function parseGrok(filePath) {
|
|
|
1242
1252
|
callId,
|
|
1243
1253
|
success: structuredError ? false : structuredSuccess ? true : undefined,
|
|
1244
1254
|
outcome: structuredError ? 'error' : structuredSuccess ? 'ok' : 'unknown',
|
|
1245
|
-
output: output
|
|
1255
|
+
output: output,
|
|
1246
1256
|
});
|
|
1247
1257
|
if (callId)
|
|
1248
1258
|
toolCallMap.delete(callId);
|
|
@@ -1360,7 +1370,7 @@ export function parseOpenCode(filePath) {
|
|
|
1360
1370
|
tool: toolName,
|
|
1361
1371
|
callId,
|
|
1362
1372
|
success: state.status === 'completed',
|
|
1363
|
-
output: outputStr
|
|
1373
|
+
output: outputStr,
|
|
1364
1374
|
});
|
|
1365
1375
|
}
|
|
1366
1376
|
break;
|
|
@@ -1470,7 +1480,7 @@ export function parseRush(filePath) {
|
|
|
1470
1480
|
tool: info?.tool ?? (typeof raw.name === 'string' ? raw.name : 'unknown'),
|
|
1471
1481
|
callId,
|
|
1472
1482
|
success,
|
|
1473
|
-
output: outputStr
|
|
1483
|
+
output: outputStr,
|
|
1474
1484
|
});
|
|
1475
1485
|
if (callId)
|
|
1476
1486
|
toolCallMap.delete(callId);
|
|
@@ -1680,7 +1690,7 @@ export function parseKimi(filePath) {
|
|
|
1680
1690
|
callId,
|
|
1681
1691
|
success: structuredError ? false : structuredSuccess ? true : undefined,
|
|
1682
1692
|
outcome: structuredError ? 'error' : structuredSuccess ? 'ok' : 'unknown',
|
|
1683
|
-
output: output
|
|
1693
|
+
output: output,
|
|
1684
1694
|
});
|
|
1685
1695
|
if (callId) {
|
|
1686
1696
|
toolCallMap.delete(callId);
|
|
@@ -1825,7 +1835,7 @@ function parseAnthropicMessageJsonl(filePath, agent) {
|
|
|
1825
1835
|
else {
|
|
1826
1836
|
events.push({
|
|
1827
1837
|
type: 'tool_result', agent, timestamp, tool: toolInfo?.tool, callId: toolId, success: true,
|
|
1828
|
-
output: output
|
|
1838
|
+
output: output,
|
|
1829
1839
|
});
|
|
1830
1840
|
}
|
|
1831
1841
|
if (toolId)
|
|
@@ -46,6 +46,11 @@ const SYNTHETIC_USER_MESSAGE_PATTERNS = [
|
|
|
46
46
|
/^\s*<\/?user-prompt-submit-hook>/i,
|
|
47
47
|
/^\s*<\/?persisted-output>/i,
|
|
48
48
|
/^\s*<permissions instructions>/i,
|
|
49
|
+
/^\s*<(?:apps|plugins|skills)_instructions>/i,
|
|
50
|
+
/^\s*<(?:multi_agent_mode|environment_context)>/i,
|
|
51
|
+
/^\s*## (?:In-flight in this repo|Host & Fleet)/i,
|
|
52
|
+
/^\s*Your current session id is\b/i,
|
|
53
|
+
/^\s*Linear context skipped:/i,
|
|
49
54
|
/^\s*<collaboration_mode>/i,
|
|
50
55
|
/^\s*Caveat: The messages below were generated by the user while running/i,
|
|
51
56
|
/^\s*\[Request interrupted/i,
|
|
@@ -108,6 +108,9 @@ export declare function filterEvents(events: SessionEvent[], opts: FilterOptions
|
|
|
108
108
|
*/
|
|
109
109
|
export interface RenderConversationMarkdownOptions {
|
|
110
110
|
redact?: boolean;
|
|
111
|
+
knownSecrets?: readonly string[];
|
|
112
|
+
reasoning?: 'omit' | 'fold' | 'include';
|
|
113
|
+
maxToolOutputChars?: number;
|
|
111
114
|
}
|
|
112
115
|
export declare function renderConversationMarkdown(events: SessionEvent[], opts?: RenderConversationMarkdownOptions): string;
|
|
113
116
|
/**
|
|
@@ -923,10 +923,23 @@ export function filterEvents(events, opts) {
|
|
|
923
923
|
const sliced = applyTurnSlice(events, opts);
|
|
924
924
|
return applyRoleFilter(sliced, opts);
|
|
925
925
|
}
|
|
926
|
+
function markdownFence(content, language = '') {
|
|
927
|
+
const fence = content.includes('```') ? '````' : '```';
|
|
928
|
+
return `${fence}${language}\n${content}\n${fence}`;
|
|
929
|
+
}
|
|
930
|
+
function truncateToolOutput(content, maxChars) {
|
|
931
|
+
if (content.length <= maxChars)
|
|
932
|
+
return content;
|
|
933
|
+
return `${content.slice(0, maxChars)}\n\n[Output truncated: ${content.length - maxChars} characters omitted.]`;
|
|
934
|
+
}
|
|
926
935
|
export function renderConversationMarkdown(events, opts = {}) {
|
|
927
936
|
const parts = [];
|
|
928
937
|
const shouldRedact = opts.redact !== false;
|
|
929
|
-
const sanitize = (text) => shouldRedact ? redactSecrets(text) : text;
|
|
938
|
+
const sanitize = (text) => shouldRedact ? redactSecrets(text, opts.knownSecrets) : text;
|
|
939
|
+
// Preserve the long-standing `sessions <id> --markdown` full-fidelity default.
|
|
940
|
+
// The shareable `sessions render` surface passes `omit` explicitly.
|
|
941
|
+
const reasoning = opts.reasoning ?? 'include';
|
|
942
|
+
const maxToolOutputChars = opts.maxToolOutputChars ?? 4000;
|
|
930
943
|
for (const event of events) {
|
|
931
944
|
if (event.type === 'message') {
|
|
932
945
|
if (event.role === 'user') {
|
|
@@ -937,27 +950,37 @@ export function renderConversationMarkdown(events, opts = {}) {
|
|
|
937
950
|
}
|
|
938
951
|
}
|
|
939
952
|
else if (event.type === 'thinking') {
|
|
940
|
-
if (event.content)
|
|
941
|
-
parts.push(`###
|
|
953
|
+
if (event.content && reasoning === 'include') {
|
|
954
|
+
parts.push(`### Reasoning\n\n${sanitize(event.content)}`);
|
|
955
|
+
}
|
|
956
|
+
else if (event.content && reasoning === 'fold') {
|
|
957
|
+
parts.push(`<details>\n<summary>Reasoning</summary>\n\n${sanitize(event.content)}\n\n</details>`);
|
|
958
|
+
}
|
|
942
959
|
}
|
|
943
960
|
else if (event.type === 'tool_use') {
|
|
944
961
|
const tool = event.tool || 'unknown';
|
|
945
962
|
if (event.command) {
|
|
946
|
-
|
|
963
|
+
const args = event.args && Object.keys(event.args).length > 0
|
|
964
|
+
? `\n\nArguments:\n\n${markdownFence(sanitize(JSON.stringify(event.args, null, 2)), 'json')}`
|
|
965
|
+
: '';
|
|
966
|
+
parts.push(`### Tool: ${tool}\n\n${markdownFence(sanitize(event.command), 'bash')}${args}`);
|
|
967
|
+
}
|
|
968
|
+
else if (event.args && Object.keys(event.args).length > 0) {
|
|
969
|
+
parts.push(`### Tool: ${tool}\n\nArguments:\n\n${markdownFence(sanitize(JSON.stringify(event.args, null, 2)), 'json')}`);
|
|
947
970
|
}
|
|
948
971
|
else if (event.path) {
|
|
949
|
-
parts.push(`### Tool: ${tool}\n\n\`${shortenPathTrace(event.path)}\``);
|
|
972
|
+
parts.push(`### Tool: ${tool}\n\n\`${sanitize(shortenPathTrace(event.path))}\``);
|
|
950
973
|
}
|
|
951
974
|
else {
|
|
952
975
|
const summary = summarizeToolUse(tool, event.args);
|
|
953
|
-
parts.push(`### Tool: ${tool}\n\n${summary}`);
|
|
976
|
+
parts.push(`### Tool: ${tool}\n\n${sanitize(summary)}`);
|
|
954
977
|
}
|
|
955
978
|
}
|
|
956
979
|
else if (event.type === 'tool_result') {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const body = sanitize(
|
|
960
|
-
parts.push(`### Tool Result\n\n
|
|
980
|
+
const output = event.content || event.output;
|
|
981
|
+
if (output) {
|
|
982
|
+
const body = sanitize(truncateToolOutput(output, maxToolOutputChars));
|
|
983
|
+
parts.push(`### Tool Result${event.tool ? `: ${event.tool}` : ''}\n\n${markdownFence(body)}`);
|
|
961
984
|
}
|
|
962
985
|
}
|
|
963
986
|
else if (event.type === 'error') {
|
|
@@ -89,7 +89,16 @@ export const loadFunnel = async () => (await import('../../commands/funnel.js'))
|
|
|
89
89
|
* inherit the root's custom help formatter rather than getting the per-command
|
|
90
90
|
* recursive pass. Keeping that ordering preserves their `--help` output exactly.
|
|
91
91
|
*/
|
|
92
|
-
|
|
92
|
+
// `roster` is an observe-umbrella alias of `sessions --active` — same module,
|
|
93
|
+
// same SQLite stack, same post-help registration order as sessions.
|
|
94
|
+
export const LAZY_COMMAND_NAMES = new Set([
|
|
95
|
+
'sessions',
|
|
96
|
+
'roster',
|
|
97
|
+
'teams',
|
|
98
|
+
'cloud',
|
|
99
|
+
'message',
|
|
100
|
+
'serve',
|
|
101
|
+
]);
|
|
93
102
|
/**
|
|
94
103
|
* User-typed top-level command name -> ordered list of module loaders to run.
|
|
95
104
|
*
|
|
@@ -194,12 +203,17 @@ export const COMMAND_LOADERS = {
|
|
|
194
203
|
setup: [loadSetup],
|
|
195
204
|
uninstall: [loadUninstall],
|
|
196
205
|
sessions: [loadSessions],
|
|
206
|
+
// Observe-umbrella alias of sessions --active (same lazy module).
|
|
207
|
+
roster: [loadSessions],
|
|
197
208
|
teams: [loadTeams],
|
|
198
209
|
cloud: [loadCloud],
|
|
199
210
|
message: [loadMessage],
|
|
200
211
|
send: [loadSend],
|
|
201
212
|
notify: [loadSend],
|
|
202
213
|
feed: [loadFeed],
|
|
214
|
+
// Observe-umbrella aliases of feed / feed --filter updates.
|
|
215
|
+
inbox: [loadFeed],
|
|
216
|
+
timeline: [loadFeed],
|
|
203
217
|
mailboxes: [loadMailboxes],
|
|
204
218
|
mailbox: [loadMailboxes],
|
|
205
219
|
serve: [loadServe],
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { type UsageHeadroom, type UsageSnapshot, type UsageInfo } from './usage.js';
|
|
2
2
|
import type { AgentId } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
3
|
+
/**
|
|
4
|
+
* Default schedule between successful (or attempted) live usage fetches for one
|
|
5
|
+
* account. Floor and ceiling of the delay helper are pinned to this so the
|
|
6
|
+
* daemon does not poll faster than 5 minutes even under high burn, and does not
|
|
7
|
+
* let an idle account rot longer than 5 minutes between attempts.
|
|
8
|
+
*/
|
|
9
|
+
export declare const REFRESH_INTERVAL_MS: number;
|
|
10
|
+
/** @deprecated alias — use {@link REFRESH_INTERVAL_MS}. Kept for test imports. */
|
|
5
11
|
export declare const REFRESH_MIN_MS: number;
|
|
6
|
-
/**
|
|
7
|
-
* least this often so a cache never silently rots. */
|
|
12
|
+
/** @deprecated alias — use {@link REFRESH_INTERVAL_MS}. Kept for test imports. */
|
|
8
13
|
export declare const REFRESH_MAX_MS: number;
|
|
9
|
-
/**
|
|
10
|
-
* not
|
|
14
|
+
/** Burn-rate divisor retained for the pure delay helper / tests; with min=max
|
|
15
|
+
* the divisor does not change the scheduled interval. */
|
|
11
16
|
export declare const REFRESH_BURN_DIVISOR = 4;
|
|
12
|
-
/** At most this many live fetches per account per rolling hour. */
|
|
13
|
-
export declare const HOURLY_CALL_CAP =
|
|
17
|
+
/** At most this many live fetches per account per rolling hour (5m cadence ⇒ 12). */
|
|
18
|
+
export declare const HOURLY_CALL_CAP = 12;
|
|
19
|
+
/** How often the daemon wakes to *consider* a refresh pass (due accounts only). */
|
|
20
|
+
export declare const USAGE_REFRESH_TICK_MS: number;
|
|
14
21
|
/**
|
|
15
22
|
* One account's refresh state + published headroom. `sessionUsedPercent` /
|
|
16
23
|
* `capturedAt` are the prior sample the NEXT tick projects the burn rate from;
|
|
@@ -38,9 +45,10 @@ export declare function readHeadroomEntry(usageKey: string): HeadroomEntry | nul
|
|
|
38
45
|
/** Merge entries into the cache (best-effort; preserves other accounts' rows). */
|
|
39
46
|
export declare function writeHeadroomEntries(entries: Record<string, HeadroomEntry>): void;
|
|
40
47
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
48
|
+
* Interval until the next refresh attempt, clamped to [minMs, maxMs].
|
|
49
|
+
* Defaults pin both ends to {@link REFRESH_INTERVAL_MS} (5 minutes) so the
|
|
50
|
+
* live daemon path is a fixed schedule. Tests may pass a wider range to
|
|
51
|
+
* exercise burn-aware scheduling without changing production cadence.
|
|
44
52
|
*/
|
|
45
53
|
export declare function computeNextRefreshDelayMs(minutesToLimit: number | null, opts?: {
|
|
46
54
|
minMs?: number;
|