@phnx-labs/agents-cli 1.20.90 → 1.20.92
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 +276 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +72 -6
- package/dist/commands/activity.js +198 -49
- package/dist/commands/beta.js +1 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/feed.js +77 -4
- package/dist/commands/hooks.js +22 -6
- package/dist/commands/perf.d.ts +14 -0
- package/dist/commands/perf.js +221 -0
- package/dist/commands/projects.d.ts +12 -0
- package/dist/commands/projects.js +358 -0
- package/dist/commands/routines.js +30 -24
- package/dist/commands/secrets.d.ts +43 -4
- package/dist/commands/secrets.js +217 -32
- package/dist/commands/send.d.ts +5 -1
- package/dist/commands/send.js +1 -1
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +107 -13
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +9 -1
- package/dist/commands/sessions.d.ts +10 -5
- package/dist/commands/sessions.js +65 -27
- package/dist/index.js +19 -3
- package/dist/lib/activity.d.ts +80 -13
- package/dist/lib/activity.js +418 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/catchup.d.ts +105 -0
- package/dist/lib/catchup.js +160 -0
- package/dist/lib/channels/providers/desktop.d.ts +49 -0
- package/dist/lib/channels/providers/desktop.js +132 -0
- package/dist/lib/channels/providers/index.js +2 -0
- package/dist/lib/daemon.js +74 -13
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/events.d.ts +12 -0
- package/dist/lib/events.js +122 -9
- package/dist/lib/exec.js +10 -0
- package/dist/lib/feed-broadcast.d.ts +47 -0
- package/dist/lib/feed-broadcast.js +65 -1
- package/dist/lib/feed-post.d.ts +10 -0
- package/dist/lib/feed-post.js +9 -3
- package/dist/lib/feed.d.ts +47 -1
- package/dist/lib/feed.js +38 -0
- package/dist/lib/hooks/cache.d.ts +2 -0
- package/dist/lib/hooks/cache.js +24 -4
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +14 -4
- package/dist/lib/menubar/install-menubar.js +20 -6
- package/dist/lib/overdue.d.ts +14 -0
- package/dist/lib/overdue.js +37 -1
- package/dist/lib/perf/db.d.ts +25 -0
- package/dist/lib/perf/db.js +290 -0
- package/dist/lib/perf/spool.d.ts +18 -0
- package/dist/lib/perf/spool.js +79 -0
- package/dist/lib/perf/types.d.ts +45 -0
- package/dist/lib/perf/types.js +2 -0
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -0
- package/dist/lib/project-root.js +16 -0
- package/dist/lib/project-status.d.ts +69 -0
- package/dist/lib/project-status.js +101 -0
- package/dist/lib/projects.d.ts +138 -0
- package/dist/lib/projects.js +301 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- package/dist/lib/routines-project.js +6 -0
- package/dist/lib/routines.d.ts +30 -1
- package/dist/lib/routines.js +11 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/list-filter.d.ts +94 -0
- package/dist/lib/secrets/list-filter.js +245 -0
- package/dist/lib/session/bash-command.d.ts +53 -0
- package/dist/lib/session/bash-command.js +364 -0
- package/dist/lib/session/digest.d.ts +13 -0
- package/dist/lib/session/digest.js +48 -1
- package/dist/lib/session/discover.d.ts +1 -2
- package/dist/lib/session/discover.js +7 -24
- package/dist/lib/session/highlights.d.ts +82 -0
- package/dist/lib/session/highlights.js +251 -0
- package/dist/lib/session/parse.js +23 -1
- package/dist/lib/session/relative-time.d.ts +37 -0
- package/dist/lib/session/relative-time.js +96 -8
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +9 -9
- package/dist/lib/session/render.js +112 -73
- package/dist/lib/session/types.d.ts +4 -1
- package/dist/lib/ssh-exec.d.ts +6 -0
- package/dist/lib/ssh-exec.js +10 -1
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +11 -0
- package/dist/lib/state.js +16 -0
- package/dist/lib/terminal/backends/index.d.ts +10 -2
- package/dist/lib/terminal/backends/index.js +14 -2
- package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
- package/dist/lib/terminal/backends/terminal-app.js +73 -0
- package/dist/lib/terminal/index.d.ts +2 -1
- package/dist/lib/terminal/index.js +2 -1
- package/dist/lib/terminal/preferred.d.ts +89 -0
- package/dist/lib/terminal/preferred.js +87 -0
- package/dist/lib/terminal/run-surface.d.ts +82 -0
- package/dist/lib/terminal/run-surface.js +146 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/package.json +4 -1
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* offline-cache banner (`remote.ts`) share one formatter — `sessions.ts` imports
|
|
5
5
|
* `remote.ts`, so a back-import would cycle.
|
|
6
6
|
*/
|
|
7
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
8
|
+
/**
|
|
9
|
+
* Calendar label for a timestamp older than the relative window. A 2-digit year
|
|
10
|
+
* is appended outside the current year so "Jun 28" is never ambiguous across
|
|
11
|
+
* years (e.g. "Jun 28 '25").
|
|
12
|
+
*/
|
|
13
|
+
function calendarLabel(thenMs, nowMs) {
|
|
14
|
+
const d = new Date(thenMs);
|
|
15
|
+
const label = `${MONTHS[d.getMonth()]} ${d.getDate()}`;
|
|
16
|
+
return d.getFullYear() === new Date(nowMs).getFullYear()
|
|
17
|
+
? label
|
|
18
|
+
: `${label} '${String(d.getFullYear()).slice(-2)}`;
|
|
19
|
+
}
|
|
7
20
|
export function formatRelativeTime(isoTimestamp) {
|
|
8
21
|
const now = Date.now();
|
|
9
22
|
const then = new Date(isoTimestamp).getTime();
|
|
@@ -21,12 +34,87 @@ export function formatRelativeTime(isoTimestamp) {
|
|
|
21
34
|
return `${diffHrs} hour${diffHrs === 1 ? '' : 's'} ago`;
|
|
22
35
|
if (diffDays < 7)
|
|
23
36
|
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
|
|
24
|
-
// Older: show the date.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
// Older: show the date.
|
|
38
|
+
return calendarLabel(then, now);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Compact age — `now`, `45m`, `3h`, `2d`, then the calendar date. This is the
|
|
42
|
+
* *creation* half of a session's time cell: it renders right next to the long
|
|
43
|
+
* last-activity label, and one row can afford one long form, not two.
|
|
44
|
+
*/
|
|
45
|
+
export function formatCompactAge(isoTimestamp) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const then = new Date(isoTimestamp).getTime();
|
|
48
|
+
if (isNaN(then))
|
|
49
|
+
return isoTimestamp;
|
|
50
|
+
const diffMs = now - then;
|
|
51
|
+
const diffMin = Math.floor(diffMs / 60_000);
|
|
52
|
+
const diffHrs = Math.floor(diffMs / 3_600_000);
|
|
53
|
+
const diffDays = Math.floor(diffMs / 86_400_000);
|
|
54
|
+
if (diffMin < 1)
|
|
55
|
+
return 'now';
|
|
56
|
+
if (diffMin < 60)
|
|
57
|
+
return `${diffMin}m`;
|
|
58
|
+
if (diffHrs < 24)
|
|
59
|
+
return `${diffHrs}h`;
|
|
60
|
+
if (diffDays < 7)
|
|
61
|
+
return `${diffDays}d`;
|
|
62
|
+
return calendarLabel(then, now);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Shortest creation→last-activity gap worth rendering as two fields. Below it
|
|
66
|
+
* both halves name the same minute, so the row would carry a duplicate instead
|
|
67
|
+
* of information.
|
|
68
|
+
*/
|
|
69
|
+
const SPAN_MIN_MS = 60_000;
|
|
70
|
+
/**
|
|
71
|
+
* Split a session's timestamps into the two fields a listing renders: when it
|
|
72
|
+
* was created and when it was last active. One last-activity label answers
|
|
73
|
+
* neither "is this the old session I'm looking for" nor "how long did it run" —
|
|
74
|
+
* a session touched an hour ago may have started last week.
|
|
75
|
+
*/
|
|
76
|
+
export function sessionAgeParts(createdIso, lastActivityIso) {
|
|
77
|
+
const lastIso = lastActivityIso ?? createdIso;
|
|
78
|
+
const last = formatRelativeTime(lastIso);
|
|
79
|
+
const createdMs = new Date(createdIso).getTime();
|
|
80
|
+
const lastMs = new Date(lastIso).getTime();
|
|
81
|
+
if (isNaN(createdMs) || isNaN(lastMs) || lastMs - createdMs < SPAN_MIN_MS)
|
|
82
|
+
return { last };
|
|
83
|
+
return { created: formatCompactAge(createdIso), last };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse a time filter string (relative like '7d' or an ISO timestamp) into epoch
|
|
87
|
+
* milliseconds. Backs `--since` on `sessions`, `cost`, `teams`, and `output`, and
|
|
88
|
+
* `--unused` on `secrets list`.
|
|
89
|
+
*
|
|
90
|
+
* It lives here, in a module with no imports, rather than in `discover.ts` where
|
|
91
|
+
* it started: `discover.ts` loads `../sqlite.js`, so importing this one function
|
|
92
|
+
* from there pulls `node:sqlite` into the caller's module graph and Node prints
|
|
93
|
+
* `ExperimentalWarning: SQLite …` on stderr. That is invisible in a command that
|
|
94
|
+
* already touches the session DB, and a broken contract in one that doesn't —
|
|
95
|
+
* `monitors --json` asserts a clean stderr. `discover.ts` re-exports it, so
|
|
96
|
+
* existing importers are unaffected.
|
|
97
|
+
*/
|
|
98
|
+
export function parseTimeFilter(input) {
|
|
99
|
+
// Units: m=minute, h=hour, d=day, w=week, mo=month(30d), y=year(365d). `mo`
|
|
100
|
+
// must precede the single-letter alternatives so "1mo" isn't read as "1m"+"o".
|
|
101
|
+
const relativeMatch = input.match(/^(\d+)(mo|[mhdwy])$/i);
|
|
102
|
+
if (relativeMatch) {
|
|
103
|
+
const value = parseInt(relativeMatch[1], 10);
|
|
104
|
+
const unit = relativeMatch[2].toLowerCase();
|
|
105
|
+
if (unit === 'm')
|
|
106
|
+
return Date.now() - value * 60_000;
|
|
107
|
+
if (unit === 'h')
|
|
108
|
+
return Date.now() - value * 3_600_000;
|
|
109
|
+
if (unit === 'd')
|
|
110
|
+
return Date.now() - value * 86_400_000;
|
|
111
|
+
if (unit === 'w')
|
|
112
|
+
return Date.now() - value * 7 * 86_400_000;
|
|
113
|
+
if (unit === 'mo')
|
|
114
|
+
return Date.now() - value * 30 * 86_400_000;
|
|
115
|
+
if (unit === 'y')
|
|
116
|
+
return Date.now() - value * 365 * 86_400_000;
|
|
117
|
+
}
|
|
118
|
+
const ts = new Date(input).getTime();
|
|
119
|
+
return Number.isNaN(ts) ? 0 : ts;
|
|
32
120
|
}
|
|
@@ -18,7 +18,7 @@ import chalk from 'chalk';
|
|
|
18
18
|
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
19
19
|
import { sshTargetFor } from '../devices/connect.js';
|
|
20
20
|
import { resolveExplicitTargets } from '../devices/resolve-target.js';
|
|
21
|
-
import { loadDevices, isControlDevice } from '../devices/registry.js';
|
|
21
|
+
import { loadDevices, isControlDevice, isDialableDevice } from '../devices/registry.js';
|
|
22
22
|
import { remoteShellFor, buildWindowsAgentsCommand } from '../hosts/remote-cmd.js';
|
|
23
23
|
import { machineId, normalizeHost } from './sync/config.js';
|
|
24
24
|
import { NO_FANOUT_ENV } from './remote-active.js';
|
|
@@ -163,7 +163,10 @@ export async function gatherRemoteList(forwardedArgs, hosts) {
|
|
|
163
163
|
return { sessions: [], deviceCount: 0, unreachable: ['device registry'] };
|
|
164
164
|
}
|
|
165
165
|
for (const d of Object.values(reg)) {
|
|
166
|
-
|
|
166
|
+
// Live SSH-probe verdict first, cached tailscale snapshot only as a
|
|
167
|
+
// fallback — see isDialableDevice. A manually-registered device has no
|
|
168
|
+
// tailscale peer entry, so gating on `online` alone hid its sessions.
|
|
169
|
+
if (!isDialableDevice(d))
|
|
167
170
|
continue;
|
|
168
171
|
if (normalizeHost(d.name) === self)
|
|
169
172
|
continue;
|
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
* transcript. Filtering by role and turn slicing is handled here as well.
|
|
8
8
|
*/
|
|
9
9
|
import type { SessionEvent, SessionMeta } from './types.js';
|
|
10
|
+
import { unwrapCommand, bucketKey } from './bash-command.js';
|
|
10
11
|
/**
|
|
11
12
|
* Return absPath relative to cwd; fall back to ~/… then absolute.
|
|
12
13
|
*/
|
|
13
14
|
export declare function relativeToCwd(absPath: string, cwd?: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Display form for a touched path: cwd-relative first; then collapse any
|
|
17
|
+
* `.agents/worktrees/<slug>` segment (in-cwd OR outside) to `⧉ <slug>/…` so
|
|
18
|
+
* group labels stay on one line instead of `~/src/…/.agents/worktrees/<slug>/…`;
|
|
19
|
+
* else home-collapse.
|
|
20
|
+
*/
|
|
21
|
+
export declare function displayPath(absPath: string, cwd?: string): string;
|
|
14
22
|
/**
|
|
15
23
|
* Wrap a filesystem path label in an OSC 8 `file://` hyperlink when the terminal
|
|
16
24
|
* supports it. Degrades to the plain label otherwise.
|
|
@@ -21,18 +29,11 @@ export declare function linkPath(absPath: string, label: string): string;
|
|
|
21
29
|
* issue, …) when the terminal supports it. Degrades to the plain label otherwise.
|
|
22
30
|
*/
|
|
23
31
|
export declare function linkUrl(url: string, label: string): string;
|
|
24
|
-
/**
|
|
25
|
-
* Unwrap wrapper prefixes to find the actual executable.
|
|
26
|
-
*/
|
|
27
|
-
export declare function unwrapCommand(cmd: string): string;
|
|
28
32
|
/**
|
|
29
33
|
* Normalize a command so trivial flag/pipe variations collapse to the same key.
|
|
30
34
|
*/
|
|
31
35
|
export declare function normalizeForDedup(cmd: string): string;
|
|
32
|
-
|
|
33
|
-
* Return the bucket key for a command (used for grouping within a category).
|
|
34
|
-
*/
|
|
35
|
-
export declare function bucketKey(cmd: string): string;
|
|
36
|
+
export { unwrapCommand, bucketKey };
|
|
36
37
|
interface CmdRun {
|
|
37
38
|
normalized: string;
|
|
38
39
|
raw: string;
|
|
@@ -125,4 +126,3 @@ export declare function renderConversationMarkdown(events: SessionEvent[], opts?
|
|
|
125
126
|
export declare function renderJson(events: SessionEvent[], meta?: SessionMeta, opts?: {
|
|
126
127
|
redact?: boolean;
|
|
127
128
|
}): string;
|
|
128
|
-
export {};
|
|
@@ -13,6 +13,8 @@ import { cleanSessionPrompt, extractSessionTopic } from './prompt.js';
|
|
|
13
13
|
import { renderMarkdown } from '../markdown.js';
|
|
14
14
|
import { redactSecrets } from '../redact.js';
|
|
15
15
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from './digest.js';
|
|
16
|
+
import { classifyBashCommand, unwrapCommand, bucketKey } from './bash-command.js';
|
|
17
|
+
import { extractArtifacts, extractHooks, extractLinks, extractSkills } from './highlights.js';
|
|
16
18
|
import { extractTodoProgressFromEvents } from './state.js';
|
|
17
19
|
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
18
20
|
/**
|
|
@@ -29,6 +31,31 @@ export function relativeToCwd(absPath, cwd) {
|
|
|
29
31
|
}
|
|
30
32
|
return absPath;
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Display form for a touched path: cwd-relative first; then collapse any
|
|
36
|
+
* `.agents/worktrees/<slug>` segment (in-cwd OR outside) to `⧉ <slug>/…` so
|
|
37
|
+
* group labels stay on one line instead of `~/src/…/.agents/worktrees/<slug>/…`;
|
|
38
|
+
* else home-collapse.
|
|
39
|
+
*/
|
|
40
|
+
export function displayPath(absPath, cwd) {
|
|
41
|
+
const rel = relativeToCwd(absPath, cwd);
|
|
42
|
+
const norm = rel.replace(/\\/g, '/');
|
|
43
|
+
const wt = norm.match(/(^|\/)\.agents\/worktrees\/([^/]+)/);
|
|
44
|
+
if (wt) {
|
|
45
|
+
const after = norm.slice(norm.indexOf(wt[0]) + wt[0].length).replace(/^\//, '');
|
|
46
|
+
return after ? `⧉ ${wt[2]}/${after}` : `⧉ ${wt[2]}`;
|
|
47
|
+
}
|
|
48
|
+
return rel;
|
|
49
|
+
}
|
|
50
|
+
/** One checklist line with a status marker: `[x] done` / `[>] doing` / `[ ] todo`. */
|
|
51
|
+
function renderTodoMarker(item) {
|
|
52
|
+
const text = item.content;
|
|
53
|
+
if (item.status === 'completed')
|
|
54
|
+
return chalk.green('[x]') + ' ' + chalk.gray(text);
|
|
55
|
+
if (item.status === 'in_progress')
|
|
56
|
+
return chalk.yellow('[>]') + ' ' + chalk.white(text);
|
|
57
|
+
return chalk.gray('[ ]') + ' ' + chalk.white(text);
|
|
58
|
+
}
|
|
32
59
|
/** Best-effort feature-detect for OSC 8 hyperlink support in the current TTY. */
|
|
33
60
|
function supportsHyperlinks() {
|
|
34
61
|
return Boolean(process.stdout.isTTY &&
|
|
@@ -56,28 +83,6 @@ export function linkUrl(url, label) {
|
|
|
56
83
|
return supportsHyperlinks() ? osc8(url, label) : label;
|
|
57
84
|
}
|
|
58
85
|
// ── Command grouping ──────────────────────────────────────────────────────────
|
|
59
|
-
/**
|
|
60
|
-
* Unwrap wrapper prefixes to find the actual executable.
|
|
61
|
-
*/
|
|
62
|
-
export function unwrapCommand(cmd) {
|
|
63
|
-
const ssh = cmd.match(/^ssh\s+\S+\s+"(.+)"\s*(?:\|.*)?$/);
|
|
64
|
-
if (ssh)
|
|
65
|
-
return unwrapCommand(ssh[1]);
|
|
66
|
-
const lead = cmd.match(/^(?:sudo|env\s+\S+=\S+|time)\s+(.+)/);
|
|
67
|
-
if (lead)
|
|
68
|
-
return unwrapCommand(lead[1]);
|
|
69
|
-
// Strip shell-style leading env assignments: `PATH=/x CMD ...`, `FOO=bar BAR=baz CMD ...`
|
|
70
|
-
const shellEnv = cmd.match(/^(?:[A-Z_][A-Z0-9_]*=\S+\s+)+(\S.*)$/);
|
|
71
|
-
if (shellEnv)
|
|
72
|
-
return unwrapCommand(shellEnv[1]);
|
|
73
|
-
const cd = cmd.match(/^cd\s+\S+\s*&&\s*(.+)/);
|
|
74
|
-
if (cd)
|
|
75
|
-
return unwrapCommand(cd[1]);
|
|
76
|
-
const npx = cmd.match(/^npx\s+(.+)/);
|
|
77
|
-
if (npx)
|
|
78
|
-
return unwrapCommand(npx[1]);
|
|
79
|
-
return cmd;
|
|
80
|
-
}
|
|
81
86
|
/**
|
|
82
87
|
* Normalize a command so trivial flag/pipe variations collapse to the same key.
|
|
83
88
|
*/
|
|
@@ -94,45 +99,35 @@ export function normalizeForDedup(cmd) {
|
|
|
94
99
|
}
|
|
95
100
|
return s.trim();
|
|
96
101
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
export function bucketKey(cmd) {
|
|
117
|
-
const unwrapped = unwrapCommand(cmd);
|
|
118
|
-
const tokens = unwrapped.trim().split(/\s+/);
|
|
119
|
-
const first = tokens[0] ?? 'other';
|
|
120
|
-
const isRemote = cmd.trim().startsWith('ssh ') || cmd.trim().startsWith('scp ');
|
|
121
|
-
if (TWO_LEVEL_TOKENS.has(first) && tokens[1]) {
|
|
122
|
-
const key = `${first} ${tokens[1]}`;
|
|
123
|
-
return isRemote ? `ssh\u2192${key}` : key;
|
|
124
|
-
}
|
|
125
|
-
return isRemote ? `ssh\u2192${first}` : first;
|
|
126
|
-
}
|
|
102
|
+
// Re-export the shared classifier/bucketing for callers in this module's surface.
|
|
103
|
+
// `bucketKey` is the single source of truth in bash-command.ts (correct subcommand
|
|
104
|
+
// scan + `ssh\u2192` remote prefix); render must not keep a divergent copy.
|
|
105
|
+
export { unwrapCommand, bucketKey };
|
|
106
|
+
const CATEGORY_NAMES = {
|
|
107
|
+
vcs: 'VCS',
|
|
108
|
+
'build-test': 'Build/test',
|
|
109
|
+
install: 'Install',
|
|
110
|
+
remote: 'Remote',
|
|
111
|
+
http: 'HTTP',
|
|
112
|
+
media: 'Media',
|
|
113
|
+
upscaling: 'Upscaling',
|
|
114
|
+
metadata: 'Metadata',
|
|
115
|
+
probe: 'Probes',
|
|
116
|
+
search: 'Search',
|
|
117
|
+
shell: 'Shell',
|
|
118
|
+
wait: 'Wait',
|
|
119
|
+
other: 'Other',
|
|
120
|
+
};
|
|
127
121
|
function categoryOf(cmd) {
|
|
128
122
|
const rawFirst = cmd.trim().split(/\s+/)[0]?.toLowerCase() ?? '';
|
|
129
123
|
// Remote wrappers: classify as Remote regardless of inner command.
|
|
130
124
|
if (['ssh', 'scp', 'rsync'].includes(rawFirst)) {
|
|
131
|
-
return
|
|
125
|
+
return { name: 'Remote', signal: 'mid' };
|
|
132
126
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
127
|
+
const info = classifyBashCommand(cmd);
|
|
128
|
+
if (info.category === 'other')
|
|
129
|
+
return null;
|
|
130
|
+
return { name: CATEGORY_NAMES[info.category], signal: info.signal };
|
|
136
131
|
}
|
|
137
132
|
/**
|
|
138
133
|
* Collapse consecutive same-normalized commands within a 60-second window
|
|
@@ -387,11 +382,12 @@ function pickDistinct(samples, max) {
|
|
|
387
382
|
return result.length > 0 ? result : samples.slice(0, max);
|
|
388
383
|
}
|
|
389
384
|
// ── File grouping ─────────────────────────────────────────────────────────────
|
|
390
|
-
/** Group file paths by their parent directory,
|
|
385
|
+
/** Group file paths by their parent directory, in display form (cwd-relative,
|
|
386
|
+
* worktree-collapsed, home-collapsed). */
|
|
391
387
|
function groupByParentDir(paths, cwd) {
|
|
392
388
|
const groups = new Map();
|
|
393
389
|
for (const p of paths) {
|
|
394
|
-
const rel =
|
|
390
|
+
const rel = displayPath(p, cwd);
|
|
395
391
|
const slashIdx = rel.lastIndexOf('/');
|
|
396
392
|
const dir = slashIdx >= 0 ? rel.slice(0, slashIdx) : '.';
|
|
397
393
|
const base = slashIdx >= 0 ? rel.slice(slashIdx + 1) : rel;
|
|
@@ -454,17 +450,17 @@ const OP_MARK = { created: '+', modified: '~', deleted: '−' };
|
|
|
454
450
|
* create/modify/delete lifecycle, plus a `+N ~N −N` summary. Replaces the old
|
|
455
451
|
* flat "Modified" list. Returns true if anything was rendered.
|
|
456
452
|
*/
|
|
457
|
-
function renderChangesSection(lines,
|
|
453
|
+
function renderChangesSection(lines, allChanges, cwd) {
|
|
458
454
|
// In-project changes only; edits outside cwd (e.g. /tmp) keep their own
|
|
459
455
|
// "External edits" section so they don't clutter the project's changeset.
|
|
460
456
|
const inCwd = (p) => !cwd || !p.startsWith('/') || p.startsWith(cwd + '/');
|
|
461
|
-
const changes =
|
|
457
|
+
const changes = allChanges.filter(ch => inCwd(ch.path));
|
|
462
458
|
if (changes.length === 0)
|
|
463
459
|
return false;
|
|
464
460
|
const c = changeCounts(changes);
|
|
465
461
|
const opByRel = new Map();
|
|
466
462
|
for (const ch of changes)
|
|
467
|
-
opByRel.set(
|
|
463
|
+
opByRel.set(displayPath(ch.path, cwd), ch.op);
|
|
468
464
|
const summary = [
|
|
469
465
|
c.created ? chalk.green(`+${c.created}`) : '',
|
|
470
466
|
c.modified ? chalk.yellow(`~${c.modified}`) : '',
|
|
@@ -528,8 +524,8 @@ export function renderSummary(events, cwd) {
|
|
|
528
524
|
const filesModifiedExternal = new Set();
|
|
529
525
|
// Commands with timestamps
|
|
530
526
|
const cmdList = [];
|
|
531
|
-
// Plan items
|
|
532
|
-
const todoItems = extractTodoProgressFromEvents(events)?.items
|
|
527
|
+
// Plan items (checklist entries keep their status for [x]/[>]/[ ] markers)
|
|
528
|
+
const todoItems = extractTodoProgressFromEvents(events)?.items ?? [];
|
|
533
529
|
let exitPlanContent = null;
|
|
534
530
|
let planFilePath = null;
|
|
535
531
|
// Subagent spawns
|
|
@@ -559,7 +555,7 @@ export function renderSummary(events, cwd) {
|
|
|
559
555
|
}
|
|
560
556
|
else {
|
|
561
557
|
(isInsideCwd(p) || !cwd ? filesModifiedAbs : filesModifiedExternal).add(p);
|
|
562
|
-
recentActivity.push({ kind: 'edit', label:
|
|
558
|
+
recentActivity.push({ kind: 'edit', label: displayPath(p, cwd), ts, absPath: p });
|
|
563
559
|
}
|
|
564
560
|
}
|
|
565
561
|
}
|
|
@@ -624,11 +620,11 @@ export function renderSummary(events, cwd) {
|
|
|
624
620
|
filesReadAbs.delete(p);
|
|
625
621
|
for (const p of filesModifiedExternal)
|
|
626
622
|
filesReadAbs.delete(p);
|
|
627
|
-
// Build abs→
|
|
623
|
+
// Build abs→display mapping for linkPath
|
|
628
624
|
const buildAbsMap = (absSet) => {
|
|
629
625
|
const m = new Map();
|
|
630
626
|
for (const abs of absSet) {
|
|
631
|
-
const rel =
|
|
627
|
+
const rel = displayPath(abs, cwd);
|
|
632
628
|
m.set(rel, abs);
|
|
633
629
|
}
|
|
634
630
|
return m;
|
|
@@ -668,22 +664,27 @@ export function renderSummary(events, cwd) {
|
|
|
668
664
|
}
|
|
669
665
|
lines.push('');
|
|
670
666
|
}
|
|
671
|
-
// 3. Plan
|
|
667
|
+
// 3. Plan — the plan document (ExitPlanMode text / plan file) AND the live
|
|
668
|
+
// checklist. Both render: the checklist used to be hidden whenever plan text
|
|
669
|
+
// existed, which read as "this session had no todos".
|
|
672
670
|
if (todoItems.length > 0 || exitPlanContent || planFilePath) {
|
|
673
671
|
lines.push(chalk.bold('Plan'));
|
|
674
672
|
if (planFilePath) {
|
|
675
673
|
const home = process.env.HOME ?? '';
|
|
676
|
-
const
|
|
677
|
-
lines.push(' ' + chalk.cyan(
|
|
674
|
+
const planPathDisplay = home && planFilePath.startsWith(home) ? planFilePath.replace(home, '~') : planFilePath;
|
|
675
|
+
lines.push(' ' + chalk.cyan(planPathDisplay));
|
|
678
676
|
}
|
|
679
677
|
if (exitPlanContent) {
|
|
680
678
|
const planLines = exitPlanContent.split('\n').slice(0, 10);
|
|
681
679
|
for (const l of planLines)
|
|
682
680
|
lines.push(' ' + l);
|
|
683
681
|
}
|
|
684
|
-
|
|
682
|
+
if (todoItems.length > 0) {
|
|
685
683
|
for (const item of todoItems.slice(0, 20)) {
|
|
686
|
-
lines.push('
|
|
684
|
+
lines.push(' ' + renderTodoMarker(item));
|
|
685
|
+
}
|
|
686
|
+
if (todoItems.length > 20) {
|
|
687
|
+
lines.push(' ' + chalk.gray(`… (${todoItems.length - 20} more)`));
|
|
687
688
|
}
|
|
688
689
|
}
|
|
689
690
|
lines.push('');
|
|
@@ -698,6 +699,30 @@ export function renderSummary(events, cwd) {
|
|
|
698
699
|
}
|
|
699
700
|
lines.push('');
|
|
700
701
|
}
|
|
702
|
+
// 4b. Highlights — what the session USED (skills, hooks) and the references
|
|
703
|
+
// it mentioned (links). Shared extraction with the picker preview so the two
|
|
704
|
+
// renders never disagree.
|
|
705
|
+
const skills = extractSkills(events);
|
|
706
|
+
if (skills.length > 0) {
|
|
707
|
+
const shown = skills.map(s => chalk.white(s.name) + (s.count > 1 ? chalk.gray(` ×${s.count}`) : ''));
|
|
708
|
+
lines.push(chalk.bold('Skills') + chalk.gray(` (${skills.length})`) + ' ' + shown.join(chalk.gray(' · ')));
|
|
709
|
+
lines.push('');
|
|
710
|
+
}
|
|
711
|
+
const hooks = extractHooks(events);
|
|
712
|
+
if (hooks.length > 0) {
|
|
713
|
+
const shown = hooks.map(h => chalk.white(h.name) + (h.count > 1 ? chalk.gray(` ×${h.count}`) : '') + (h.failed ? chalk.red(` (${h.failed} failed)`) : ''));
|
|
714
|
+
lines.push(chalk.bold('Hooks') + chalk.gray(` (${hooks.length})`) + ' ' + shown.join(chalk.gray(' · ')));
|
|
715
|
+
lines.push('');
|
|
716
|
+
}
|
|
717
|
+
const links = extractLinks(events);
|
|
718
|
+
if (links.length > 0) {
|
|
719
|
+
// Width-capped like the picker's Dirs line: a link-heavy session must not
|
|
720
|
+
// wrap the summary pane.
|
|
721
|
+
const shown = links.slice(0, 6).map(l => chalk.blue(linkUrl(l.url, l.label)));
|
|
722
|
+
const more = links.length > 6 ? chalk.gray(` · +${links.length - 6} more`) : '';
|
|
723
|
+
lines.push(chalk.bold('Links') + chalk.gray(` (${links.length})`) + ' ' + shown.join(chalk.gray(' · ')) + more);
|
|
724
|
+
lines.push('');
|
|
725
|
+
}
|
|
701
726
|
// 5. Errors (moved up from the bottom: it describes failed attempts, not the
|
|
702
727
|
// session's final state. Sitting at the bottom previously made early errors
|
|
703
728
|
// look recent, which confused readers.)
|
|
@@ -713,8 +738,22 @@ export function renderSummary(events, cwd) {
|
|
|
713
738
|
lines.push('');
|
|
714
739
|
}
|
|
715
740
|
// 6. Changes — files grouped by directory with create/modify/delete lifecycle
|
|
716
|
-
// (replaces the old flat "Modified" + "External edits" lists).
|
|
717
|
-
|
|
741
|
+
// (replaces the old flat "Modified" + "External edits" lists). Classified
|
|
742
|
+
// once here and shared with the Artifacts section below.
|
|
743
|
+
const allChanges = classifyFileChanges(events);
|
|
744
|
+
renderChangesSection(lines, allChanges, cwd);
|
|
745
|
+
// 6a. Artifacts — documents the session PRODUCED (`.agents/artifacts|plans|
|
|
746
|
+
// reports`, other *.md/*.html creations), named and clickable. These drown in
|
|
747
|
+
// the Changeset's source churn, so they get their own section.
|
|
748
|
+
const artifacts = extractArtifacts(allChanges);
|
|
749
|
+
if (artifacts.length > 0) {
|
|
750
|
+
lines.push(chalk.bold('Artifacts') + chalk.gray(` (${artifacts.length})`));
|
|
751
|
+
for (const a of artifacts) {
|
|
752
|
+
const tag = a.bucket === 'docs' ? '' : chalk.gray(` (${a.bucket})`);
|
|
753
|
+
lines.push(' ' + chalk.green('+') + ' ' + chalk.cyan(linkPath(a.path, a.basename)) + tag);
|
|
754
|
+
}
|
|
755
|
+
lines.push('');
|
|
756
|
+
}
|
|
718
757
|
// 6b. Catch-up signals: last test/build verdict, then the tool histogram.
|
|
719
758
|
renderTestsLine(lines, events);
|
|
720
759
|
renderToolsSection(lines, computeSummaryStats(events));
|
|
@@ -18,7 +18,7 @@ export declare const SESSION_AGENTS: SessionAgentId[];
|
|
|
18
18
|
export declare function isSessionTrackedAgent(agent: string): agent is SessionAgentId;
|
|
19
19
|
/** A single normalized event within a session (message, tool call, thinking, etc.). */
|
|
20
20
|
export interface SessionEvent {
|
|
21
|
-
type: 'message' | 'tool_use' | 'tool_result' | 'thinking' | 'error' | 'init' | 'result' | 'usage' | 'attachment';
|
|
21
|
+
type: 'message' | 'tool_use' | 'tool_result' | 'thinking' | 'error' | 'init' | 'result' | 'usage' | 'attachment' | 'hook';
|
|
22
22
|
agent: SessionAgentId;
|
|
23
23
|
timestamp: string;
|
|
24
24
|
role?: 'user' | 'assistant';
|
|
@@ -49,6 +49,9 @@ export interface SessionEvent {
|
|
|
49
49
|
name?: string;
|
|
50
50
|
mediaType?: string;
|
|
51
51
|
sizeBytes?: number;
|
|
52
|
+
hookName?: string;
|
|
53
|
+
/** Lifecycle event the hook fired on (SessionStart, PreToolUse, …). */
|
|
54
|
+
hookEvent?: string;
|
|
52
55
|
}
|
|
53
56
|
/** A displayable file attachment discovered in a session transcript. */
|
|
54
57
|
export interface SessionAttachment {
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -73,6 +73,12 @@ export declare function sshExec(target: string, remoteCmd: string, opts?: SshExe
|
|
|
73
73
|
/**
|
|
74
74
|
* Async variant of {@link sshExec}. Same hardened argv composition, but uses
|
|
75
75
|
* child_process.spawn so fleet fan-outs can probe multiple hosts concurrently.
|
|
76
|
+
*
|
|
77
|
+
* A timeout-bearing call uses a fresh ssh connection (`multiplex: false`) even
|
|
78
|
+
* when the caller requests multiplexing: a control-master outlives the local
|
|
79
|
+
* client, so killing the local ssh process on timeout would leave the remote
|
|
80
|
+
* command running. With a direct connection, terminating the local child tears
|
|
81
|
+
* down the remote side (RUSH-2114).
|
|
76
82
|
*/
|
|
77
83
|
export declare function sshExecAsync(target: string, remoteCmd: string, opts?: SshExecOptions): Promise<SshExecResult>;
|
|
78
84
|
export interface SshExecRawResult {
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -128,10 +128,19 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
128
128
|
/**
|
|
129
129
|
* Async variant of {@link sshExec}. Same hardened argv composition, but uses
|
|
130
130
|
* child_process.spawn so fleet fan-outs can probe multiple hosts concurrently.
|
|
131
|
+
*
|
|
132
|
+
* A timeout-bearing call uses a fresh ssh connection (`multiplex: false`) even
|
|
133
|
+
* when the caller requests multiplexing: a control-master outlives the local
|
|
134
|
+
* client, so killing the local ssh process on timeout would leave the remote
|
|
135
|
+
* command running. With a direct connection, terminating the local child tears
|
|
136
|
+
* down the remote side (RUSH-2114).
|
|
131
137
|
*/
|
|
132
138
|
export function sshExecAsync(target, remoteCmd, opts = {}) {
|
|
133
139
|
assertValidSshTarget(target);
|
|
134
|
-
|
|
140
|
+
// Control-master connections defeat local timeouts — the master keeps the
|
|
141
|
+
// remote command alive after we kill the client. Force a fresh connection
|
|
142
|
+
// whenever the caller asked for a timeout so the timeout actually stops work.
|
|
143
|
+
const mux = opts.multiplex === false || opts.timeoutMs ? [] : controlOpts();
|
|
135
144
|
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
136
145
|
return new Promise((resolve) => {
|
|
137
146
|
const child = spawn('ssh', args, {
|
|
@@ -44,6 +44,7 @@ export declare const loadExport: ModuleLoader;
|
|
|
44
44
|
export declare const loadPackages: ModuleLoader;
|
|
45
45
|
export declare const loadRoutines: ModuleLoader;
|
|
46
46
|
export declare const loadMonitors: ModuleLoader;
|
|
47
|
+
export declare const loadProjects: ModuleLoader;
|
|
47
48
|
export declare const loadRun: ModuleLoader;
|
|
48
49
|
export declare const loadFork: ModuleLoader;
|
|
49
50
|
export declare const loadDefaults: ModuleLoader;
|
|
@@ -70,6 +71,7 @@ export declare const loadDrive: ModuleLoader;
|
|
|
70
71
|
export declare const loadFactory: ModuleLoader;
|
|
71
72
|
export declare const loadUsage: ModuleLoader;
|
|
72
73
|
export declare const loadCost: ModuleLoader;
|
|
74
|
+
export declare const loadPerf: ModuleLoader;
|
|
73
75
|
export declare const loadOutput: ModuleLoader;
|
|
74
76
|
export declare const loadBudget: ModuleLoader;
|
|
75
77
|
export declare const loadAlias: ModuleLoader;
|
|
@@ -22,6 +22,7 @@ export const loadExport = async () => (await import('../../commands/export.js'))
|
|
|
22
22
|
export const loadPackages = async () => (await import('../../commands/packages.js')).registerPackagesCommands;
|
|
23
23
|
export const loadRoutines = async () => (await import('../../commands/routines.js')).registerRoutinesCommands;
|
|
24
24
|
export const loadMonitors = async () => (await import('../../commands/monitors.js')).registerMonitorsCommands;
|
|
25
|
+
export const loadProjects = async () => (await import('../../commands/projects.js')).registerProjectsCommands;
|
|
25
26
|
export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
|
|
26
27
|
export const loadFork = async () => (await import('../../commands/fork.js')).registerForkCommand;
|
|
27
28
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
@@ -48,6 +49,7 @@ export const loadDrive = async () => (await import('../../commands/drive.js')).r
|
|
|
48
49
|
export const loadFactory = async () => (await import('../../commands/factory.js')).registerFactoryCommands;
|
|
49
50
|
export const loadUsage = async () => (await import('../../commands/usage.js')).registerUsageCommand;
|
|
50
51
|
export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
|
|
52
|
+
export const loadPerf = async () => (await import('../../commands/perf.js')).registerPerfCommand;
|
|
51
53
|
export const loadOutput = async () => (await import('../../commands/output.js')).registerOutputCommand;
|
|
52
54
|
export const loadBudget = async () => (await import('../../commands/budget.js')).registerBudgetCommand;
|
|
53
55
|
export const loadAlias = async () => (await import('../../commands/alias.js')).registerAliasCommand;
|
|
@@ -133,6 +135,7 @@ export const COMMAND_LOADERS = {
|
|
|
133
135
|
install: [loadPackages],
|
|
134
136
|
routines: [loadRoutines],
|
|
135
137
|
monitors: [loadMonitors],
|
|
138
|
+
projects: [loadProjects],
|
|
136
139
|
run: [loadRun],
|
|
137
140
|
fork: [loadFork],
|
|
138
141
|
defaults: [loadDefaults],
|
|
@@ -162,6 +165,7 @@ export const COMMAND_LOADERS = {
|
|
|
162
165
|
factory: [loadFactory],
|
|
163
166
|
usage: [loadUsage],
|
|
164
167
|
cost: [loadCost],
|
|
168
|
+
perf: [loadPerf],
|
|
165
169
|
output: [loadOutput],
|
|
166
170
|
budget: [loadBudget],
|
|
167
171
|
alias: [loadAlias],
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -109,6 +109,8 @@ export declare function getCacheDir(): string;
|
|
|
109
109
|
export declare function getPackagesDir(): string;
|
|
110
110
|
/** Path to routine YAML definitions (~/.agents/routines/). */
|
|
111
111
|
export declare function getRoutinesDir(): string;
|
|
112
|
+
/** Path to named project definitions (~/.agents/projects/). */
|
|
113
|
+
export declare function getProjectsDir(): string;
|
|
112
114
|
/**
|
|
113
115
|
* Path to webhook handler YAML definitions (~/.agents/webhooks/). Handlers are
|
|
114
116
|
* one-off triggers for agents/workflows/commands/routines, layered the same way
|
|
@@ -208,6 +210,15 @@ export declare function getCloudDir(): string;
|
|
|
208
210
|
export declare function getTerminalsDir(): string;
|
|
209
211
|
/** Path to runtime logs (~/.agents/.cache/logs/). */
|
|
210
212
|
export declare function getLogsDir(): string;
|
|
213
|
+
/**
|
|
214
|
+
* Path to disposable performance samples (~/.agents/.cache/perf/).
|
|
215
|
+
* Holds `perf.db` + a hook-shim spool. Loss is acceptable — wipe freely.
|
|
216
|
+
*/
|
|
217
|
+
export declare function getPerfDir(): string;
|
|
218
|
+
/** Path to the perf SQLite warehouse (~/.agents/.cache/perf/perf.db). */
|
|
219
|
+
export declare function getPerfDbPath(): string;
|
|
220
|
+
/** Path to the hook-shim NDJSON spool drained into perf.db on open. */
|
|
221
|
+
export declare function getPerfSpoolPath(): string;
|
|
211
222
|
/** Path to per-process runtime state (~/.agents/.cache/state/). */
|
|
212
223
|
export declare function getRuntimeStateDir(): string;
|
|
213
224
|
/** Path to companion-extension scratch (~/.agents/.cache/companion/). */
|