@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
|
@@ -13,6 +13,13 @@ export interface FileChange {
|
|
|
13
13
|
path: string;
|
|
14
14
|
op: FileOp;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Path-shaped noise that must never surface as a session "change": shell
|
|
18
|
+
* redirect tokens (`2>&1`), unexpanded env-var prefixes (`$WT/...`), dependency
|
|
19
|
+
* dirs (`node_modules`), and agents-cli's internal `.system` marker. Filtered at
|
|
20
|
+
* the source so every consumer (summary, picker, digest counts) agrees.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isNoisePath(p: string): boolean;
|
|
16
23
|
/** Extract file paths deleted by a shell command (rm / git rm / unlink). Conservative. */
|
|
17
24
|
export declare function extractDeletedPaths(command: string): string[];
|
|
18
25
|
/**
|
|
@@ -35,6 +42,12 @@ export declare function toolHistogram(toolCounts: Record<string, number>, top?:
|
|
|
35
42
|
tool: string;
|
|
36
43
|
count: number;
|
|
37
44
|
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Aggregate recognized external tools invoked via Bash across a session.
|
|
47
|
+
* Unknown one-off commands are bucketed as `other` so the ranking surface
|
|
48
|
+
* (e.g. `agents activity --tools`) only surfaces repeatable tools.
|
|
49
|
+
*/
|
|
50
|
+
export declare function bashToolCounts(events: SessionEvent[]): Record<string, number>;
|
|
38
51
|
export interface TestResult {
|
|
39
52
|
runner: string;
|
|
40
53
|
passed?: number;
|
|
@@ -7,11 +7,35 @@
|
|
|
7
7
|
* last test/build result. Consumed by the single-session view and the picker
|
|
8
8
|
* preview. No I/O — fully unit-testable.
|
|
9
9
|
*/
|
|
10
|
+
import { bucketKey, classifyBashCommand } from './bash-command.js';
|
|
10
11
|
// Tool vocab mirrors parse.ts / render.ts so classification matches what those
|
|
11
12
|
// modules already recognize across Claude/Codex/others.
|
|
12
13
|
const READ_TOOLS = new Set(['Read', 'read_file', 'view_file', 'cat_file', 'get_file']);
|
|
13
14
|
const WRITE_TOOLS = new Set(['Write', 'write_file', 'create_file']);
|
|
14
15
|
const EDIT_TOOLS = new Set(['Edit', 'edit_file', 'replace', 'patch', 'MultiEdit', 'apply_patch']);
|
|
16
|
+
/**
|
|
17
|
+
* Path-shaped noise that must never surface as a session "change": shell
|
|
18
|
+
* redirect tokens (`2>&1`), unexpanded env-var prefixes (`$WT/...`), dependency
|
|
19
|
+
* dirs (`node_modules`), and agents-cli's internal `.system` marker. Filtered at
|
|
20
|
+
* the source so every consumer (summary, picker, digest counts) agrees.
|
|
21
|
+
*/
|
|
22
|
+
export function isNoisePath(p) {
|
|
23
|
+
if (!p)
|
|
24
|
+
return true;
|
|
25
|
+
if (/^\d+>[&/]?/.test(p))
|
|
26
|
+
return true; // 2>&1, 1>&2, 2>/dev/null
|
|
27
|
+
if (p.includes('>&'))
|
|
28
|
+
return true;
|
|
29
|
+
if (p.startsWith('$') || p.startsWith('"$') || p.startsWith("'$"))
|
|
30
|
+
return true; // unexpanded $VAR
|
|
31
|
+
if (/(^|\/)node_modules(\/|$)/.test(p))
|
|
32
|
+
return true;
|
|
33
|
+
if (/(^|\/)\.system$/.test(p))
|
|
34
|
+
return true;
|
|
35
|
+
if (p.includes('/.agents/.history/'))
|
|
36
|
+
return true; // agents-cli internal archives
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
15
39
|
/** Extract file paths deleted by a shell command (rm / git rm / unlink). Conservative. */
|
|
16
40
|
export function extractDeletedPaths(command) {
|
|
17
41
|
const out = [];
|
|
@@ -25,7 +49,10 @@ export function extractDeletedPaths(command) {
|
|
|
25
49
|
continue; // flags (-r, -f, --force)
|
|
26
50
|
if (/[*?{}]/.test(tok))
|
|
27
51
|
continue; // globs — too imprecise to attribute
|
|
28
|
-
|
|
52
|
+
const p = tok.replace(/^['"]|['"]$/g, ''); // unquote
|
|
53
|
+
if (isNoisePath(p))
|
|
54
|
+
continue;
|
|
55
|
+
out.push(p);
|
|
29
56
|
}
|
|
30
57
|
}
|
|
31
58
|
return out;
|
|
@@ -57,6 +84,8 @@ export function classifyFileChanges(events) {
|
|
|
57
84
|
continue;
|
|
58
85
|
if (p.includes('.claude/plans/') && p.endsWith('.md'))
|
|
59
86
|
continue;
|
|
87
|
+
if (isNoisePath(p))
|
|
88
|
+
continue;
|
|
60
89
|
if (READ_TOOLS.has(tool)) {
|
|
61
90
|
readBefore.add(p);
|
|
62
91
|
}
|
|
@@ -99,6 +128,24 @@ export function toolHistogram(toolCounts, top = 8) {
|
|
|
99
128
|
.sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool))
|
|
100
129
|
.slice(0, top);
|
|
101
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Aggregate recognized external tools invoked via Bash across a session.
|
|
133
|
+
* Unknown one-off commands are bucketed as `other` so the ranking surface
|
|
134
|
+
* (e.g. `agents activity --tools`) only surfaces repeatable tools.
|
|
135
|
+
*/
|
|
136
|
+
export function bashToolCounts(events) {
|
|
137
|
+
const counts = {};
|
|
138
|
+
for (const e of events) {
|
|
139
|
+
if (e.type !== 'tool_use' || e._local)
|
|
140
|
+
continue;
|
|
141
|
+
if (e.tool !== 'Bash' || !e.command)
|
|
142
|
+
continue;
|
|
143
|
+
const info = classifyBashCommand(e.command);
|
|
144
|
+
const key = info.category === 'other' ? 'other' : bucketKey(e.command);
|
|
145
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
146
|
+
}
|
|
147
|
+
return counts;
|
|
148
|
+
}
|
|
102
149
|
/**
|
|
103
150
|
* Recognized test/build runners → the label we show.
|
|
104
151
|
*
|
|
@@ -674,5 +674,4 @@ export declare function readGrokMeta(filePath: string, currentVersion?: string):
|
|
|
674
674
|
meta: SessionMeta;
|
|
675
675
|
content: string;
|
|
676
676
|
} | null;
|
|
677
|
-
|
|
678
|
-
export declare function parseTimeFilter(input: string): number;
|
|
677
|
+
export { parseTimeFilter } from './relative-time.js';
|
|
@@ -16,6 +16,7 @@ import { promisify } from 'util';
|
|
|
16
16
|
import Database from '../sqlite.js';
|
|
17
17
|
import { getAgentsDir, getUserAgentsDir, getHistoryDir, getRunsDir } from '../state.js';
|
|
18
18
|
import { shortCodexHome } from '../codex-home.js';
|
|
19
|
+
import { parseTimeFilter } from './relative-time.js';
|
|
19
20
|
const execFileAsync = promisify(execFile);
|
|
20
21
|
import { AGENTS, agentConfigDirName, getCliVersion } from '../agents.js';
|
|
21
22
|
import { walkForFilesWithStat } from '../fs-walk.js';
|
|
@@ -4117,27 +4118,9 @@ export function readGrokMeta(filePath, currentVersion) {
|
|
|
4117
4118
|
};
|
|
4118
4119
|
return { meta, content: topic || '' };
|
|
4119
4120
|
}
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
const value = parseInt(relativeMatch[1], 10);
|
|
4127
|
-
const unit = relativeMatch[2].toLowerCase();
|
|
4128
|
-
if (unit === 'm')
|
|
4129
|
-
return Date.now() - value * 60_000;
|
|
4130
|
-
if (unit === 'h')
|
|
4131
|
-
return Date.now() - value * 3_600_000;
|
|
4132
|
-
if (unit === 'd')
|
|
4133
|
-
return Date.now() - value * 86_400_000;
|
|
4134
|
-
if (unit === 'w')
|
|
4135
|
-
return Date.now() - value * 7 * 86_400_000;
|
|
4136
|
-
if (unit === 'mo')
|
|
4137
|
-
return Date.now() - value * 30 * 86_400_000;
|
|
4138
|
-
if (unit === 'y')
|
|
4139
|
-
return Date.now() - value * 365 * 86_400_000;
|
|
4140
|
-
}
|
|
4141
|
-
const ts = new Date(input).getTime();
|
|
4142
|
-
return Number.isNaN(ts) ? 0 : ts;
|
|
4143
|
-
}
|
|
4121
|
+
// parseTimeFilter moved to ./relative-time.js — a leaf module — so a caller that
|
|
4122
|
+
// only needs the duration parser does not pull this file's `../sqlite.js` import
|
|
4123
|
+
// (and Node's SQLite ExperimentalWarning on stderr) into its module graph.
|
|
4124
|
+
// Re-exported so every existing importer keeps working unchanged. Imported too,
|
|
4125
|
+
// because a bare re-export does not bind the name for this file's own callers.
|
|
4126
|
+
export { parseTimeFilter } from './relative-time.js';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session highlight extractors.
|
|
3
|
+
*
|
|
4
|
+
* Pure derivations over a session's `SessionEvent[]` that power the "what did
|
|
5
|
+
* this session use and produce" sections of both renders of a session — the
|
|
6
|
+
* picker quick preview (`sessions-picker.ts`) and the full summary
|
|
7
|
+
* (`render.ts`). One module, two consumers, so the panes never drift.
|
|
8
|
+
*
|
|
9
|
+
* Skills/hooks/links are no-I/O. `extractRepos` touches the filesystem (a
|
|
10
|
+
* bounded `.git` walk over a handful of candidate dirs) and is the only
|
|
11
|
+
* non-pure function here.
|
|
12
|
+
*/
|
|
13
|
+
import type { SessionEvent } from './types.js';
|
|
14
|
+
import { type FileChange } from './digest.js';
|
|
15
|
+
export interface SkillUse {
|
|
16
|
+
name: string;
|
|
17
|
+
count: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Skills invoked during the session, from `Skill` tool calls. Claude and Kimi
|
|
21
|
+
* both name the tool `Skill` and carry the skill id in `args.skill` (plugin
|
|
22
|
+
* skills surface here too — a plugin-provided skill is invoked through the
|
|
23
|
+
* same tool). Sorted by count desc, then name.
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractSkills(events: SessionEvent[]): SkillUse[];
|
|
26
|
+
export interface HookUse {
|
|
27
|
+
/** Hook name as configured, e.g. `SessionStart:startup`. */
|
|
28
|
+
name: string;
|
|
29
|
+
/** Lifecycle event, e.g. `SessionStart`. */
|
|
30
|
+
event?: string;
|
|
31
|
+
/** How many times it fired. */
|
|
32
|
+
count: number;
|
|
33
|
+
/** How many firings failed (non-`hook_success` records). */
|
|
34
|
+
failed: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Hooks that fired during the session, folded from `hook` events (parsed from
|
|
38
|
+
* Claude's `hook_success`/`hook_error`/… attachment records; other harnesses
|
|
39
|
+
* don't record firings in their transcripts, so they yield an empty list).
|
|
40
|
+
* Sorted by count desc, then name.
|
|
41
|
+
*/
|
|
42
|
+
export declare function extractHooks(events: SessionEvent[]): HookUse[];
|
|
43
|
+
export type LinkKind = 'linear' | 'jira' | 'github' | 'gitlab' | 'other';
|
|
44
|
+
export interface SessionLink {
|
|
45
|
+
kind: LinkKind;
|
|
46
|
+
url: string;
|
|
47
|
+
/** Short display label: `RUSH-2076`, `PR#1755`, `owner/repo#123`, host. */
|
|
48
|
+
label: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Links mentioned in user/assistant messages, classified (Linear/Jira/GitHub/
|
|
52
|
+
* GitLab), deduped by URL AND by label (a session that quotes the same PR in
|
|
53
|
+
* five messages shows it once), first-seen order. Capped so a link-heavy
|
|
54
|
+
* session can't flood the pane.
|
|
55
|
+
*/
|
|
56
|
+
export declare function extractLinks(events: SessionEvent[]): SessionLink[];
|
|
57
|
+
export type ArtifactBucket = 'artifacts' | 'plans' | 'reports' | 'docs';
|
|
58
|
+
export interface ProducedArtifact {
|
|
59
|
+
/** Absolute (or session-relative) path of the created file. */
|
|
60
|
+
path: string;
|
|
61
|
+
basename: string;
|
|
62
|
+
bucket: ArtifactBucket;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Documents/files the session CREATED, from the already-classified changes
|
|
66
|
+
* (callers classify once — this never re-derives). Keeps the ones a human
|
|
67
|
+
* browses later: anything under `.agents/artifacts|plans|reports/`, plus other
|
|
68
|
+
* `*.md`/`*.html` creations. Source/config churn (the bulk of `+N`) stays in
|
|
69
|
+
* the Changes line.
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractArtifacts(changes: FileChange[]): ProducedArtifact[];
|
|
72
|
+
/**
|
|
73
|
+
* Repos the session worked in, from the directories its file paths live under
|
|
74
|
+
* (a bounded `.git` walk-up; a `.git` FILE counts too — that's the worktree
|
|
75
|
+
* layout). Names are repo dir basenames, first-seen order, capped.
|
|
76
|
+
*
|
|
77
|
+
* Relative paths resolve against the SESSION's cwd only — when it is unknown
|
|
78
|
+
* (e.g. kimi rows carry no cwd today) they are skipped: resolving them against
|
|
79
|
+
* the viewer's process cwd attributes the session to whatever repo the CLI
|
|
80
|
+
* happens to run in, which is a wrong answer, not a degraded one.
|
|
81
|
+
*/
|
|
82
|
+
export declare function extractRepos(events: SessionEvent[], cwd?: string): string[];
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session highlight extractors.
|
|
3
|
+
*
|
|
4
|
+
* Pure derivations over a session's `SessionEvent[]` that power the "what did
|
|
5
|
+
* this session use and produce" sections of both renders of a session — the
|
|
6
|
+
* picker quick preview (`sessions-picker.ts`) and the full summary
|
|
7
|
+
* (`render.ts`). One module, two consumers, so the panes never drift.
|
|
8
|
+
*
|
|
9
|
+
* Skills/hooks/links are no-I/O. `extractRepos` touches the filesystem (a
|
|
10
|
+
* bounded `.git` walk over a handful of candidate dirs) and is the only
|
|
11
|
+
* non-pure function here.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { isNoisePath } from './digest.js';
|
|
17
|
+
/**
|
|
18
|
+
* Skills invoked during the session, from `Skill` tool calls. Claude and Kimi
|
|
19
|
+
* both name the tool `Skill` and carry the skill id in `args.skill` (plugin
|
|
20
|
+
* skills surface here too — a plugin-provided skill is invoked through the
|
|
21
|
+
* same tool). Sorted by count desc, then name.
|
|
22
|
+
*/
|
|
23
|
+
export function extractSkills(events) {
|
|
24
|
+
const counts = new Map();
|
|
25
|
+
for (const e of events) {
|
|
26
|
+
if (e.type !== 'tool_use' || e._local)
|
|
27
|
+
continue;
|
|
28
|
+
if (e.tool !== 'Skill')
|
|
29
|
+
continue;
|
|
30
|
+
const name = e.args?.skill ?? e.args?.name;
|
|
31
|
+
if (typeof name !== 'string' || !name.trim())
|
|
32
|
+
continue;
|
|
33
|
+
const key = name.trim();
|
|
34
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
return [...counts.entries()]
|
|
37
|
+
.map(([name, count]) => ({ name, count }))
|
|
38
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Hooks that fired during the session, folded from `hook` events (parsed from
|
|
42
|
+
* Claude's `hook_success`/`hook_error`/… attachment records; other harnesses
|
|
43
|
+
* don't record firings in their transcripts, so they yield an empty list).
|
|
44
|
+
* Sorted by count desc, then name.
|
|
45
|
+
*/
|
|
46
|
+
export function extractHooks(events) {
|
|
47
|
+
const byName = new Map();
|
|
48
|
+
for (const e of events) {
|
|
49
|
+
if (e.type !== 'hook')
|
|
50
|
+
continue;
|
|
51
|
+
const name = e.hookName?.trim() || e.hookEvent?.trim() || 'hook';
|
|
52
|
+
const existing = byName.get(name);
|
|
53
|
+
if (existing) {
|
|
54
|
+
existing.count++;
|
|
55
|
+
if (e.success === false)
|
|
56
|
+
existing.failed++;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
byName.set(name, {
|
|
60
|
+
name,
|
|
61
|
+
event: e.hookEvent,
|
|
62
|
+
count: 1,
|
|
63
|
+
failed: e.success === false ? 1 : 0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
68
|
+
}
|
|
69
|
+
/** Bare URL scan over message text; trailing punctuation stripped. Backticks
|
|
70
|
+
* and ellipses excluded so markdown-wrapped or truncated URLs don't leak in. */
|
|
71
|
+
const URL_RE = /https?:\/\/[^\s"'`()<>\]\\…]+/g;
|
|
72
|
+
/** A routable host: dotted domain (optionally :port). localhost/IPs-of-one-segment
|
|
73
|
+
* and markdown garbage (`…`) are not Links-section material. */
|
|
74
|
+
const HOST_RE = /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}(:\d+)?$/i;
|
|
75
|
+
function classifyLink(url) {
|
|
76
|
+
let m;
|
|
77
|
+
// Linear: https://linear.app/<workspace>/issue/RUSH-2076/slug
|
|
78
|
+
if ((m = url.match(/https?:\/\/linear\.app\/[\w-]+\/issue\/([A-Z]{2,6}-\d+)/))) {
|
|
79
|
+
return { kind: 'linear', url, label: m[1] };
|
|
80
|
+
}
|
|
81
|
+
// Jira: https://<host>.atlassian.net/browse/PROJ-123 (or /jira/browse/)
|
|
82
|
+
if ((m = url.match(/https?:\/\/[\w.-]*(?:atlassian\.net|jira[\w.-]*)\/browse\/([A-Z]{2,10}-\d+)/))) {
|
|
83
|
+
return { kind: 'jira', url, label: m[1] };
|
|
84
|
+
}
|
|
85
|
+
// GitHub: PR / issue / repo
|
|
86
|
+
if ((m = url.match(/https?:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/pull\/(\d+)/))) {
|
|
87
|
+
return { kind: 'github', url, label: `PR#${m[2]}` };
|
|
88
|
+
}
|
|
89
|
+
if ((m = url.match(/https?:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)/))) {
|
|
90
|
+
return { kind: 'github', url, label: `${m[1]}#${m[2]}` };
|
|
91
|
+
}
|
|
92
|
+
if ((m = url.match(/https?:\/\/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/))) {
|
|
93
|
+
return { kind: 'github', url, label: m[1] };
|
|
94
|
+
}
|
|
95
|
+
// GitLab: MR / issue
|
|
96
|
+
if ((m = url.match(/https?:\/\/(gitlab\.com|[\w.-]*gitlab[\w.-]*)\/(.+?)\/-\/merge_requests\/(\d+)/))) {
|
|
97
|
+
return { kind: 'gitlab', url, label: `${m[2]}!${m[3]}` };
|
|
98
|
+
}
|
|
99
|
+
if ((m = url.match(/https?:\/\/(gitlab\.com|[\w.-]*gitlab[\w.-]*)\/(.+?)\/-\/issues\/(\d+)/))) {
|
|
100
|
+
return { kind: 'gitlab', url, label: `${m[2]}#${m[3]}` };
|
|
101
|
+
}
|
|
102
|
+
const host = url.match(/^https?:\/\/([^/?]+)/)?.[1];
|
|
103
|
+
if (!host || !HOST_RE.test(host))
|
|
104
|
+
return undefined;
|
|
105
|
+
return { kind: 'other', url, label: host };
|
|
106
|
+
}
|
|
107
|
+
const MAX_LINKS = 12;
|
|
108
|
+
/**
|
|
109
|
+
* Links mentioned in user/assistant messages, classified (Linear/Jira/GitHub/
|
|
110
|
+
* GitLab), deduped by URL AND by label (a session that quotes the same PR in
|
|
111
|
+
* five messages shows it once), first-seen order. Capped so a link-heavy
|
|
112
|
+
* session can't flood the pane.
|
|
113
|
+
*/
|
|
114
|
+
export function extractLinks(events) {
|
|
115
|
+
const seenUrl = new Set();
|
|
116
|
+
const seenLabel = new Set();
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const e of events) {
|
|
119
|
+
if (e.type !== 'message' || !e.content)
|
|
120
|
+
continue;
|
|
121
|
+
// Harness-injected scaffolding (bash wrappers, system reminders) carries
|
|
122
|
+
// URLs that aren't conversation references — same exclusion the rest of
|
|
123
|
+
// the pipeline applies.
|
|
124
|
+
if (e._synthetic)
|
|
125
|
+
continue;
|
|
126
|
+
for (const raw of e.content.match(URL_RE) ?? []) {
|
|
127
|
+
const url = raw.replace(/[.,;:)\]`]+$/, '');
|
|
128
|
+
if (seenUrl.has(url))
|
|
129
|
+
continue;
|
|
130
|
+
seenUrl.add(url);
|
|
131
|
+
const link = classifyLink(url);
|
|
132
|
+
if (!link || seenLabel.has(link.label))
|
|
133
|
+
continue;
|
|
134
|
+
seenLabel.add(link.label);
|
|
135
|
+
out.push(link);
|
|
136
|
+
if (out.length >= MAX_LINKS)
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
const ARTIFACT_EXT_RE = /\.(md|markdown|html?)$/i;
|
|
143
|
+
const MAX_ARTIFACTS = 12;
|
|
144
|
+
/**
|
|
145
|
+
* Documents/files the session CREATED, from the already-classified changes
|
|
146
|
+
* (callers classify once — this never re-derives). Keeps the ones a human
|
|
147
|
+
* browses later: anything under `.agents/artifacts|plans|reports/`, plus other
|
|
148
|
+
* `*.md`/`*.html` creations. Source/config churn (the bulk of `+N`) stays in
|
|
149
|
+
* the Changes line.
|
|
150
|
+
*/
|
|
151
|
+
export function extractArtifacts(changes) {
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const ch of changes) {
|
|
154
|
+
if (ch.op !== 'created')
|
|
155
|
+
continue;
|
|
156
|
+
const p = ch.path;
|
|
157
|
+
if (isNoisePath(p))
|
|
158
|
+
continue;
|
|
159
|
+
const norm = p.replace(/\\/g, '/');
|
|
160
|
+
let bucket;
|
|
161
|
+
if (/\/\.agents\/artifacts\//.test(norm))
|
|
162
|
+
bucket = 'artifacts';
|
|
163
|
+
else if (/\/\.agents\/plans\//.test(norm) || /\/plans\/[^/]+\.md$/i.test(norm))
|
|
164
|
+
bucket = 'plans';
|
|
165
|
+
else if (/\/\.agents\/reports\//.test(norm))
|
|
166
|
+
bucket = 'reports';
|
|
167
|
+
else if (ARTIFACT_EXT_RE.test(norm))
|
|
168
|
+
bucket = 'docs';
|
|
169
|
+
if (!bucket)
|
|
170
|
+
continue;
|
|
171
|
+
out.push({ path: p, basename: path.posix.basename(norm), bucket });
|
|
172
|
+
if (out.length >= MAX_ARTIFACTS)
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
// ── Repos ─────────────────────────────────────────────────────────────────────
|
|
178
|
+
/** Candidate dirs to probe for a `.git` root, and the walk depth per dir. */
|
|
179
|
+
const MAX_REPO_PROBES = 12;
|
|
180
|
+
const REPO_WALK_DEPTH = 6;
|
|
181
|
+
/** A walk-up that lands here overshot the workspace — never a "repo worked in". */
|
|
182
|
+
function isOvershotRoot(dir) {
|
|
183
|
+
return dir === '/' || dir === os.tmpdir() || dir === os.homedir();
|
|
184
|
+
}
|
|
185
|
+
function repoRootFrom(dir) {
|
|
186
|
+
let cur = dir;
|
|
187
|
+
for (let i = 0; i < REPO_WALK_DEPTH; i++) {
|
|
188
|
+
try {
|
|
189
|
+
if (fs.existsSync(path.join(cur, '.git'))) {
|
|
190
|
+
return isOvershotRoot(cur) ? undefined : cur;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const parent = path.dirname(cur);
|
|
197
|
+
if (parent === cur)
|
|
198
|
+
return undefined;
|
|
199
|
+
cur = parent;
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Repos the session worked in, from the directories its file paths live under
|
|
205
|
+
* (a bounded `.git` walk-up; a `.git` FILE counts too — that's the worktree
|
|
206
|
+
* layout). Names are repo dir basenames, first-seen order, capped.
|
|
207
|
+
*
|
|
208
|
+
* Relative paths resolve against the SESSION's cwd only — when it is unknown
|
|
209
|
+
* (e.g. kimi rows carry no cwd today) they are skipped: resolving them against
|
|
210
|
+
* the viewer's process cwd attributes the session to whatever repo the CLI
|
|
211
|
+
* happens to run in, which is a wrong answer, not a degraded one.
|
|
212
|
+
*/
|
|
213
|
+
export function extractRepos(events, cwd) {
|
|
214
|
+
const candidates = [];
|
|
215
|
+
const seenCand = new Set();
|
|
216
|
+
const addCandidate = (p) => {
|
|
217
|
+
if (!p || isNoisePath(p))
|
|
218
|
+
return;
|
|
219
|
+
// DotAgents internals (the `.system` registry repo, run archives) are
|
|
220
|
+
// infrastructure, not "repos the user works in".
|
|
221
|
+
if (p.includes('/.agents/.system/') || p.includes('/.agents/.history/'))
|
|
222
|
+
return;
|
|
223
|
+
if (!path.isAbsolute(p) && !cwd)
|
|
224
|
+
return;
|
|
225
|
+
const abs = path.isAbsolute(p) ? p : path.resolve(cwd, p);
|
|
226
|
+
const dir = path.dirname(abs);
|
|
227
|
+
if (seenCand.has(dir))
|
|
228
|
+
return;
|
|
229
|
+
seenCand.add(dir);
|
|
230
|
+
candidates.push(dir);
|
|
231
|
+
};
|
|
232
|
+
for (const e of events) {
|
|
233
|
+
if (e.type !== 'tool_use' || e._local)
|
|
234
|
+
continue;
|
|
235
|
+
const p = e.path || e.args?.file_path || e.args?.path || '';
|
|
236
|
+
if (typeof p === 'string' && p)
|
|
237
|
+
addCandidate(p);
|
|
238
|
+
if (candidates.length >= MAX_REPO_PROBES)
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
const repos = [];
|
|
242
|
+
const seenRoot = new Set();
|
|
243
|
+
for (const dir of candidates) {
|
|
244
|
+
const root = repoRootFrom(dir);
|
|
245
|
+
if (!root || seenRoot.has(root))
|
|
246
|
+
continue;
|
|
247
|
+
seenRoot.add(root);
|
|
248
|
+
repos.push(path.basename(root));
|
|
249
|
+
}
|
|
250
|
+
return repos;
|
|
251
|
+
}
|
|
@@ -66,6 +66,10 @@ function sanitizeEvent(e) {
|
|
|
66
66
|
e.model = sanitizeForTerminal(e.model);
|
|
67
67
|
if (e.mediaType)
|
|
68
68
|
e.mediaType = sanitizeForTerminal(e.mediaType);
|
|
69
|
+
if (e.hookName)
|
|
70
|
+
e.hookName = sanitizeForTerminal(e.hookName);
|
|
71
|
+
if (e.hookEvent)
|
|
72
|
+
e.hookEvent = sanitizeForTerminal(e.hookEvent);
|
|
69
73
|
if (e.args)
|
|
70
74
|
e.args = sanitizeArgsDeep(e.args);
|
|
71
75
|
}
|
|
@@ -484,7 +488,25 @@ export function parseClaudeContent(content) {
|
|
|
484
488
|
content: raw.subtype || 'success',
|
|
485
489
|
});
|
|
486
490
|
}
|
|
487
|
-
|
|
491
|
+
else if (type === 'attachment') {
|
|
492
|
+
// Hook firings are recorded as attachments: `hook_success` / `hook_error` /
|
|
493
|
+
// `hook_blocked` per firing, plus a derivative `hook_additional_context`
|
|
494
|
+
// record for the SAME firing (shared toolUseID) — skip the derivative or
|
|
495
|
+
// every firing counts twice.
|
|
496
|
+
const att = raw.attachment;
|
|
497
|
+
const attType = att?.type;
|
|
498
|
+
if (typeof attType === 'string' && attType.startsWith('hook_') && attType !== 'hook_additional_context') {
|
|
499
|
+
events.push({
|
|
500
|
+
type: 'hook',
|
|
501
|
+
agent: 'claude',
|
|
502
|
+
timestamp,
|
|
503
|
+
hookName: typeof att.hookName === 'string' ? att.hookName : undefined,
|
|
504
|
+
hookEvent: typeof att.hookEvent === 'string' ? att.hookEvent : undefined,
|
|
505
|
+
success: attType === 'hook_success',
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Skip: permission-mode, non-hook attachments, and other line types
|
|
488
510
|
}
|
|
489
511
|
return events;
|
|
490
512
|
}
|
|
@@ -5,3 +5,40 @@
|
|
|
5
5
|
* `remote.ts`, so a back-import would cycle.
|
|
6
6
|
*/
|
|
7
7
|
export declare function formatRelativeTime(isoTimestamp: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Compact age — `now`, `45m`, `3h`, `2d`, then the calendar date. This is the
|
|
10
|
+
* *creation* half of a session's time cell: it renders right next to the long
|
|
11
|
+
* last-activity label, and one row can afford one long form, not two.
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatCompactAge(isoTimestamp: string): string;
|
|
14
|
+
/** The two time fields a session listing shows for one row. */
|
|
15
|
+
export interface SessionAgeParts {
|
|
16
|
+
/**
|
|
17
|
+
* Compact age of the session's creation, set only when the session ran long
|
|
18
|
+
* enough (>= a minute) for it to say something `last` does not.
|
|
19
|
+
*/
|
|
20
|
+
created?: string;
|
|
21
|
+
/** Last-activity label — the long relative form the listing sorts by. */
|
|
22
|
+
last: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Split a session's timestamps into the two fields a listing renders: when it
|
|
26
|
+
* was created and when it was last active. One last-activity label answers
|
|
27
|
+
* neither "is this the old session I'm looking for" nor "how long did it run" —
|
|
28
|
+
* a session touched an hour ago may have started last week.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sessionAgeParts(createdIso: string, lastActivityIso?: string): SessionAgeParts;
|
|
31
|
+
/**
|
|
32
|
+
* Parse a time filter string (relative like '7d' or an ISO timestamp) into epoch
|
|
33
|
+
* milliseconds. Backs `--since` on `sessions`, `cost`, `teams`, and `output`, and
|
|
34
|
+
* `--unused` on `secrets list`.
|
|
35
|
+
*
|
|
36
|
+
* It lives here, in a module with no imports, rather than in `discover.ts` where
|
|
37
|
+
* it started: `discover.ts` loads `../sqlite.js`, so importing this one function
|
|
38
|
+
* from there pulls `node:sqlite` into the caller's module graph and Node prints
|
|
39
|
+
* `ExperimentalWarning: SQLite …` on stderr. That is invisible in a command that
|
|
40
|
+
* already touches the session DB, and a broken contract in one that doesn't —
|
|
41
|
+
* `monitors --json` asserts a clean stderr. `discover.ts` re-exports it, so
|
|
42
|
+
* existing importers are unaffected.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseTimeFilter(input: string): number;
|