@phnx-labs/agents-cli 1.20.30 → 1.20.32
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 +15 -0
- package/dist/commands/sessions-picker.js +35 -10
- package/dist/commands/sessions.js +9 -1
- package/dist/commands/setup.js +8 -0
- package/dist/commands/ssh.js +145 -14
- package/dist/commands/view.js +11 -1
- package/dist/lib/agents.js +69 -18
- package/dist/lib/daemon.js +32 -0
- package/dist/lib/devices/pending.d.ts +18 -0
- package/dist/lib/devices/pending.js +103 -0
- package/dist/lib/devices/registry.d.ts +11 -0
- package/dist/lib/devices/registry.js +53 -1
- package/dist/lib/devices/sync.d.ts +61 -0
- package/dist/lib/devices/sync.js +101 -0
- package/dist/lib/session/active.d.ts +2 -0
- package/dist/lib/session/active.js +29 -1
- package/dist/lib/session/digest.d.ts +50 -0
- package/dist/lib/session/digest.js +170 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +83 -10
- package/dist/lib/state.d.ts +4 -0
- package/dist/lib/state.js +4 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +14 -0
- package/dist/lib/versions.d.ts +11 -0
- package/dist/lib/versions.js +20 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import { summarizeToolUse } from './parse.js';
|
|
|
11
11
|
import { cleanSessionPrompt, extractSessionTopic } from './prompt.js';
|
|
12
12
|
import { renderMarkdown } from '../markdown.js';
|
|
13
13
|
import { redactSecrets } from '../redact.js';
|
|
14
|
+
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from './digest.js';
|
|
14
15
|
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
15
16
|
/**
|
|
16
17
|
* Return absPath relative to cwd; fall back to ~/… then absolute.
|
|
@@ -153,6 +154,7 @@ export function collapseRetries(commands) {
|
|
|
153
154
|
/** Compute aggregate statistics (turns, tools, tokens, duration) from session events. */
|
|
154
155
|
export function computeSummaryStats(events) {
|
|
155
156
|
const modelSet = new Set();
|
|
157
|
+
const toolCounts = {};
|
|
156
158
|
let userTurns = 0;
|
|
157
159
|
let assistantTurns = 0;
|
|
158
160
|
let toolCount = 0;
|
|
@@ -177,6 +179,8 @@ export function computeSummaryStats(events) {
|
|
|
177
179
|
}
|
|
178
180
|
else if (e.type === 'tool_use' && !e._local) {
|
|
179
181
|
toolCount++;
|
|
182
|
+
if (e.tool)
|
|
183
|
+
toolCounts[e.tool] = (toolCounts[e.tool] ?? 0) + 1;
|
|
180
184
|
}
|
|
181
185
|
else if (e.type === 'error') {
|
|
182
186
|
errorCount++;
|
|
@@ -193,6 +197,7 @@ export function computeSummaryStats(events) {
|
|
|
193
197
|
userTurns,
|
|
194
198
|
assistantTurns,
|
|
195
199
|
toolCount,
|
|
200
|
+
toolCounts,
|
|
196
201
|
errorCount,
|
|
197
202
|
outputTokens,
|
|
198
203
|
cacheReadTokens,
|
|
@@ -426,6 +431,76 @@ function renderActivityLine(item) {
|
|
|
426
431
|
return chalk.green('Msg ') + ' ' + chalk.gray('"' + trim(item.label) + '"');
|
|
427
432
|
}
|
|
428
433
|
}
|
|
434
|
+
// ── Catch-up digest sections ──────────────────────────────────────────────────
|
|
435
|
+
const OP_GLYPH = {
|
|
436
|
+
created: (s) => chalk.green(s),
|
|
437
|
+
modified: (s) => chalk.yellow(s),
|
|
438
|
+
deleted: (s) => chalk.red(s),
|
|
439
|
+
};
|
|
440
|
+
const OP_MARK = { created: '+', modified: '~', deleted: '−' };
|
|
441
|
+
/**
|
|
442
|
+
* Render the Changes section: files grouped by directory, each tagged with its
|
|
443
|
+
* create/modify/delete lifecycle, plus a `+N ~N −N` summary. Replaces the old
|
|
444
|
+
* flat "Modified" list. Returns true if anything was rendered.
|
|
445
|
+
*/
|
|
446
|
+
function renderChangesSection(lines, events, cwd) {
|
|
447
|
+
// In-project changes only; edits outside cwd (e.g. /tmp) keep their own
|
|
448
|
+
// "External edits" section so they don't clutter the project's changeset.
|
|
449
|
+
const inCwd = (p) => !cwd || !p.startsWith('/') || p.startsWith(cwd + '/');
|
|
450
|
+
const changes = classifyFileChanges(events).filter(ch => inCwd(ch.path));
|
|
451
|
+
if (changes.length === 0)
|
|
452
|
+
return false;
|
|
453
|
+
const c = changeCounts(changes);
|
|
454
|
+
const opByRel = new Map();
|
|
455
|
+
for (const ch of changes)
|
|
456
|
+
opByRel.set(relativeToCwd(ch.path, cwd), ch.op);
|
|
457
|
+
const summary = [
|
|
458
|
+
c.created ? chalk.green(`+${c.created}`) : '',
|
|
459
|
+
c.modified ? chalk.yellow(`~${c.modified}`) : '',
|
|
460
|
+
c.deleted ? chalk.red(`−${c.deleted}`) : '',
|
|
461
|
+
].filter(Boolean).join(' ');
|
|
462
|
+
lines.push(chalk.bold('Changes') + chalk.gray(` (${changes.length}) `) + summary);
|
|
463
|
+
const groups = groupByParentDir(changes.map(ch => ch.path), cwd);
|
|
464
|
+
const single = groups.size === 1;
|
|
465
|
+
for (const [dir, files] of groups) {
|
|
466
|
+
// Single dir: show the full relative path per file (dir/base). Multiple
|
|
467
|
+
// dirs: a dir header, then bare filenames under it.
|
|
468
|
+
if (!single)
|
|
469
|
+
lines.push(' ' + chalk.dim(dir + '/'));
|
|
470
|
+
for (const f of files.sort()) {
|
|
471
|
+
const rel = dir === '.' ? f : `${dir}/${f}`;
|
|
472
|
+
const op = opByRel.get(rel) ?? 'modified';
|
|
473
|
+
const shown = single ? rel : f;
|
|
474
|
+
const name = op === 'deleted' ? chalk.strikethrough(chalk.gray(shown)) : shown;
|
|
475
|
+
lines.push((single ? ' ' : ' ') + OP_GLYPH[op](OP_MARK[op]) + ' ' + name);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
lines.push('');
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
/** Render the tool histogram: `Edit 61 · Bash 48 · Read 35 …`. */
|
|
482
|
+
function renderToolsSection(lines, stats) {
|
|
483
|
+
const hist = toolHistogram(stats.toolCounts, 8);
|
|
484
|
+
if (hist.length === 0)
|
|
485
|
+
return;
|
|
486
|
+
const parts = hist.map(h => `${chalk.white(h.tool)} ${chalk.gray(String(h.count))}`);
|
|
487
|
+
lines.push(chalk.bold('Tools') + ' ' + parts.join(chalk.gray(' · ')));
|
|
488
|
+
lines.push('');
|
|
489
|
+
}
|
|
490
|
+
/** Render the last test/build verdict, e.g. `Tests tests: 294 pass · 4 fail`. */
|
|
491
|
+
function renderTestsLine(lines, events) {
|
|
492
|
+
const r = detectTestResult(events);
|
|
493
|
+
if (!r || !r.ok)
|
|
494
|
+
return;
|
|
495
|
+
const bits = [];
|
|
496
|
+
if (r.passed !== undefined)
|
|
497
|
+
bits.push(chalk.green(`${r.passed} pass`));
|
|
498
|
+
if (r.failed !== undefined)
|
|
499
|
+
bits.push(r.failed > 0 ? chalk.red(`${r.failed} fail`) : chalk.gray('0 fail'));
|
|
500
|
+
const verdict = r.failed && r.failed > 0 ? chalk.red('✗') : chalk.green('✓');
|
|
501
|
+
lines.push(chalk.bold('Tests') + ` ${verdict} ${chalk.cyan(r.runner)} ${bits.join(chalk.gray(' · '))}`);
|
|
502
|
+
lines.push('');
|
|
503
|
+
}
|
|
429
504
|
// ── Main summary renderer ─────────────────────────────────────────────────────
|
|
430
505
|
/**
|
|
431
506
|
* Render session as an activity summary.
|
|
@@ -560,7 +635,6 @@ export function renderSummary(events, cwd) {
|
|
|
560
635
|
}
|
|
561
636
|
return m;
|
|
562
637
|
};
|
|
563
|
-
const modifiedAbsMap = buildAbsMap(filesModifiedAbs);
|
|
564
638
|
const readAbsMap = buildAbsMap(filesReadAbs);
|
|
565
639
|
// ── Render sections ───────────────────────────────────────────────────────
|
|
566
640
|
const lines = [''];
|
|
@@ -640,15 +714,14 @@ export function renderSummary(events, cwd) {
|
|
|
640
714
|
chalk.gray(`: ${errors.length} failure${errors.length !== 1 ? 's' : ''} — first: ${firstDesc}`));
|
|
641
715
|
lines.push('');
|
|
642
716
|
}
|
|
643
|
-
// 6.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
//
|
|
651
|
-
// Filter out plan files (already shown in Plan section)
|
|
717
|
+
// 6. Changes — files grouped by directory with create/modify/delete lifecycle
|
|
718
|
+
// (replaces the old flat "Modified" + "External edits" lists).
|
|
719
|
+
renderChangesSection(lines, events, cwd);
|
|
720
|
+
// 6b. Catch-up signals: last test/build verdict, then the tool histogram.
|
|
721
|
+
renderTestsLine(lines, events);
|
|
722
|
+
renderToolsSection(lines, computeSummaryStats(events));
|
|
723
|
+
// 6c. External edits (files edited outside the project root — typically /tmp).
|
|
724
|
+
// Filter out plan files (already shown in Plan section).
|
|
652
725
|
const externalNonPlan = [...filesModifiedExternal].filter(p => !(p.includes('.claude/plans/') && p.endsWith('.md')));
|
|
653
726
|
if (externalNonPlan.length > 0) {
|
|
654
727
|
const externalList = externalNonPlan.sort();
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -156,6 +156,10 @@ export declare function getTeamsAgentsDir(): string;
|
|
|
156
156
|
export declare function getTeamsRegistryPath(): string;
|
|
157
157
|
/** Path to the device registry — SSH device profiles with platform/auth metadata. Durable runtime, per-machine (host list + addresses are NOT pulled by `agents repo push`). */
|
|
158
158
|
export declare function getDevicesRegistryPath(): string;
|
|
159
|
+
/** Path to the device ignore-list — tailscale node names the user dismissed, so auto-discovery never re-suggests them. Per-machine, same dir as the registry. */
|
|
160
|
+
export declare function getDevicesIgnoredPath(): string;
|
|
161
|
+
/** Dir of "pending device" sentinels (~/.agents/.cache/state/devices-pending/) — one empty-ish file per newly-discovered, not-yet-approved tailnet node. Written by the daemon probe, read by the menu-bar helper (mirrors the attention sentinel dir). */
|
|
162
|
+
export declare function getDevicesPendingDir(): string;
|
|
159
163
|
/** Path to cloud dispatch cache (~/.agents/.cache/cloud/). */
|
|
160
164
|
export declare function getCloudDir(): string;
|
|
161
165
|
/** Path to terminal session metadata (~/.agents/.cache/terminals/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -352,6 +352,10 @@ export function getTeamsAgentsDir() { return TEAMS_AGENTS_DIR; }
|
|
|
352
352
|
export function getTeamsRegistryPath() { return path.join(HISTORY_DIR, 'teams', 'registry.json'); }
|
|
353
353
|
/** Path to the device registry — SSH device profiles with platform/auth metadata. Durable runtime, per-machine (host list + addresses are NOT pulled by `agents repo push`). */
|
|
354
354
|
export function getDevicesRegistryPath() { return path.join(HISTORY_DIR, 'devices', 'registry.json'); }
|
|
355
|
+
/** Path to the device ignore-list — tailscale node names the user dismissed, so auto-discovery never re-suggests them. Per-machine, same dir as the registry. */
|
|
356
|
+
export function getDevicesIgnoredPath() { return path.join(HISTORY_DIR, 'devices', 'ignored.json'); }
|
|
357
|
+
/** Dir of "pending device" sentinels (~/.agents/.cache/state/devices-pending/) — one empty-ish file per newly-discovered, not-yet-approved tailnet node. Written by the daemon probe, read by the menu-bar helper (mirrors the attention sentinel dir). */
|
|
358
|
+
export function getDevicesPendingDir() { return path.join(RUNTIME_STATE_DIR, 'devices-pending'); }
|
|
355
359
|
/** Path to cloud dispatch cache (~/.agents/.cache/cloud/). */
|
|
356
360
|
export function getCloudDir() { return CLOUD_DIR; }
|
|
357
361
|
/** Path to terminal session metadata (~/.agents/.cache/terminals/). */
|
|
@@ -120,6 +120,20 @@ export async function runUmbrellaSync(args) {
|
|
|
120
120
|
const { refresh } = await import('./refresh.js');
|
|
121
121
|
await refresh({ skipPrompts: yes });
|
|
122
122
|
result.reconciled = true;
|
|
123
|
+
// Keep already-registered devices' reachability current, and surface newly
|
|
124
|
+
// appeared tailnet nodes as "pending" for the menu-bar Register/Ignore gate
|
|
125
|
+
// rather than silently adding them (refresh mode). Soft: a machine without
|
|
126
|
+
// tailscale is a clean no-op, never a sync failure. First-run population is
|
|
127
|
+
// `agents setup` / manual `agents devices sync` (bootstrap).
|
|
128
|
+
const { runDeviceSync } = await import('./devices/sync.js');
|
|
129
|
+
const { reconcilePendingSentinels } = await import('./devices/pending.js');
|
|
130
|
+
const dev = await runDeviceSync({ soft: true, mode: 'refresh' });
|
|
131
|
+
if (dev.ok)
|
|
132
|
+
reconcilePendingSentinels(dev.pending);
|
|
133
|
+
result.devices = { synced: dev.synced, pending: dev.pending.length, skipped: !dev.ok };
|
|
134
|
+
if (dev.ok) {
|
|
135
|
+
log(`devices: ${dev.synced} refreshed${dev.pending.length ? `, ${dev.pending.length} new pending` : ''}`);
|
|
136
|
+
}
|
|
123
137
|
}
|
|
124
138
|
return result;
|
|
125
139
|
}
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -197,6 +197,17 @@ export declare function installVersion(agent: AgentId, version: string, onProgre
|
|
|
197
197
|
* when nothing was resolved or no stale dir is present, so it is safe to call
|
|
198
198
|
* on every script-based install. Returns the action taken (for tests/logging).
|
|
199
199
|
*/
|
|
200
|
+
/**
|
|
201
|
+
* Proactively fold a stale `latest` version-home into its concrete version,
|
|
202
|
+
* WITHOUT needing a fresh install (RUSH-1320). `reconcileStaleLatestDir` only
|
|
203
|
+
* fires at install time, so a `latest` dir left by an old probe-failed install
|
|
204
|
+
* lingers in `agents view` indefinitely. This resolves the live CLI version and
|
|
205
|
+
* reconciles — cheap no-op when there's no `latest` dir (the common case, so
|
|
206
|
+
* `agents view` pays a `--version` shell-out only the once, until it's folded).
|
|
207
|
+
* Skipped when the active config symlink still points at `latest`, since
|
|
208
|
+
* renaming that dir would dangle the live symlink.
|
|
209
|
+
*/
|
|
210
|
+
export declare function reconcileStaleLatestForAgent(agent: AgentId): Promise<void>;
|
|
200
211
|
export declare function reconcileStaleLatestDir(agent: AgentId, installedVersion: string): Promise<'none' | 'renamed' | 'trashed'>;
|
|
201
212
|
/**
|
|
202
213
|
* Soft-delete a version directory by moving it to ~/.agents/.system/trash/versions/.
|
package/dist/lib/versions.js
CHANGED
|
@@ -1209,6 +1209,26 @@ function removeInstallArtifacts(versionDir) {
|
|
|
1209
1209
|
* when nothing was resolved or no stale dir is present, so it is safe to call
|
|
1210
1210
|
* on every script-based install. Returns the action taken (for tests/logging).
|
|
1211
1211
|
*/
|
|
1212
|
+
/**
|
|
1213
|
+
* Proactively fold a stale `latest` version-home into its concrete version,
|
|
1214
|
+
* WITHOUT needing a fresh install (RUSH-1320). `reconcileStaleLatestDir` only
|
|
1215
|
+
* fires at install time, so a `latest` dir left by an old probe-failed install
|
|
1216
|
+
* lingers in `agents view` indefinitely. This resolves the live CLI version and
|
|
1217
|
+
* reconciles — cheap no-op when there's no `latest` dir (the common case, so
|
|
1218
|
+
* `agents view` pays a `--version` shell-out only the once, until it's folded).
|
|
1219
|
+
* Skipped when the active config symlink still points at `latest`, since
|
|
1220
|
+
* renaming that dir would dangle the live symlink.
|
|
1221
|
+
*/
|
|
1222
|
+
export async function reconcileStaleLatestForAgent(agent) {
|
|
1223
|
+
if (!fs.existsSync(getVersionDir(agent, 'latest')))
|
|
1224
|
+
return;
|
|
1225
|
+
if (getConfigSymlinkVersion(agent) === 'latest')
|
|
1226
|
+
return;
|
|
1227
|
+
const concrete = await getCliVersionFromPath(agent);
|
|
1228
|
+
if (concrete && concrete !== 'latest') {
|
|
1229
|
+
await reconcileStaleLatestDir(agent, concrete);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1212
1232
|
export async function reconcileStaleLatestDir(agent, installedVersion) {
|
|
1213
1233
|
if (installedVersion === 'latest')
|
|
1214
1234
|
return 'none';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.32",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|