@phnx-labs/agents-cli 1.22.21 → 1.22.23
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 +256 -0
- package/README.md +8 -2
- package/dist/commands/doctor.js +15 -7
- package/dist/commands/exec.js +20 -6
- package/dist/commands/focus.d.ts +76 -4
- package/dist/commands/focus.js +219 -40
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +25 -0
- package/dist/commands/go.js +63 -2
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/insights.d.ts +32 -0
- package/dist/commands/insights.js +478 -0
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets.js +25 -30
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions-resume.d.ts +21 -3
- package/dist/commands/sessions-resume.js +50 -11
- package/dist/commands/sessions.d.ts +81 -5
- package/dist/commands/sessions.js +331 -66
- package/dist/commands/watchdog.js +13 -2
- package/dist/index.js +3 -1
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- package/dist/lib/exec.d.ts +4 -0
- package/dist/lib/exec.js +88 -54
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks.js +12 -2
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/picker.d.ts +45 -0
- package/dist/lib/picker.js +75 -6
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/runner.js +28 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +118 -49
- package/dist/lib/secrets/reaper.d.ts +87 -0
- package/dist/lib/secrets/reaper.js +184 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/session/active.d.ts +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +35 -1
- package/dist/lib/session/db.js +132 -1
- package/dist/lib/session/digest.d.ts +3 -0
- package/dist/lib/session/digest.js +3 -3
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/insights.d.ts +126 -0
- package/dist/lib/session/insights.js +330 -0
- package/dist/lib/session/parse.d.ts +29 -2
- package/dist/lib/session/parse.js +111 -5
- package/dist/lib/session/remote-list.d.ts +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +40 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/usage.d.ts +13 -0
- package/dist/lib/usage.js +254 -3
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
- package/dist/bin/agents +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioural facets of a coding session, and the cross-session rollup built from them.
|
|
3
|
+
*
|
|
4
|
+
* This is the engine behind `agents insights`. It answers "how do you work" rather than
|
|
5
|
+
* "what did you spend" (`agents cost`) or "what shipped" (`agents output`), and it is
|
|
6
|
+
* the only surface that splits any of it by the account that produced the work.
|
|
7
|
+
*
|
|
8
|
+
* Everything here is a pure function of a parsed `SessionEvent[]`, so it is testable
|
|
9
|
+
* without a database and cheap to re-run. The expensive part is parsing the transcript,
|
|
10
|
+
* which is why results are cached per session in `session_insights` and recomputed only
|
|
11
|
+
* when the file's (mtime, size) changes — the same staleness contract as `scan_ledger`.
|
|
12
|
+
*
|
|
13
|
+
* Prior art: Claude Code's own `/insights`, which computes a comparable set from
|
|
14
|
+
* `~/.claude/projects` alone. Two deliberate differences:
|
|
15
|
+
*
|
|
16
|
+
* - It sees ONE account's directory. This reads every indexed session, across every
|
|
17
|
+
* Claude account and every other harness, and reports them apart.
|
|
18
|
+
* - It collapses conversation branches before counting. agents-cli is file-per-session
|
|
19
|
+
* throughout (`discover.ts` keys on the transcript's basename), so session counts
|
|
20
|
+
* here will read slightly higher than `/insights` on the same machine. Reported as
|
|
21
|
+
* the raw file count rather than quietly differing.
|
|
22
|
+
*/
|
|
23
|
+
import { computeSummaryStats, shortenModel } from './render.js';
|
|
24
|
+
import { classifyFileChanges, EDIT_TOOLS, WRITE_TOOLS } from './digest.js';
|
|
25
|
+
/** File extension → language label. Mirrors the set `/insights` attributes by. */
|
|
26
|
+
const LANGUAGE_BY_EXT = {
|
|
27
|
+
'.ts': 'TypeScript', '.tsx': 'TypeScript', '.mts': 'TypeScript', '.cts': 'TypeScript',
|
|
28
|
+
'.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
|
|
29
|
+
'.py': 'Python', '.go': 'Go', '.rs': 'Rust', '.rb': 'Ruby', '.java': 'Java',
|
|
30
|
+
'.kt': 'Kotlin', '.swift': 'Swift', '.c': 'C', '.h': 'C', '.cc': 'C++', '.cpp': 'C++',
|
|
31
|
+
'.hpp': 'C++', '.cs': 'C#', '.php': 'PHP', '.sh': 'Shell', '.bash': 'Shell',
|
|
32
|
+
'.zsh': 'Shell', '.fish': 'Shell', '.sql': 'SQL', '.css': 'CSS', '.scss': 'CSS',
|
|
33
|
+
'.html': 'HTML', '.vue': 'Vue', '.svelte': 'Svelte', '.md': 'Markdown',
|
|
34
|
+
'.json': 'JSON', '.yaml': 'YAML', '.yml': 'YAML', '.toml': 'TOML',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Tool-failure categories, matched in order against the result text (lowercased).
|
|
38
|
+
* Substring rules, not judgement calls — the same shape `/insights` uses, so the
|
|
39
|
+
* buckets stay comparable. First match wins; anything unmatched is "Other".
|
|
40
|
+
*/
|
|
41
|
+
const ERROR_CATEGORIES = [
|
|
42
|
+
[['string to replace not found', 'no changes to make'], 'Edit Failed'],
|
|
43
|
+
[['has been modified since', 'modified since read'], 'File Changed'],
|
|
44
|
+
[['exceeds maximum', 'too large', 'too long'], 'File Too Large'],
|
|
45
|
+
[['file not found', 'does not exist', 'no such file'], 'File Not Found'],
|
|
46
|
+
[['rejected', "doesn't want to proceed", 'user doesn’t want'], 'User Rejected'],
|
|
47
|
+
[['exit code', 'command failed', 'error:'], 'Command Failed'],
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Gaps longer than this are someone leaving and coming back, not a reply latency.
|
|
51
|
+
* Counted separately rather than silently dropped.
|
|
52
|
+
*/
|
|
53
|
+
const GAP_CEILING_SECONDS = 3600;
|
|
54
|
+
/** Response-gap buckets, in ascending order. Upper bound is exclusive. */
|
|
55
|
+
const GAP_BUCKETS = [
|
|
56
|
+
['<10s', 10], ['10-30s', 30], ['30s-1m', 60], ['1-2m', 120],
|
|
57
|
+
['2-5m', 300], ['5-15m', 900], ['15-60m', Infinity],
|
|
58
|
+
];
|
|
59
|
+
function emptyFacets() {
|
|
60
|
+
return {
|
|
61
|
+
toolCounts: {}, models: {}, languages: {}, slashCommands: {}, errorCategories: {},
|
|
62
|
+
interruptions: 0, responseGaps: [], gapsOverCeiling: 0,
|
|
63
|
+
linesTouchedBefore: 0, linesTouchedAfter: 0, editingToolCalls: 0,
|
|
64
|
+
filesCreated: 0, filesModified: 0, filesDeleted: 0, gitCommits: 0, gitPushes: 0,
|
|
65
|
+
shellCommandsSeen: 0,
|
|
66
|
+
messageHours: new Array(24).fill(0), userTurns: 0, assistantTurns: 0,
|
|
67
|
+
toolCount: 0, errorCount: 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function bump(map, key, by = 1) {
|
|
71
|
+
map[key] = (map[key] ?? 0) + by;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Lines in a string, 0 for empty. A trailing newline terminates the last line rather
|
|
75
|
+
* than starting a new one, so `"a\nb\n"` is 2 — `split('\n').length` would say 3 and
|
|
76
|
+
* over-count every newline-terminated Write by one.
|
|
77
|
+
*/
|
|
78
|
+
function lineCount(text) {
|
|
79
|
+
if (typeof text !== 'string' || text === '')
|
|
80
|
+
return 0;
|
|
81
|
+
const trimmed = text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
82
|
+
return trimmed.split('\n').length;
|
|
83
|
+
}
|
|
84
|
+
function categorizeError(text) {
|
|
85
|
+
const lower = text.toLowerCase();
|
|
86
|
+
for (const [needles, label] of ERROR_CATEGORIES) {
|
|
87
|
+
if (needles.some((n) => lower.includes(n)))
|
|
88
|
+
return label;
|
|
89
|
+
}
|
|
90
|
+
return 'Other';
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Count invocations of a git subcommand in a shell command line.
|
|
94
|
+
*
|
|
95
|
+
* Matches `git commit`, `git -C /repo commit`, and the same again after a `&&`, `||`
|
|
96
|
+
* or `;`, without trying to parse shell. The subcommand must be its own whitespace-
|
|
97
|
+
* separated token, so `git-commit` (a different binary) does not count.
|
|
98
|
+
*
|
|
99
|
+
* Known limitation, shared with the `/insights` implementation this mirrors: a git
|
|
100
|
+
* command quoted inside another command (`echo "run git commit later"`) still counts.
|
|
101
|
+
* Distinguishing that needs a real shell parse, which is not worth it for a rollup.
|
|
102
|
+
*/
|
|
103
|
+
function countGitOp(command, op) {
|
|
104
|
+
const re = new RegExp(`(?:^|[\\s&|;(])git(?:\\s+[^\\s&|;]+)*?\\s+${op}\\b`, 'g');
|
|
105
|
+
return (command.match(re) ?? []).length;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compute every behavioural facet of one session from its parsed events.
|
|
109
|
+
*
|
|
110
|
+
* Pure: no I/O, no clock, no filesystem. `timezoneOffsetMinutes` is injected rather
|
|
111
|
+
* than read from the environment so the hour histogram is deterministic in tests and
|
|
112
|
+
* can be re-bucketed for a different display timezone without re-parsing.
|
|
113
|
+
*/
|
|
114
|
+
export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().getTimezoneOffset()) {
|
|
115
|
+
const f = emptyFacets();
|
|
116
|
+
const stats = computeSummaryStats(events);
|
|
117
|
+
f.toolCounts = stats.toolCounts;
|
|
118
|
+
f.userTurns = stats.userTurns;
|
|
119
|
+
f.assistantTurns = stats.assistantTurns;
|
|
120
|
+
f.toolCount = stats.toolCount;
|
|
121
|
+
f.errorCount = stats.errorCount;
|
|
122
|
+
const changes = classifyFileChanges(events);
|
|
123
|
+
for (const c of changes) {
|
|
124
|
+
if (c.op === 'created')
|
|
125
|
+
f.filesCreated++;
|
|
126
|
+
else if (c.op === 'modified')
|
|
127
|
+
f.filesModified++;
|
|
128
|
+
else
|
|
129
|
+
f.filesDeleted++;
|
|
130
|
+
const dot = c.path.lastIndexOf('.');
|
|
131
|
+
if (dot > 0) {
|
|
132
|
+
const lang = LANGUAGE_BY_EXT[c.path.slice(dot).toLowerCase()];
|
|
133
|
+
if (lang)
|
|
134
|
+
bump(f.languages, lang);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Response gap: assistant goes quiet, user speaks again.
|
|
138
|
+
//
|
|
139
|
+
// No lower bound. /insights drops gaps under 2s, which sounds harmless and is not:
|
|
140
|
+
// measured over 782 real transcripts it censors 28.4% of the sample and inflates the
|
|
141
|
+
// reported p50 by 63% (143s against a true 88s), because fast replies are common and
|
|
142
|
+
// dropping them all shifts the median right. A 0-second reply is a real reply.
|
|
143
|
+
//
|
|
144
|
+
// The upper bound stays: past an hour the user went away and came back, which is not
|
|
145
|
+
// a reply latency. It censors 5.5% of gaps, and `gapsOverCeiling` reports how many so
|
|
146
|
+
// the number is never quietly truncated.
|
|
147
|
+
let lastAssistantTs = null;
|
|
148
|
+
for (const e of events) {
|
|
149
|
+
const ts = new Date(e.timestamp).getTime();
|
|
150
|
+
const hasTs = !Number.isNaN(ts);
|
|
151
|
+
switch (e.type) {
|
|
152
|
+
case 'interrupt':
|
|
153
|
+
f.interruptions++;
|
|
154
|
+
break;
|
|
155
|
+
case 'usage':
|
|
156
|
+
// shortenModel so the label matches `agents sessions <id>` and `trends`
|
|
157
|
+
// rather than printing the raw id beside their shortened one.
|
|
158
|
+
if (e.model)
|
|
159
|
+
bump(f.models, shortenModel(e.model));
|
|
160
|
+
break;
|
|
161
|
+
case 'error':
|
|
162
|
+
bump(f.errorCategories, categorizeError(e.content ?? e.output ?? ''));
|
|
163
|
+
break;
|
|
164
|
+
case 'message':
|
|
165
|
+
if (e.role === 'assistant') {
|
|
166
|
+
if (hasTs)
|
|
167
|
+
lastAssistantTs = ts;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if (e.role !== 'user')
|
|
171
|
+
break;
|
|
172
|
+
if (hasTs) {
|
|
173
|
+
// Local-time hour. parse.ts falls back to `new Date()` for a record with no
|
|
174
|
+
// timestamp; those are indistinguishable here, but they are rare and would
|
|
175
|
+
// only smear the histogram toward the scan time, never invent a session.
|
|
176
|
+
const local = new Date(ts - timezoneOffsetMinutes * 60_000);
|
|
177
|
+
f.messageHours[local.getUTCHours()]++;
|
|
178
|
+
if (lastAssistantTs !== null) {
|
|
179
|
+
const gap = (ts - lastAssistantTs) / 1000;
|
|
180
|
+
// >= 0 because clock skew between records can produce a negative gap
|
|
181
|
+
// (one of -8.662s in a real corpus). Removing the old 2s floor removed
|
|
182
|
+
// this guard with it; a negative reply latency is not a data point.
|
|
183
|
+
if (gap >= 0 && gap < GAP_CEILING_SECONDS)
|
|
184
|
+
f.responseGaps.push(gap);
|
|
185
|
+
else if (gap >= GAP_CEILING_SECONDS)
|
|
186
|
+
f.gapsOverCeiling++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
lastAssistantTs = null;
|
|
190
|
+
if (e.slashCommand)
|
|
191
|
+
bump(f.slashCommands, e.slashCommand);
|
|
192
|
+
break;
|
|
193
|
+
case 'tool_use': {
|
|
194
|
+
if (e._local)
|
|
195
|
+
break;
|
|
196
|
+
if (hasTs)
|
|
197
|
+
lastAssistantTs = ts;
|
|
198
|
+
const args = e.args ?? {};
|
|
199
|
+
const toolName = e.tool ?? '';
|
|
200
|
+
// Keyed on the SHARED cross-harness vocabulary, not Claude's literals. Keying
|
|
201
|
+
// on 'Edit'|'MultiEdit'|'Write' meant codex (whose vocabulary is exec /
|
|
202
|
+
// exec_command / write_stdin) reported 5,197 tool calls and exactly zero lines
|
|
203
|
+
// touched, rendered under the same column heading as a real number.
|
|
204
|
+
if (EDIT_TOOLS.has(toolName)) {
|
|
205
|
+
f.linesTouchedBefore += lineCount(args.old_string);
|
|
206
|
+
f.linesTouchedAfter += lineCount(args.new_string);
|
|
207
|
+
for (const edit of Array.isArray(args.edits) ? args.edits : []) {
|
|
208
|
+
f.linesTouchedBefore += lineCount(edit?.old_string);
|
|
209
|
+
f.linesTouchedAfter += lineCount(edit?.new_string);
|
|
210
|
+
}
|
|
211
|
+
f.editingToolCalls++;
|
|
212
|
+
}
|
|
213
|
+
else if (WRITE_TOOLS.has(toolName)) {
|
|
214
|
+
f.linesTouchedAfter += lineCount(args.content);
|
|
215
|
+
f.editingToolCalls++;
|
|
216
|
+
}
|
|
217
|
+
if (e.command) {
|
|
218
|
+
f.shellCommandsSeen++;
|
|
219
|
+
f.gitCommits += countGitOp(e.command, 'commit');
|
|
220
|
+
f.gitPushes += countGitOp(e.command, 'push');
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
default:
|
|
225
|
+
if (hasTs && e.role === 'assistant')
|
|
226
|
+
lastAssistantTs = ts;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return f;
|
|
230
|
+
}
|
|
231
|
+
/** Percentile of a numeric sample, nearest-rank. Returns 0 for an empty sample. */
|
|
232
|
+
export function percentile(values, p) {
|
|
233
|
+
if (values.length === 0)
|
|
234
|
+
return 0;
|
|
235
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
236
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
|
|
237
|
+
return sorted[idx];
|
|
238
|
+
}
|
|
239
|
+
/** Bucket response gaps for display. Returns every bucket, including empty ones. */
|
|
240
|
+
export function bucketGaps(gaps) {
|
|
241
|
+
const out = GAP_BUCKETS.map(([bucket]) => ({ bucket, count: 0 }));
|
|
242
|
+
for (const g of gaps) {
|
|
243
|
+
for (let i = 0; i < GAP_BUCKETS.length; i++) {
|
|
244
|
+
if (g < GAP_BUCKETS[i][1]) {
|
|
245
|
+
out[i].count++;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Detect concurrent sessions by interval intersection.
|
|
254
|
+
*
|
|
255
|
+
* This is the metric that makes the account split legible rather than academic: a
|
|
256
|
+
* cross-account overlap is `balanced` rotation actively running two orgs' quota at the
|
|
257
|
+
* same moment. `/insights` has a comparable "multi-clauding" count, but with one
|
|
258
|
+
* account it can only ever report the same-account case.
|
|
259
|
+
*
|
|
260
|
+
* Sweep in start order, keeping only spans that could still intersect, so this is
|
|
261
|
+
* O(n log n + pairs) rather than O(n^2) over ~3k sessions.
|
|
262
|
+
*/
|
|
263
|
+
export function detectOverlap(spans) {
|
|
264
|
+
const usable = spans
|
|
265
|
+
.filter((s) => Number.isFinite(s.startMs) && Number.isFinite(s.endMs) && s.endMs > s.startMs)
|
|
266
|
+
.sort((a, b) => a.startMs - b.startMs);
|
|
267
|
+
let overlappingPairs = 0;
|
|
268
|
+
let crossAccountPairs = 0;
|
|
269
|
+
const involved = new Set();
|
|
270
|
+
const active = [];
|
|
271
|
+
for (const span of usable) {
|
|
272
|
+
// Drop spans that ended before this one started; they cannot intersect it or
|
|
273
|
+
// anything after it.
|
|
274
|
+
for (let i = active.length - 1; i >= 0; i--) {
|
|
275
|
+
if (active[i].endMs <= span.startMs)
|
|
276
|
+
active.splice(i, 1);
|
|
277
|
+
}
|
|
278
|
+
for (const other of active) {
|
|
279
|
+
overlappingPairs++;
|
|
280
|
+
if (other.accountKey !== span.accountKey)
|
|
281
|
+
crossAccountPairs++;
|
|
282
|
+
involved.add(other.id);
|
|
283
|
+
involved.add(span.id);
|
|
284
|
+
}
|
|
285
|
+
active.push(span);
|
|
286
|
+
}
|
|
287
|
+
return { overlappingPairs, crossAccountPairs, sessionsInvolved: involved.size };
|
|
288
|
+
}
|
|
289
|
+
/** Merge a session's facets into a running total. */
|
|
290
|
+
export function mergeFacets(into, add) {
|
|
291
|
+
for (const [k, v] of Object.entries(add.toolCounts))
|
|
292
|
+
bump(into.toolCounts, k, v);
|
|
293
|
+
for (const [k, v] of Object.entries(add.models))
|
|
294
|
+
bump(into.models, k, v);
|
|
295
|
+
for (const [k, v] of Object.entries(add.languages))
|
|
296
|
+
bump(into.languages, k, v);
|
|
297
|
+
for (const [k, v] of Object.entries(add.slashCommands))
|
|
298
|
+
bump(into.slashCommands, k, v);
|
|
299
|
+
for (const [k, v] of Object.entries(add.errorCategories))
|
|
300
|
+
bump(into.errorCategories, k, v);
|
|
301
|
+
into.interruptions += add.interruptions;
|
|
302
|
+
into.responseGaps.push(...add.responseGaps);
|
|
303
|
+
into.gapsOverCeiling += add.gapsOverCeiling;
|
|
304
|
+
into.linesTouchedBefore += add.linesTouchedBefore;
|
|
305
|
+
into.linesTouchedAfter += add.linesTouchedAfter;
|
|
306
|
+
into.editingToolCalls += add.editingToolCalls;
|
|
307
|
+
into.filesCreated += add.filesCreated;
|
|
308
|
+
into.filesModified += add.filesModified;
|
|
309
|
+
into.filesDeleted += add.filesDeleted;
|
|
310
|
+
into.gitCommits += add.gitCommits;
|
|
311
|
+
into.gitPushes += add.gitPushes;
|
|
312
|
+
into.shellCommandsSeen += add.shellCommandsSeen;
|
|
313
|
+
into.userTurns += add.userTurns;
|
|
314
|
+
into.assistantTurns += add.assistantTurns;
|
|
315
|
+
into.toolCount += add.toolCount;
|
|
316
|
+
into.errorCount += add.errorCount;
|
|
317
|
+
for (let i = 0; i < 24; i++)
|
|
318
|
+
into.messageHours[i] += add.messageHours[i];
|
|
319
|
+
}
|
|
320
|
+
/** A fresh zeroed accumulator, for callers folding many sessions together. */
|
|
321
|
+
export function newFacetAccumulator() {
|
|
322
|
+
return emptyFacets();
|
|
323
|
+
}
|
|
324
|
+
/** Top-N entries of a count map, highest first, ties broken by name for determinism. */
|
|
325
|
+
export function topEntries(counts, limit) {
|
|
326
|
+
return Object.entries(counts)
|
|
327
|
+
.map(([name, count]) => ({ name, count }))
|
|
328
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name))
|
|
329
|
+
.slice(0, limit);
|
|
330
|
+
}
|
|
@@ -34,6 +34,23 @@ export declare function safeReadSessionFile(filePath: string, maxBytes?: number)
|
|
|
34
34
|
export interface ParseSessionOptions {
|
|
35
35
|
/** Keep normalized tool results compact by default; renderers can request full output. */
|
|
36
36
|
maxToolOutputChars?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Emit an `interrupt` event where the transcript records `[Request interrupted`.
|
|
39
|
+
*
|
|
40
|
+
* OFF by default, deliberately. That marker is not a user message, and the default
|
|
41
|
+
* event array is a versioned consumer contract: `agents sessions <id> --json`
|
|
42
|
+
* serializes it verbatim (see render.ts, issue #743), `computeSummaryStats` folds
|
|
43
|
+
* every event's timestamp into the session duration, and the live-state reader and
|
|
44
|
+
* tail renderer inspect fixed-size windows of the last N events. Emitting it
|
|
45
|
+
* unconditionally changed all four — a measured 12x duration swing on one real
|
|
46
|
+
* transcript, a new object in a published payload, and an eviction from the
|
|
47
|
+
* 12-event rate-limit window whose trigger shape (a trailing interrupt) is exactly
|
|
48
|
+
* a session the user just cancelled.
|
|
49
|
+
*
|
|
50
|
+
* `agents insights` opts in: an interruption is a real friction signal, and dropping
|
|
51
|
+
* it outright is what made it unrecoverable.
|
|
52
|
+
*/
|
|
53
|
+
includeInterrupts?: boolean;
|
|
37
54
|
}
|
|
38
55
|
export declare function parseSession(filePath: string, agent?: SessionAgentId, opts?: ParseSessionOptions): SessionEvent[];
|
|
39
56
|
/** Infer the agent type from a session file path using known directory conventions. */
|
|
@@ -54,14 +71,14 @@ export declare function isCompletedTodoStatus(status: unknown): boolean;
|
|
|
54
71
|
*/
|
|
55
72
|
export declare function summarizeToolUse(tool: string, args?: Record<string, any>): string;
|
|
56
73
|
/** Parse a Claude JSONL session file into normalized events. */
|
|
57
|
-
export declare function parseClaude(filePath: string): SessionEvent[];
|
|
74
|
+
export declare function parseClaude(filePath: string, opts?: ParseSessionOptions): SessionEvent[];
|
|
58
75
|
/**
|
|
59
76
|
* Parse Claude JSONL *content* (already read into a string) into normalized
|
|
60
77
|
* events. Split from `parseClaude` so the tail reader can parse just the last
|
|
61
78
|
* chunk of a file without re-reading the whole thing. Malformed leading lines
|
|
62
79
|
* (a tail that starts mid-line) are skipped by the per-line try/catch below.
|
|
63
80
|
*/
|
|
64
|
-
export declare function parseClaudeContent(content: string): SessionEvent[];
|
|
81
|
+
export declare function parseClaudeContent(content: string, opts?: ParseSessionOptions): SessionEvent[];
|
|
65
82
|
/** Parse a Codex JSONL session file into normalized events. */
|
|
66
83
|
export declare function parseCodex(filePath: string): SessionEvent[];
|
|
67
84
|
/**
|
|
@@ -116,6 +133,16 @@ export declare function parseGrok(filePath: string): SessionEvent[];
|
|
|
116
133
|
export declare function parseOpenCode(filePath: string): SessionEvent[];
|
|
117
134
|
/** Parse a Rush JSONL session file into normalized events. */
|
|
118
135
|
export declare function parseRush(filePath: string): SessionEvent[];
|
|
136
|
+
/**
|
|
137
|
+
* Muse Code session.jsonl → normalized events.
|
|
138
|
+
*
|
|
139
|
+
* Muse records an append-only event log. We map:
|
|
140
|
+
* - runtime.command_intake.received / turn_submit → user message
|
|
141
|
+
* - assistant_message_committed → assistant message
|
|
142
|
+
* - model_completed.usage → usage
|
|
143
|
+
* - tool-related events when present
|
|
144
|
+
*/
|
|
145
|
+
export declare function parseMuse(filePath: string): SessionEvent[];
|
|
119
146
|
/** Parse a Hermes session JSON file into normalized events. */
|
|
120
147
|
export declare function parseHermes(filePath: string): SessionEvent[];
|
|
121
148
|
/** Parse a Kimi session state.json file by reading its agents/main/wire.jsonl. */
|
|
@@ -125,7 +125,7 @@ export function parseSession(filePath, agent, opts = {}) {
|
|
|
125
125
|
let events;
|
|
126
126
|
switch (detected) {
|
|
127
127
|
case 'claude':
|
|
128
|
-
events = parseClaude(filePath);
|
|
128
|
+
events = parseClaude(filePath, opts);
|
|
129
129
|
break;
|
|
130
130
|
case 'codex':
|
|
131
131
|
events = parseCodex(filePath);
|
|
@@ -160,6 +160,9 @@ export function parseSession(filePath, agent, opts = {}) {
|
|
|
160
160
|
case 'cursor':
|
|
161
161
|
events = parseCursor(filePath);
|
|
162
162
|
break;
|
|
163
|
+
case 'muse':
|
|
164
|
+
events = parseMuse(filePath);
|
|
165
|
+
break;
|
|
163
166
|
}
|
|
164
167
|
// Chokepoint: every string field that originated in an untrusted session
|
|
165
168
|
// file gets stripped of terminal escapes here, so renderers downstream can
|
|
@@ -206,6 +209,13 @@ export function detectAgent(filePath) {
|
|
|
206
209
|
return 'droid';
|
|
207
210
|
if (filePath.includes('/.cursor/') || filePath.includes('\\.cursor\\'))
|
|
208
211
|
return 'cursor';
|
|
212
|
+
// Muse sessions: ~/.local/share/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl
|
|
213
|
+
if (filePath.includes('/muse/sessions/') ||
|
|
214
|
+
filePath.includes('\\muse\\sessions\\') ||
|
|
215
|
+
filePath.includes('/.local/share/muse/') ||
|
|
216
|
+
filePath.includes('\\.local\\share\\muse\\')) {
|
|
217
|
+
return 'muse';
|
|
218
|
+
}
|
|
209
219
|
// Cloud convention: cloud-sessions/<id>/session.<format>.jsonl
|
|
210
220
|
const cloudMatch = filePath.match(/session\.(claude|codex|rush)\.jsonl(?:$|[?#])/);
|
|
211
221
|
if (cloudMatch)
|
|
@@ -311,8 +321,8 @@ function shortenPath(p) {
|
|
|
311
321
|
// Claude parser
|
|
312
322
|
// ---------------------------------------------------------------------------
|
|
313
323
|
/** Parse a Claude JSONL session file into normalized events. */
|
|
314
|
-
export function parseClaude(filePath) {
|
|
315
|
-
return parseClaudeContent(safeReadSessionFile(filePath));
|
|
324
|
+
export function parseClaude(filePath, opts = {}) {
|
|
325
|
+
return parseClaudeContent(safeReadSessionFile(filePath), opts);
|
|
316
326
|
}
|
|
317
327
|
/**
|
|
318
328
|
* Parse Claude JSONL *content* (already read into a string) into normalized
|
|
@@ -320,7 +330,7 @@ export function parseClaude(filePath) {
|
|
|
320
330
|
* chunk of a file without re-reading the whole thing. Malformed leading lines
|
|
321
331
|
* (a tail that starts mid-line) are skipped by the per-line try/catch below.
|
|
322
332
|
*/
|
|
323
|
-
export function parseClaudeContent(content) {
|
|
333
|
+
export function parseClaudeContent(content, opts = {}) {
|
|
324
334
|
const lines = content.split('\n').filter(l => l.trim());
|
|
325
335
|
const events = [];
|
|
326
336
|
// Map tool_use id -> {tool, args} for correlating with tool_result
|
|
@@ -436,7 +446,15 @@ export function parseClaudeContent(content) {
|
|
|
436
446
|
for (const block of contentBlocks) {
|
|
437
447
|
if (block.type === 'text') {
|
|
438
448
|
const text = (block.text || '').trim();
|
|
439
|
-
if (text
|
|
449
|
+
if (text.startsWith('[Request interrupted')) {
|
|
450
|
+
// The harness's marker for a turn the user cut short, not a user
|
|
451
|
+
// message. Surfaced only on request — see includeInterrupts for why
|
|
452
|
+
// the default stream must stay byte-identical.
|
|
453
|
+
if (opts.includeInterrupts) {
|
|
454
|
+
events.push({ type: 'interrupt', agent: 'claude', timestamp, content: text });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (text) {
|
|
440
458
|
events.push({
|
|
441
459
|
type: 'message',
|
|
442
460
|
agent: 'claude',
|
|
@@ -1496,6 +1514,94 @@ export function parseRush(filePath) {
|
|
|
1496
1514
|
// system_prompt, message_count, messages: [{role, content}, ...] }
|
|
1497
1515
|
// Content may be a string or an array of text parts.
|
|
1498
1516
|
// ---------------------------------------------------------------------------
|
|
1517
|
+
/**
|
|
1518
|
+
* Muse Code session.jsonl → normalized events.
|
|
1519
|
+
*
|
|
1520
|
+
* Muse records an append-only event log. We map:
|
|
1521
|
+
* - runtime.command_intake.received / turn_submit → user message
|
|
1522
|
+
* - assistant_message_committed → assistant message
|
|
1523
|
+
* - model_completed.usage → usage
|
|
1524
|
+
* - tool-related events when present
|
|
1525
|
+
*/
|
|
1526
|
+
export function parseMuse(filePath) {
|
|
1527
|
+
const content = safeReadSessionFile(filePath);
|
|
1528
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
1529
|
+
const events = [];
|
|
1530
|
+
const museTs = (raw) => {
|
|
1531
|
+
if (typeof raw?.recorded_at === 'number') {
|
|
1532
|
+
const v = raw.recorded_at;
|
|
1533
|
+
const ms = v > 1e14 ? Math.floor(v / 1000) : v;
|
|
1534
|
+
return new Date(ms).toISOString();
|
|
1535
|
+
}
|
|
1536
|
+
return new Date().toISOString();
|
|
1537
|
+
};
|
|
1538
|
+
for (const line of lines) {
|
|
1539
|
+
let raw;
|
|
1540
|
+
try {
|
|
1541
|
+
raw = JSON.parse(line);
|
|
1542
|
+
}
|
|
1543
|
+
catch {
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const timestamp = museTs(raw);
|
|
1547
|
+
const payloadType = raw.payload_type;
|
|
1548
|
+
const payload = raw.payload;
|
|
1549
|
+
const event = payload?.event ?? payload;
|
|
1550
|
+
if (payloadType === 'runtime.command_intake.received') {
|
|
1551
|
+
const cmd = payload?.record?.command;
|
|
1552
|
+
if (cmd?.kind === 'turn_submit' && typeof cmd.prompt === 'string' && cmd.prompt.trim()) {
|
|
1553
|
+
events.push({
|
|
1554
|
+
type: 'message',
|
|
1555
|
+
agent: 'muse',
|
|
1556
|
+
timestamp,
|
|
1557
|
+
role: 'user',
|
|
1558
|
+
content: cmd.prompt.trim(),
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (event?.kind === 'assistant_message_committed' && typeof event.text === 'string') {
|
|
1564
|
+
events.push({
|
|
1565
|
+
type: 'message',
|
|
1566
|
+
agent: 'muse',
|
|
1567
|
+
timestamp,
|
|
1568
|
+
role: 'assistant',
|
|
1569
|
+
content: event.text,
|
|
1570
|
+
});
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
if (event?.kind === 'model_completed' && event.usage && typeof event.usage === 'object') {
|
|
1574
|
+
const u = event.usage;
|
|
1575
|
+
events.push({
|
|
1576
|
+
type: 'usage',
|
|
1577
|
+
agent: 'muse',
|
|
1578
|
+
timestamp,
|
|
1579
|
+
content: JSON.stringify({
|
|
1580
|
+
input_tokens: u.input_tokens ?? 0,
|
|
1581
|
+
output_tokens: u.output_tokens ?? 0,
|
|
1582
|
+
cached_tokens: u.cached_tokens ?? 0,
|
|
1583
|
+
reasoning_tokens: u.reasoning_tokens ?? 0,
|
|
1584
|
+
}),
|
|
1585
|
+
});
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
// Tool call / result shapes when Muse records them under task events
|
|
1589
|
+
if (event?.kind === 'tool_call' || event?.kind === 'tool.called') {
|
|
1590
|
+
const tool = event.tool || event.name || event.tool_name;
|
|
1591
|
+
if (typeof tool === 'string') {
|
|
1592
|
+
events.push({
|
|
1593
|
+
type: 'tool_use',
|
|
1594
|
+
agent: 'muse',
|
|
1595
|
+
timestamp,
|
|
1596
|
+
tool,
|
|
1597
|
+
args: typeof event.args === 'object' ? event.args : undefined,
|
|
1598
|
+
callId: typeof event.call_id === 'string' ? event.call_id : undefined,
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return events;
|
|
1604
|
+
}
|
|
1499
1605
|
/** Parse a Hermes session JSON file into normalized events. */
|
|
1500
1606
|
export function parseHermes(filePath) {
|
|
1501
1607
|
let session;
|
|
@@ -73,7 +73,16 @@ export declare function isAutomaticSessionPeer(d: DeviceProfile, self: string):
|
|
|
73
73
|
* address. `forwardedArgs` are the caller's own sessions args (query + filters,
|
|
74
74
|
* already `--json`) so every peer returns the same slice this machine asked for.
|
|
75
75
|
*/
|
|
76
|
-
export
|
|
76
|
+
export interface GatherRemoteListOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Opt-in early-exit for a globally-unique id lookup (a full UUID): the first
|
|
79
|
+
* peer to return the matching row resolves the fan-out and cancels the rest.
|
|
80
|
+
* Omitted for browse/label/prefix sweeps, which must wait for every peer to
|
|
81
|
+
* know whether the match is unique or conflicting.
|
|
82
|
+
*/
|
|
83
|
+
isDefinitive?: (session: SessionMeta, machine: string) => boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare function gatherRemoteList(forwardedArgs: string[], hosts?: string[], opts?: GatherRemoteListOptions): Promise<RemoteListResult>;
|
|
77
86
|
export interface RemoteToolSearchResult {
|
|
78
87
|
envelopes: Array<{
|
|
79
88
|
machine: string;
|
|
@@ -191,19 +191,13 @@ export function isAutomaticSessionPeer(d, self) {
|
|
|
191
191
|
return false;
|
|
192
192
|
return d.platform === 'windows' || d.platform === 'linux' || d.platform === 'macos';
|
|
193
193
|
}
|
|
194
|
-
|
|
195
|
-
* Gather listing sessions from other machines. With an explicit `hosts` list
|
|
196
|
-
* (from `--host`), fan out to exactly those. Otherwise sweep the registered,
|
|
197
|
-
* online devices from `ag devices`, excluding this machine and any without an
|
|
198
|
-
* address. `forwardedArgs` are the caller's own sessions args (query + filters,
|
|
199
|
-
* already `--json`) so every peer returns the same slice this machine asked for.
|
|
200
|
-
*/
|
|
201
|
-
export async function gatherRemoteList(forwardedArgs, hosts) {
|
|
194
|
+
export async function gatherRemoteList(forwardedArgs, hosts, opts) {
|
|
202
195
|
const safeResolver = forwardedArgs.includes('--resolve-safe-v1');
|
|
203
196
|
const result = await gatherRemoteAgentsJson({
|
|
204
197
|
args: forwardedArgs,
|
|
205
198
|
noFanoutEnv: NO_FANOUT_ENV,
|
|
206
199
|
hosts,
|
|
200
|
+
earlyExit: opts?.isDefinitive ? { isDefinitive: opts.isDefinitive } : undefined,
|
|
207
201
|
parse: (stdout, machine) => parseRemoteListPayload(stdout, machine, safeResolver),
|
|
208
202
|
});
|
|
209
203
|
return {
|
|
@@ -54,20 +54,74 @@ export declare function classifySshFailure(res: {
|
|
|
54
54
|
error?: Error | null;
|
|
55
55
|
status: number | null;
|
|
56
56
|
}): SshOutcome;
|
|
57
|
+
/**
|
|
58
|
+
* How long a successful remote fetch may be served without re-SSHing.
|
|
59
|
+
* Short on purpose: session listings must stay near-live (RUSH-2062). Match the
|
|
60
|
+
* active-session snapshot window so surfaces share one freshness model.
|
|
61
|
+
*/
|
|
62
|
+
export declare const REMOTE_CACHE_MAX_AGE_MS = 15000;
|
|
57
63
|
/**
|
|
58
64
|
* Deterministic cache path for a (host, forwarded-args) pair. The forwarded args
|
|
59
65
|
* are hashed so distinct queries cache independently; the host stays readable in
|
|
60
66
|
* the filename (sanitised so `user@host` and aliases are filesystem-safe).
|
|
61
67
|
*/
|
|
62
68
|
export declare function remoteCachePath(host: string, forwardedArgs: string[]): string;
|
|
69
|
+
/**
|
|
70
|
+
* Pure freshness check for a remote-sessions cache entry. A reachable host
|
|
71
|
+
* skips SSH only while this returns true; unreachable fallback ignores age.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isRemoteCacheFresh(mtimeMs: number, nowMs: number, maxAgeMs?: number): boolean;
|
|
74
|
+
export interface RemoteCacheHit {
|
|
75
|
+
output: string;
|
|
76
|
+
mtimeMs: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read a cached remote fetch. When `maxAgeMs` is set, returns null if the
|
|
80
|
+
* entry is older than the window (cache-first path for reachable hosts).
|
|
81
|
+
* Omit `maxAgeMs` to accept any age (unreachable fallback).
|
|
82
|
+
*/
|
|
83
|
+
export declare function readRemoteCache(host: string, forwardedArgs: string[], opts?: {
|
|
84
|
+
maxAgeMs?: number;
|
|
85
|
+
nowMs?: number;
|
|
86
|
+
}): RemoteCacheHit | null;
|
|
63
87
|
/** Banner shown above replayed cache rows when the peer is offline. */
|
|
64
88
|
export declare function formatStaleBanner(host: string, mtimeMs: number): string;
|
|
65
89
|
/** Message shown when a host is unreachable and there is no cache to fall back to. */
|
|
66
90
|
export declare function formatUnreachable(host: string): string;
|
|
91
|
+
/** Persist a successful fetch for later cache-first / offline replay.
|
|
92
|
+
* Best-effort: a cache write must never break the live query. Exported for tests. */
|
|
93
|
+
export declare function writeRemoteCache(host: string, forwardedArgs: string[], output: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Serve a *fresh* cache entry for a reachable-host skip (no banner — the data
|
|
96
|
+
* is still within the freshness window). Returns false when missing/stale so
|
|
97
|
+
* the caller SSHes. RUSH-2062: without this, a reachable host never skipped SSH
|
|
98
|
+
* even when the cache was just written.
|
|
99
|
+
*/
|
|
100
|
+
export declare function serveWarmRemoteCache(host: string, forwardedArgs: string[], opts?: {
|
|
101
|
+
maxAgeMs?: number;
|
|
102
|
+
nowMs?: number;
|
|
103
|
+
}): boolean;
|
|
104
|
+
/** Replay a cached fetch for an unreachable host (any age). Banner goes to
|
|
105
|
+
* stderr (so a piped stdout stays exactly the cached rows); returns false when
|
|
106
|
+
* nothing is cached for this exact (host, query). */
|
|
107
|
+
export declare function replayRemoteCache(host: string, forwardedArgs: string[]): boolean;
|
|
108
|
+
export interface RunRemoteSessionsOptions {
|
|
109
|
+
/** Skip warm cache and SSH every host (force-refresh). */
|
|
110
|
+
forceRefresh?: boolean;
|
|
111
|
+
/** Override freshness window for the warm path. */
|
|
112
|
+
maxAgeMs?: number;
|
|
113
|
+
/** Clock (tests). */
|
|
114
|
+
nowMs?: number;
|
|
115
|
+
}
|
|
67
116
|
/**
|
|
68
117
|
* Run the current `agents sessions` invocation on one or more remote machines over
|
|
69
|
-
* SSH, writing each remote's output to the terminal.
|
|
70
|
-
*
|
|
118
|
+
* SSH, writing each remote's output to the terminal.
|
|
119
|
+
*
|
|
120
|
+
* Cache policy (RUSH-2062):
|
|
121
|
+
* - **Default:** serve a fresh cache hit without SSH; SSH only on miss/stale.
|
|
122
|
+
* - **`forceRefresh`:** always SSH, then rewrite the cache.
|
|
123
|
+
* - **Unreachable:** fall back to any cached output (with a stale banner).
|
|
124
|
+
*
|
|
71
125
|
* Sets `process.exitCode = 1` if any host could not be answered (live or cached).
|
|
72
126
|
* Reads the invocation from `process.argv` (override via `argv` for testing).
|
|
73
127
|
*
|
|
@@ -75,4 +129,4 @@ export declare function formatUnreachable(host: string): string;
|
|
|
75
129
|
* Session output is small and the remote returns quickly, so buffering is
|
|
76
130
|
* imperceptible; `maxBuffer` is generous for the rare large `--markdown <id>` dump.
|
|
77
131
|
*/
|
|
78
|
-
export declare function runRemoteSessions(hosts: string[], argv?: string[]): void;
|
|
132
|
+
export declare function runRemoteSessions(hosts: string[], argv?: string[], opts?: RunRemoteSessionsOptions): void;
|