@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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insights command — how you actually work, split by the account that did the work.
|
|
3
|
+
*
|
|
4
|
+
* The behavioural sibling of the existing rollups, and deliberately not a duplicate of
|
|
5
|
+
* any of them:
|
|
6
|
+
*
|
|
7
|
+
* agents cost what you spent ($ and duration)
|
|
8
|
+
* agents output what shipped (burn vs PRs and commits)
|
|
9
|
+
* agents usage live quota headroom (rate-limit windows, right now)
|
|
10
|
+
* agents trends aggregate distributions (harness mix, tools-per-session, token ratios)
|
|
11
|
+
* agents sessions browse individual work (search, resume, render)
|
|
12
|
+
* agents insights HOW you work (tools, friction, rhythm, per account)
|
|
13
|
+
*
|
|
14
|
+
* The closest neighbour is `agents trends`, and the boundary is the data path: trends
|
|
15
|
+
* reads counters — `tool_scan_ledger` call counts and the analytics warehouse — to
|
|
16
|
+
* produce distributions ("how many tool calls per session, by harness"). This reads
|
|
17
|
+
* transcript CONTENT through `parseSession` to produce behaviour ("which tools, which
|
|
18
|
+
* languages, where it went wrong, when you were working"), and splits all of it by
|
|
19
|
+
* account, a dimension trends does not have. They overlap in spirit on tool and model
|
|
20
|
+
* mix; they do not read the same store or answer the same question.
|
|
21
|
+
*
|
|
22
|
+
* Modelled on Claude Code's `/insights`, with the difference that motivated it: that
|
|
23
|
+
* command reads one account's directory, while `balanced` rotation sprays sessions
|
|
24
|
+
* across every signed-in account. This reads the whole index and reports the accounts
|
|
25
|
+
* apart — see lib/session/claude-accounts.ts for how a transcript is attributed.
|
|
26
|
+
*
|
|
27
|
+
* The deterministic report makes zero network calls. `--narrative` is opt-in and adds
|
|
28
|
+
* the coaching prose by piping the AGGREGATE (never raw transcripts) through a headless
|
|
29
|
+
* `claude -p`.
|
|
30
|
+
*/
|
|
31
|
+
import * as fs from 'fs';
|
|
32
|
+
import chalk from 'chalk';
|
|
33
|
+
import { execFile } from 'child_process';
|
|
34
|
+
import { promisify } from 'util';
|
|
35
|
+
import { addHostOption } from '../lib/hosts/option.js';
|
|
36
|
+
import { setHelpSections } from '../lib/help.js';
|
|
37
|
+
import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
|
|
38
|
+
import { querySessions, readSessionInsights, writeSessionInsights, clearSessionInsights, } from '../lib/session/db.js';
|
|
39
|
+
import { parseSession } from '../lib/session/parse.js';
|
|
40
|
+
import { computeInsightFacets, mergeFacets, newFacetAccumulator, detectOverlap, percentile, bucketGaps, topEntries, } from '../lib/session/insights.js';
|
|
41
|
+
import { formatUsd } from '../lib/pricing/index.js';
|
|
42
|
+
import { formatDuration } from '../lib/session/render.js';
|
|
43
|
+
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
44
|
+
const execFileAsync = promisify(execFile);
|
|
45
|
+
function resolveGroup(by) {
|
|
46
|
+
if (by === undefined)
|
|
47
|
+
return 'account';
|
|
48
|
+
if (by === 'account' || by === 'agent' || by === 'project' || by === 'day')
|
|
49
|
+
return by;
|
|
50
|
+
console.error(chalk.red('error: --by must be one of: account, agent, project, day'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Sessions too short to say anything about how you work.
|
|
55
|
+
*
|
|
56
|
+
* Inspired by the filter `/insights` applies, but NOT identical and deliberately not
|
|
57
|
+
* claimed to be: `/insights` counts USER messages, while `messageCount` on the index
|
|
58
|
+
* counts both roles, so the same threshold is a weaker bar here. Matching it exactly
|
|
59
|
+
* would mean parsing every session just to decide whether to parse it. The dropped
|
|
60
|
+
* count is always reported, never silent.
|
|
61
|
+
*/
|
|
62
|
+
function isSubstantive(m, minMessages) {
|
|
63
|
+
if ((m.messageCount ?? 0) < minMessages)
|
|
64
|
+
return false;
|
|
65
|
+
if ((m.durationMs ?? 0) < 60_000)
|
|
66
|
+
return false;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function groupKeyFor(m, dim) {
|
|
70
|
+
switch (dim) {
|
|
71
|
+
case 'account': return m.accountKey ?? `unattributed:${m.agent}`;
|
|
72
|
+
case 'agent': return m.agent;
|
|
73
|
+
case 'project': return m.project || '(no project)';
|
|
74
|
+
case 'day': return m.timestamp.slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function groupLabelFor(m, dim, key) {
|
|
78
|
+
if (dim !== 'account')
|
|
79
|
+
return key;
|
|
80
|
+
if (m.accountOrg && m.account)
|
|
81
|
+
return `${m.accountOrg} <${m.account}>`;
|
|
82
|
+
return key;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Load facets for every in-scope session, parsing only what the cache does not
|
|
86
|
+
* already hold. A cold first run parses every transcript once; after that only files
|
|
87
|
+
* whose (mtime, size) changed are re-read.
|
|
88
|
+
*/
|
|
89
|
+
async function collectFacets(rows, onProgress) {
|
|
90
|
+
let unreadable = 0;
|
|
91
|
+
const cached = readSessionInsights(rows.map((r) => r.id));
|
|
92
|
+
const stale = rows.filter((r) => !cached.has(r.id) && r.filePath);
|
|
93
|
+
if (stale.length === 0)
|
|
94
|
+
return { facets: cached, unreadable };
|
|
95
|
+
const fresh = [];
|
|
96
|
+
let done = 0;
|
|
97
|
+
for (const row of stale) {
|
|
98
|
+
try {
|
|
99
|
+
// Stat BEFORE reading, so the stamp we persist describes bytes no newer than the
|
|
100
|
+
// ones parsed: a rescan landing mid-read then reads as stale, not as a hit.
|
|
101
|
+
const st = fs.statSync(row.filePath);
|
|
102
|
+
// includeInterrupts: the default event array is a versioned contract, so the
|
|
103
|
+
// marker is opt-in and this is the reader that opts in.
|
|
104
|
+
const events = parseSession(row.filePath, row.agent, { includeInterrupts: true });
|
|
105
|
+
const facets = computeInsightFacets(events);
|
|
106
|
+
cached.set(row.id, facets);
|
|
107
|
+
fresh.push({ id: row.id, fileMtimeMs: Math.floor(st.mtimeMs), fileSize: st.size, facets });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Deleted or corrupt since it was indexed. Counted and reported below, never
|
|
111
|
+
// silently contributing zero.
|
|
112
|
+
unreadable++;
|
|
113
|
+
}
|
|
114
|
+
done++;
|
|
115
|
+
if (done % 25 === 0)
|
|
116
|
+
onProgress(done, stale.length);
|
|
117
|
+
// Persist in batches so an interrupted cold run does not lose everything.
|
|
118
|
+
if (fresh.length >= 200) {
|
|
119
|
+
writeSessionInsights(fresh.splice(0, fresh.length));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (fresh.length > 0)
|
|
123
|
+
writeSessionInsights(fresh);
|
|
124
|
+
onProgress(stale.length, stale.length);
|
|
125
|
+
return { facets: cached, unreadable };
|
|
126
|
+
}
|
|
127
|
+
function buildGroups(rows, facetsById, dim) {
|
|
128
|
+
const byKey = new Map();
|
|
129
|
+
for (const m of rows) {
|
|
130
|
+
const key = groupKeyFor(m, dim);
|
|
131
|
+
let g = byKey.get(key);
|
|
132
|
+
if (!g) {
|
|
133
|
+
g = {
|
|
134
|
+
key,
|
|
135
|
+
label: groupLabelFor(m, dim, key),
|
|
136
|
+
plan: null,
|
|
137
|
+
sessions: 0,
|
|
138
|
+
costUsd: 0,
|
|
139
|
+
durationMs: 0,
|
|
140
|
+
outputTokens: 0,
|
|
141
|
+
facets: newFacetAccumulator(),
|
|
142
|
+
};
|
|
143
|
+
byKey.set(key, g);
|
|
144
|
+
}
|
|
145
|
+
g.sessions++;
|
|
146
|
+
g.costUsd += m.costUsd ?? 0;
|
|
147
|
+
g.durationMs += m.durationMs ?? 0;
|
|
148
|
+
g.outputTokens += m.outputTokens ?? 0;
|
|
149
|
+
const f = facetsById.get(m.id);
|
|
150
|
+
if (f)
|
|
151
|
+
mergeFacets(g.facets, f);
|
|
152
|
+
}
|
|
153
|
+
return [...byKey.values()].sort((a, b) => b.sessions - a.sessions || a.key.localeCompare(b.key));
|
|
154
|
+
}
|
|
155
|
+
/** A compact bar for a count relative to the row maximum. */
|
|
156
|
+
function bar(count, max, width) {
|
|
157
|
+
if (max <= 0)
|
|
158
|
+
return '';
|
|
159
|
+
const filled = Math.max(1, Math.round((count / max) * width));
|
|
160
|
+
return '█'.repeat(filled);
|
|
161
|
+
}
|
|
162
|
+
function renderCounts(title, entries, out) {
|
|
163
|
+
if (entries.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
out.push('');
|
|
166
|
+
out.push(chalk.bold(title));
|
|
167
|
+
const nameW = Math.max(...entries.map((e) => stringWidth(e.name)));
|
|
168
|
+
const countW = Math.max(...entries.map((e) => String(e.count).length));
|
|
169
|
+
const max = Math.max(...entries.map((e) => e.count));
|
|
170
|
+
const barW = Math.max(6, Math.min(28, terminalWidth() - nameW - countW - 8));
|
|
171
|
+
for (const e of entries) {
|
|
172
|
+
out.push(` ${padToWidth(e.name, nameW)} ${chalk.cyan(String(e.count).padStart(countW))} ` +
|
|
173
|
+
chalk.gray(bar(e.count, max, barW)));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function renderHours(hours, out) {
|
|
177
|
+
const total = hours.reduce((a, b) => a + b, 0);
|
|
178
|
+
if (total === 0)
|
|
179
|
+
return;
|
|
180
|
+
const blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
181
|
+
const max = Math.max(...hours);
|
|
182
|
+
const spark = hours
|
|
183
|
+
.map((h) => (h === 0 ? ' ' : blocks[Math.min(blocks.length - 1, Math.floor((h / max) * (blocks.length - 1)))]))
|
|
184
|
+
.join('');
|
|
185
|
+
out.push('');
|
|
186
|
+
out.push(chalk.bold('When you work') + chalk.gray(' (local time)'));
|
|
187
|
+
out.push(` ${chalk.cyan(spark)}`);
|
|
188
|
+
out.push(` ${chalk.gray('0h'.padEnd(6))}${chalk.gray('6h'.padEnd(6))}${chalk.gray('12h'.padEnd(6))}${chalk.gray('18h'.padEnd(5))}${chalk.gray('23h')}`);
|
|
189
|
+
}
|
|
190
|
+
function renderReport(groups, dim, meta) {
|
|
191
|
+
const out = [];
|
|
192
|
+
const scope = meta.since ? `last ${meta.since}` : 'all time';
|
|
193
|
+
out.push(chalk.bold('Insights') + chalk.gray(` ${scope} · ${meta.analyzed} of ${meta.scanned} sessions`));
|
|
194
|
+
if (groups.length === 0) {
|
|
195
|
+
out.push('');
|
|
196
|
+
out.push(chalk.gray(' No sessions in scope. Try a wider --since, or run `agents sessions --all` to index.'));
|
|
197
|
+
console.log(out.join('\n'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Per-group table — the headline, and the thing no sibling command produces.
|
|
201
|
+
out.push('');
|
|
202
|
+
out.push(chalk.bold(`By ${dim}`));
|
|
203
|
+
const labelW = Math.min(Math.max(...groups.map((g) => stringWidth(g.label)), 5), Math.max(20, terminalWidth() - 46));
|
|
204
|
+
const sessW = Math.max(...groups.map((g) => String(g.sessions).length), 3);
|
|
205
|
+
for (const g of groups) {
|
|
206
|
+
const cost = g.costUsd > 0 ? formatUsd(g.costUsd) : '—';
|
|
207
|
+
const dur = g.durationMs > 0 ? formatDuration(g.durationMs) : '—';
|
|
208
|
+
out.push(` ${padToWidth(truncateToWidth(g.label, labelW), labelW)} ` +
|
|
209
|
+
`${chalk.gray(String(g.sessions).padStart(sessW))} ${chalk.gray('sess')} ` +
|
|
210
|
+
`${chalk.green(padToWidth(cost, 9))} ${chalk.gray(dur)}`);
|
|
211
|
+
}
|
|
212
|
+
// Everything below is the whole scope folded together; per-group detail is in --json.
|
|
213
|
+
const all = newFacetAccumulator();
|
|
214
|
+
for (const g of groups)
|
|
215
|
+
mergeFacets(all, g.facets);
|
|
216
|
+
renderCounts('Top tools', topEntries(all.toolCounts, 8), out);
|
|
217
|
+
renderCounts('Languages', topEntries(all.languages, 6), out);
|
|
218
|
+
renderCounts('Models', topEntries(all.models, 6), out);
|
|
219
|
+
// Friction — the section that earns the command.
|
|
220
|
+
const gaps = all.responseGaps;
|
|
221
|
+
out.push('');
|
|
222
|
+
out.push(chalk.bold('Friction'));
|
|
223
|
+
out.push(` ${padToWidth('interruptions', 18)} ${chalk.cyan(String(all.interruptions))}` +
|
|
224
|
+
chalk.gray(' turns you cut short'));
|
|
225
|
+
out.push(` ${padToWidth('tool errors', 18)} ${chalk.cyan(String(all.errorCount))}`);
|
|
226
|
+
if (gaps.length > 0) {
|
|
227
|
+
out.push(` ${padToWidth('your reply time', 18)} ` +
|
|
228
|
+
chalk.cyan(`p50 ${Math.round(percentile(gaps, 50))}s`) + chalk.gray(` · p90 ${Math.round(percentile(gaps, 90))}s`));
|
|
229
|
+
}
|
|
230
|
+
const errs = topEntries(all.errorCategories, 6);
|
|
231
|
+
if (errs.length > 0) {
|
|
232
|
+
for (const e of errs)
|
|
233
|
+
out.push(` ${chalk.gray('·')} ${padToWidth(e.name, 16)} ${chalk.gray(String(e.count))}`);
|
|
234
|
+
}
|
|
235
|
+
// Output
|
|
236
|
+
out.push('');
|
|
237
|
+
out.push(chalk.bold('What you changed'));
|
|
238
|
+
// Gate on whether anything was actually measured, not on whether an edit-shaped call
|
|
239
|
+
// was seen. Codex patches through `exec`, so it can log edit-class calls and still
|
|
240
|
+
// expose no line arguments to count — rendering that as "0 lines" would read as "wrote
|
|
241
|
+
// nothing" for a harness that wrote plenty.
|
|
242
|
+
if (all.linesTouchedAfter > 0 || all.linesTouchedBefore > 0) {
|
|
243
|
+
// "touched", not "+/-": these are the before/after line counts of each edit, so an
|
|
244
|
+
// Edit with unchanged context lines counts them on both sides. Not a diffstat, and
|
|
245
|
+
// labelled so nobody reads it as one.
|
|
246
|
+
out.push(` ${chalk.cyan(String(all.linesTouchedAfter))} ${chalk.gray('lines written,')} ` +
|
|
247
|
+
`${chalk.cyan(String(all.linesTouchedBefore))} ${chalk.gray('replaced')} ` +
|
|
248
|
+
chalk.gray('(lines touched, not a diff)'));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
out.push(` ${chalk.gray('lines touched — not measurable for this harness (edits go through the shell)')}`);
|
|
252
|
+
}
|
|
253
|
+
out.push(` ${chalk.gray(`${all.filesCreated} created, ${all.filesModified} modified, ${all.filesDeleted} deleted`)}`);
|
|
254
|
+
// Same not-measurable rule as the lines above. These are substring-matched from
|
|
255
|
+
// shell command TEXT, and not every harness exposes it — the codex parser populates
|
|
256
|
+
// `command` for `exec_command` but not plain `exec`, its dominant tool — so gate on
|
|
257
|
+
// whether we had anything to search rather than on seeing a shell-shaped tool call.
|
|
258
|
+
// When we did, the count is real, and still disagrees with `agents output`, which
|
|
259
|
+
// counts deduped SHAs from git log.
|
|
260
|
+
if (all.shellCommandsSeen > 0) {
|
|
261
|
+
out.push(` ${chalk.gray(`${all.gitCommits} commits · ${all.gitPushes} pushes (seen in shell commands)`)}`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
out.push(` ${chalk.gray('commits — not measurable for this harness')}`);
|
|
265
|
+
}
|
|
266
|
+
renderHours(all.messageHours, out);
|
|
267
|
+
// Concurrency — direct evidence that a single-account view would be wrong.
|
|
268
|
+
if (meta.overlap.overlappingPairs > 0) {
|
|
269
|
+
out.push('');
|
|
270
|
+
out.push(chalk.bold('Parallel sessions'));
|
|
271
|
+
out.push(` ${chalk.cyan(String(meta.overlap.sessionsInvolved))} ${chalk.gray('sessions ran alongside another')}`);
|
|
272
|
+
// Pairs, not sessions — stated as pairs so the two numbers are not read as a
|
|
273
|
+
// subset of each other.
|
|
274
|
+
const crossNote = meta.overlap.crossAccountPairs > 0
|
|
275
|
+
? `, ${meta.overlap.crossAccountPairs} of them across two different accounts`
|
|
276
|
+
: '';
|
|
277
|
+
out.push(chalk.gray(` ${meta.overlap.overlappingPairs} overlapping pairs${crossNote}`));
|
|
278
|
+
}
|
|
279
|
+
if (meta.filteredOut > 0) {
|
|
280
|
+
out.push('');
|
|
281
|
+
out.push(chalk.gray(` ${meta.filteredOut} sessions excluded as too short (under ${meta.minMessages} messages or 1 minute).`));
|
|
282
|
+
}
|
|
283
|
+
if (meta.unreadable > 0) {
|
|
284
|
+
if (meta.filteredOut === 0)
|
|
285
|
+
out.push('');
|
|
286
|
+
out.push(chalk.yellow(` ${meta.unreadable} transcripts could not be read; their behaviour is missing from these totals.`));
|
|
287
|
+
}
|
|
288
|
+
if (all.gapsOverCeiling > 0) {
|
|
289
|
+
out.push(chalk.gray(` ${all.gapsOverCeiling} reply gaps over an hour excluded from the percentiles.`));
|
|
290
|
+
}
|
|
291
|
+
out.push('');
|
|
292
|
+
out.push(chalk.gray(' `agents insights --by project` to see it per repo'));
|
|
293
|
+
out.push(chalk.gray(' `agents insights --narrative` for a written read on what to change'));
|
|
294
|
+
console.log(out.join('\n'));
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The opt-in coaching layer. Pipes the AGGREGATE through a headless `claude -p` — never
|
|
298
|
+
* raw transcripts, unlike `/insights`, which ships session text to the API. Reuses
|
|
299
|
+
* whatever account the shim resolves, so there is no API key handling here.
|
|
300
|
+
*/
|
|
301
|
+
async function renderNarrative(payload) {
|
|
302
|
+
const prompt = [
|
|
303
|
+
'You are reading a developer\'s own coding-session telemetry, already aggregated.',
|
|
304
|
+
'Write a short, direct read for them. Four sections, 2-3 sentences each:',
|
|
305
|
+
'1. What is working — the patterns worth keeping.',
|
|
306
|
+
'2. What is costing you — split into the assistant\'s fault vs your own workflow.',
|
|
307
|
+
'3. Quick wins — concrete, tied to a number in the data.',
|
|
308
|
+
'4. Worth trying — one more ambitious workflow change.',
|
|
309
|
+
'Be specific and cite the numbers. No preamble, no flattery, no bullet padding.',
|
|
310
|
+
'',
|
|
311
|
+
JSON.stringify(payload),
|
|
312
|
+
].join('\n');
|
|
313
|
+
try {
|
|
314
|
+
const { stdout } = await execFileAsync('claude', ['-p', prompt], {
|
|
315
|
+
timeout: 180_000,
|
|
316
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
317
|
+
});
|
|
318
|
+
// stderr, always. Under --json stdout is a machine contract, and prose appended
|
|
319
|
+
// after the closing brace makes the payload unparseable; on a TTY stderr renders
|
|
320
|
+
// identically, so there is nothing to special-case.
|
|
321
|
+
process.stderr.write('\n' + chalk.bold('Narrative') + '\n');
|
|
322
|
+
process.stderr.write(stdout.trim().split('\n').map((l) => ` ${l}`).join('\n') + '\n');
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
const msg = err.code === 'ENOENT'
|
|
326
|
+
? 'claude is not on PATH'
|
|
327
|
+
: (err.message ?? 'unknown error');
|
|
328
|
+
console.error('');
|
|
329
|
+
console.error(chalk.red(`✗ narrative unavailable: ${msg}`));
|
|
330
|
+
console.error(chalk.gray(' The report above is complete; only the written section was skipped.'));
|
|
331
|
+
// A scripted caller asked for this section and did not get it. Say so in the exit
|
|
332
|
+
// code rather than reporting success for a partial result.
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function insightsAction(options) {
|
|
337
|
+
const dim = resolveGroup(options.by);
|
|
338
|
+
const minMessages = Number.parseInt(options.minMessages ?? '2', 10);
|
|
339
|
+
if (!Number.isFinite(minMessages) || minMessages < 0) {
|
|
340
|
+
console.error(chalk.red('error: --min-messages must be a non-negative integer'));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const since = options.since ?? '30d';
|
|
344
|
+
const sinceMs = since === 'all' ? undefined : parseTimeFilter(since);
|
|
345
|
+
// Refresh the index first, exactly as `agents cost` does, so a report never silently
|
|
346
|
+
// describes a stale picture of disk.
|
|
347
|
+
await discoverSessions({ all: true, since: since === 'all' ? undefined : since, limit: 1 });
|
|
348
|
+
if (options.refresh)
|
|
349
|
+
clearSessionInsights();
|
|
350
|
+
const filter = { sinceMs };
|
|
351
|
+
if (options.agent)
|
|
352
|
+
filter.agent = options.agent;
|
|
353
|
+
const scanned = querySessions(filter);
|
|
354
|
+
const wanted = options.account?.toLowerCase();
|
|
355
|
+
const inScope = scanned.filter((m) => {
|
|
356
|
+
if (!wanted)
|
|
357
|
+
return true;
|
|
358
|
+
return [m.accountKey, m.account, m.accountOrg]
|
|
359
|
+
.some((v) => v?.toLowerCase().includes(wanted));
|
|
360
|
+
});
|
|
361
|
+
const substantive = inScope.filter((m) => isSubstantive(m, minMessages));
|
|
362
|
+
const filteredOut = inScope.length - substantive.length;
|
|
363
|
+
const isTty = process.stdout.isTTY && !options.json;
|
|
364
|
+
const { facets: facetsById, unreadable } = await collectFacets(substantive, (done, total) => {
|
|
365
|
+
if (isTty && done < total)
|
|
366
|
+
process.stderr.write(`\rReading transcripts ${done}/${total}…`);
|
|
367
|
+
else if (isTty)
|
|
368
|
+
process.stderr.write('\r'.padEnd(40) + '\r');
|
|
369
|
+
});
|
|
370
|
+
const spans = substantive.map((m) => {
|
|
371
|
+
const start = new Date(m.timestamp).getTime();
|
|
372
|
+
return {
|
|
373
|
+
id: m.id,
|
|
374
|
+
accountKey: m.accountKey ?? `unattributed:${m.agent}`,
|
|
375
|
+
startMs: start,
|
|
376
|
+
endMs: start + (m.durationMs ?? 0),
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
const overlap = detectOverlap(spans);
|
|
380
|
+
const groups = buildGroups(substantive, facetsById, dim);
|
|
381
|
+
if (options.json) {
|
|
382
|
+
const payload = {
|
|
383
|
+
generatedAt: new Date().toISOString(),
|
|
384
|
+
window: { since: since === 'all' ? null : since },
|
|
385
|
+
scanned: inScope.length,
|
|
386
|
+
analyzed: substantive.length,
|
|
387
|
+
filteredOut,
|
|
388
|
+
unreadable,
|
|
389
|
+
minMessages,
|
|
390
|
+
by: dim,
|
|
391
|
+
overlap,
|
|
392
|
+
groups: groups.map((g) => ({
|
|
393
|
+
key: g.key,
|
|
394
|
+
label: g.label,
|
|
395
|
+
sessions: g.sessions,
|
|
396
|
+
costUsd: g.costUsd,
|
|
397
|
+
durationMs: g.durationMs,
|
|
398
|
+
outputTokens: g.outputTokens,
|
|
399
|
+
...g.facets,
|
|
400
|
+
responseGapP50: Math.round(percentile(g.facets.responseGaps, 50)),
|
|
401
|
+
responseGapP90: Math.round(percentile(g.facets.responseGaps, 90)),
|
|
402
|
+
responseGapBuckets: bucketGaps(g.facets.responseGaps),
|
|
403
|
+
// The raw sample is large and uninteresting once bucketed.
|
|
404
|
+
responseGaps: undefined,
|
|
405
|
+
})),
|
|
406
|
+
};
|
|
407
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
408
|
+
if (options.narrative)
|
|
409
|
+
await renderNarrative(payload);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
renderReport(groups, dim, {
|
|
413
|
+
since: since === 'all' ? undefined : since,
|
|
414
|
+
scanned: inScope.length,
|
|
415
|
+
analyzed: substantive.length,
|
|
416
|
+
filteredOut,
|
|
417
|
+
unreadable,
|
|
418
|
+
minMessages,
|
|
419
|
+
overlap,
|
|
420
|
+
});
|
|
421
|
+
if (options.narrative) {
|
|
422
|
+
await renderNarrative(groups.map((g) => ({
|
|
423
|
+
account: g.label, sessions: g.sessions, costUsd: g.costUsd,
|
|
424
|
+
topTools: topEntries(g.facets.toolCounts, 8),
|
|
425
|
+
languages: topEntries(g.facets.languages, 6),
|
|
426
|
+
errorCategories: topEntries(g.facets.errorCategories, 6),
|
|
427
|
+
interruptions: g.facets.interruptions,
|
|
428
|
+
linesTouchedAfter: g.facets.linesTouchedAfter, linesTouchedBefore: g.facets.linesTouchedBefore,
|
|
429
|
+
gitCommits: g.facets.gitCommits,
|
|
430
|
+
replyP50s: Math.round(percentile(g.facets.responseGaps, 50)),
|
|
431
|
+
})));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
export function registerInsightsCommand(program) {
|
|
435
|
+
const cmd = addHostOption(program.command('insights'))
|
|
436
|
+
.description('How you work — tools, friction, and rhythm, split by the account that did the work')
|
|
437
|
+
.option('--json', 'Output the full report as JSON')
|
|
438
|
+
.option('--since <time>', 'Window: 7d, 4w, 3mo, an ISO date, or "all" (default 30d)')
|
|
439
|
+
.option('--by <dimension>', 'Group by: account (default), agent, project, or day')
|
|
440
|
+
.option('--account <match>', 'Only sessions whose account key, email, or org contains this')
|
|
441
|
+
.option('--agent <id>', 'Only one harness (claude, codex, droid, …)')
|
|
442
|
+
.option('--min-messages <n>', 'Skip sessions under this many messages, both roles counted (default 2)')
|
|
443
|
+
.option('--refresh', 'Discard cached facets and re-read every transcript')
|
|
444
|
+
.option('--narrative', 'Add a written read on the numbers via a headless `claude -p`')
|
|
445
|
+
.action(async (options) => {
|
|
446
|
+
await insightsAction(options);
|
|
447
|
+
});
|
|
448
|
+
setHelpSections(cmd, {
|
|
449
|
+
examples: `
|
|
450
|
+
# Last 30 days, split by Claude account — the default
|
|
451
|
+
agents insights
|
|
452
|
+
|
|
453
|
+
# Which repo is eating the time
|
|
454
|
+
agents insights --by project --since 90d
|
|
455
|
+
|
|
456
|
+
# One account only, all of its history
|
|
457
|
+
agents insights --account "Turing Labs" --since all
|
|
458
|
+
|
|
459
|
+
# Machine-readable, for a dashboard or a slash command
|
|
460
|
+
agents insights --json
|
|
461
|
+
|
|
462
|
+
# Add a written read on what to change
|
|
463
|
+
agents insights --narrative
|
|
464
|
+
`,
|
|
465
|
+
notes: `
|
|
466
|
+
Answers "how do you work". For "what did it cost" use \`agents cost\`, for "what
|
|
467
|
+
shipped" use \`agents output\`, for live quota use \`agents usage\`.
|
|
468
|
+
|
|
469
|
+
The first run parses every in-scope transcript and caches the result; later runs
|
|
470
|
+
re-read only files that changed. \`--refresh\` forces a full re-read.
|
|
471
|
+
|
|
472
|
+
Account attribution is Claude-only today. Sessions from other harnesses group
|
|
473
|
+
under \`unattributed:<agent>\`.
|
|
474
|
+
|
|
475
|
+
Everything except \`--narrative\` is local and makes no network calls.
|
|
476
|
+
`,
|
|
477
|
+
});
|
|
478
|
+
}
|
package/dist/commands/resume.js
CHANGED
|
@@ -22,8 +22,8 @@ export function buildResumeRunArgs(session, prompt, options) {
|
|
|
22
22
|
}
|
|
23
23
|
export function registerResumeCommand(program) {
|
|
24
24
|
const cmd = program
|
|
25
|
-
.command('resume <session
|
|
26
|
-
.description('Resume a session with its original harness, version, device, account, cwd, and mode')
|
|
25
|
+
.command('resume <session> [prompt]')
|
|
26
|
+
.description('Resume a session by id, tmux alias, or exact label with its original harness, version, device, account, cwd, and mode. Searches the fleet automatically; a local full-id hit resumes with zero SSH.')
|
|
27
27
|
.option('-m, --mode <mode>', 'Override the recorded launch mode')
|
|
28
28
|
.option('-i, --interactive', 'Resume interactively even when a prompt is provided')
|
|
29
29
|
.option('--headless', 'Resume headlessly (a prompt is required)')
|
|
@@ -71,10 +71,17 @@ export function registerResumeCommand(program) {
|
|
|
71
71
|
# Resume and continue headlessly
|
|
72
72
|
agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 "finish the tests"
|
|
73
73
|
|
|
74
|
+
# Resume by exact label (auto-resumes the one match)
|
|
75
|
+
agents resume "fix the flaky ssh test"
|
|
76
|
+
|
|
77
|
+
# Resume by durable tmux alias or its unique suffix
|
|
78
|
+
agents resume ag-codex-c1f3d813
|
|
79
|
+
agents resume c1f3d813
|
|
80
|
+
|
|
74
81
|
# Deliberately change permissions
|
|
75
82
|
agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 --mode edit`,
|
|
76
83
|
notes: `
|
|
77
|
-
|
|
84
|
+
A full ID resolves from the local session database first (zero SSH) and, on a local miss, fans out with the first peer holding it cancelling the rest. An exact label always consults the fleet (labels are not globally unique) and auto-resumes the one match; a cross-machine label collision surfaces as an ambiguity.
|
|
78
85
|
Use agents run auto --resume <id> when the original account is unavailable and another harness may continue.`,
|
|
79
86
|
});
|
|
80
87
|
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as path from 'path';
|
|
|
15
15
|
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
16
16
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
17
17
|
import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
18
|
-
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
|
|
18
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, buildRemoteFileImportCommand, } from '../lib/secrets/remote.js';
|
|
19
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
20
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
21
21
|
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
@@ -333,10 +333,6 @@ function getCliVersion() {
|
|
|
333
333
|
return '0.0.0';
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
|
-
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
337
|
-
function shellQuote(s) {
|
|
338
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
339
|
-
}
|
|
340
336
|
/**
|
|
341
337
|
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
342
338
|
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
@@ -1332,7 +1328,7 @@ export function registerSecretsCommands(program) {
|
|
|
1332
1328
|
if (bundle.allow_exec)
|
|
1333
1329
|
console.log(chalk.yellow('allow_exec: true'));
|
|
1334
1330
|
if (bundle.backend === 'file')
|
|
1335
|
-
console.log(chalk.gray('backend: file (
|
|
1331
|
+
console.log(chalk.gray('backend: file (encrypted at rest; headless reads via a machine-local key, or AGENTS_SECRETS_PASSPHRASE if set — no Touch ID)'));
|
|
1336
1332
|
if (bundle.backend === 'vault')
|
|
1337
1333
|
console.log(chalk.gray('storage: synced (age-encrypted ~/.agents/vault.age; needs agents login)'));
|
|
1338
1334
|
if (bundlePolicy(bundle) === 'never') {
|
|
@@ -1588,7 +1584,7 @@ export function registerSecretsCommands(program) {
|
|
|
1588
1584
|
console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only; rotate anything sensitive out of it.'));
|
|
1589
1585
|
}
|
|
1590
1586
|
if (backend === 'file') {
|
|
1591
|
-
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE
|
|
1587
|
+
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted at rest under a machine-local key (or AGENTS_SECRETS_PASSPHRASE if set); headless reads, no Touch ID.'));
|
|
1592
1588
|
}
|
|
1593
1589
|
if (backend === 'vault') {
|
|
1594
1590
|
console.log(chalk.gray('Synced: items are encrypted in ~/.agents/vault.age. Copy that file with your sync tool of choice.'));
|
|
@@ -2064,7 +2060,7 @@ Examples:
|
|
|
2064
2060
|
.option('--vault <name>', '1Password vault name (used with --to-1password)')
|
|
2065
2061
|
.option('--host <target...>', 'Push the bundle over SSH to this target (host alias or user@host); repeatable for multiple machines')
|
|
2066
2062
|
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
2067
|
-
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file
|
|
2063
|
+
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file. file is headless-readable via the remote\'s machine-local key; it forwards AGENTS_SECRETS_PASSPHRASE over stdin only if set (opt-in).', 'keychain')
|
|
2068
2064
|
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --host)')
|
|
2069
2065
|
.option('--format <shell|json>', 'Output for --plaintext export: shell (default) or json (lossless, machine-readable; used by remote resolve)', 'shell')
|
|
2070
2066
|
.option('--to-file <path>', 'Write the bundle as an AES-256-GCM encrypted offline file (needs AGENTS_SECRETS_PASSPHRASE; symmetric counterpart of import --from-file)')
|
|
@@ -2097,20 +2093,16 @@ Examples:
|
|
|
2097
2093
|
for (const h of hosts)
|
|
2098
2094
|
assertValidSshTarget(h);
|
|
2099
2095
|
const remoteBackend = parseBackendOpt(opts.remoteBackend);
|
|
2100
|
-
// For a file-backed remote bundle
|
|
2101
|
-
//
|
|
2102
|
-
//
|
|
2103
|
-
//
|
|
2104
|
-
//
|
|
2105
|
-
//
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
throw new Error('--remote-backend file needs AGENTS_SECRETS_PASSPHRASE set locally to encrypt the ' +
|
|
2111
|
-
'bundle at rest on the remote. Set it for this command, then unlock it the same way per run.');
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2096
|
+
// For a file-backed remote bundle a passphrase is OPTIONAL. The file
|
|
2097
|
+
// store is passphrase-free by default: with AGENTS_SECRETS_PASSPHRASE
|
|
2098
|
+
// unset the remote `import --backend file` auto-provisions the remote's
|
|
2099
|
+
// own machine-local key (0600 under ~/.agents/.secrets-key/), so reads
|
|
2100
|
+
// are HEADLESS. We forward the LOCAL AGENTS_SECRETS_PASSPHRASE only when
|
|
2101
|
+
// the operator opts in by setting it (e.g. to key the bundle off-disk
|
|
2102
|
+
// under a shared secret) — shipped as the FIRST stdin line so it never
|
|
2103
|
+
// lands in argv / `ps` / the remote shell history. Forcing a shared
|
|
2104
|
+
// passphrase would defeat headless reads, so we no longer require one.
|
|
2105
|
+
const remotePassphrase = remoteBackend === 'file' ? (process.env.AGENTS_SECRETS_PASSPHRASE ?? '') : '';
|
|
2114
2106
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `ssh export`, keyMode: 'storage', agentOnly: true });
|
|
2115
2107
|
const dotenv = bundleEnvToDotenv(env);
|
|
2116
2108
|
const keyCount = Object.keys(env).length;
|
|
@@ -2126,19 +2118,22 @@ Examples:
|
|
|
2126
2118
|
for (const host of hosts) {
|
|
2127
2119
|
let res;
|
|
2128
2120
|
if (remoteBackend === 'file') {
|
|
2129
|
-
// File backend
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2132
|
-
//
|
|
2133
|
-
//
|
|
2121
|
+
// File backend: headless-readable via the remote's machine-local key
|
|
2122
|
+
// when no passphrase is set; otherwise forwards AGENTS_SECRETS_PASSPHRASE
|
|
2123
|
+
// as the FIRST stdin line (consumed by `read`, so it never lands in
|
|
2124
|
+
// argv / `ps` / remote history), then the .env. Both build a POSIX
|
|
2125
|
+
// `bash -lc` command — refuse a Windows target cleanly rather than
|
|
2126
|
+
// emit broken PowerShell.
|
|
2134
2127
|
if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2135
2128
|
failures++;
|
|
2136
2129
|
console.error(chalk.red(`${host}: file backend export to a Windows target is not yet supported.`));
|
|
2137
2130
|
continue;
|
|
2138
2131
|
}
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2132
|
+
const { remoteCmd, input } = buildRemoteFileImportCommand(resolvedBundleName, dotenv, {
|
|
2133
|
+
passphrase: remotePassphrase,
|
|
2134
|
+
force: opts.force,
|
|
2135
|
+
});
|
|
2136
|
+
res = sshExec(host, remoteCmd, { input });
|
|
2142
2137
|
}
|
|
2143
2138
|
else if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2144
2139
|
// Keychain on a Windows target: the `agents.ps1` shim doesn't
|
|
@@ -28,6 +28,8 @@ export interface SessionPickerConfig {
|
|
|
28
28
|
initialSearch?: string;
|
|
29
29
|
/** Verb shown on the Enter key in the footer (default 'resume'). */
|
|
30
30
|
enterHint?: string;
|
|
31
|
+
/** Lines the caller printed above the prompt (hidden-session footer). */
|
|
32
|
+
linesAbovePrompt?: number;
|
|
31
33
|
}
|
|
32
34
|
/** Build a cached multi-line preview string for display in the session picker. */
|
|
33
35
|
export declare function buildPreview(session: SessionMeta): string;
|
|
@@ -801,6 +801,7 @@ export async function sessionPicker(config) {
|
|
|
801
801
|
initialSearch: config.initialSearch,
|
|
802
802
|
emptyMessage: 'No sessions match.',
|
|
803
803
|
enterHint: config.enterHint ?? 'resume',
|
|
804
|
+
linesAbovePrompt: config.linesAbovePrompt,
|
|
804
805
|
});
|
|
805
806
|
if (!picked)
|
|
806
807
|
return null;
|