@phnx-labs/agents-cli 1.22.5 → 1.22.7
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 +90 -0
- package/README.md +7 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +46 -5
- package/dist/commands/feed.d.ts +2 -1
- package/dist/commands/feed.js +47 -20
- package/dist/commands/focus.js +22 -1
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/projects.js +122 -107
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/sessions-backfill.d.ts +33 -0
- package/dist/commands/sessions-backfill.js +83 -1
- package/dist/commands/sessions-stats.d.ts +36 -0
- package/dist/commands/sessions-stats.js +263 -0
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +21 -1
- package/dist/commands/view.d.ts +2 -1
- package/dist/commands/view.js +62 -11
- package/dist/index.js +3 -3
- package/dist/lib/activity.js +2 -2
- package/dist/lib/agents.js +68 -11
- package/dist/lib/analytics/recipes.js +11 -5
- package/dist/lib/browser/profiles.d.ts +15 -7
- package/dist/lib/browser/profiles.js +53 -12
- package/dist/lib/byok-usage.d.ts +38 -0
- package/dist/lib/byok-usage.js +117 -0
- package/dist/lib/capabilities.js +1 -1
- package/dist/lib/exec.d.ts +20 -0
- package/dist/lib/exec.js +73 -8
- package/dist/lib/feed-outcome.d.ts +1 -0
- package/dist/lib/feed-outcome.js +2 -0
- package/dist/lib/feed-post.js +1 -1
- package/dist/lib/feed-ranking.d.ts +1 -0
- package/dist/lib/feed-ranking.js +4 -0
- package/dist/lib/feed.d.ts +4 -1
- package/dist/lib/feed.js +25 -0
- package/dist/lib/hosts/passthrough.js +0 -1
- package/dist/lib/mcp.js +6 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.js +4 -1
- package/dist/lib/models.js +63 -0
- package/dist/lib/profiles.d.ts +42 -1
- package/dist/lib/profiles.js +50 -2
- package/dist/lib/project-focus.d.ts +9 -0
- package/dist/lib/project-focus.js +23 -0
- package/dist/lib/project-key.d.ts +1 -1
- package/dist/lib/project-key.js +1 -1
- package/dist/lib/project-probe.d.ts +18 -0
- package/dist/lib/project-probe.js +46 -0
- package/dist/lib/project-status.d.ts +56 -0
- package/dist/lib/project-status.js +125 -18
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources/types.d.ts +1 -1
- 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/audit.js +1 -1
- package/dist/lib/secrets/index.d.ts +1 -0
- package/dist/lib/secrets/index.js +29 -15
- package/dist/lib/secrets/remote.d.ts +1 -1
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/session/active.d.ts +2 -0
- package/dist/lib/session/bash-command.d.ts +2 -3
- package/dist/lib/session/db.d.ts +92 -1
- package/dist/lib/session/db.js +230 -1
- package/dist/lib/session/digest.d.ts +1 -1
- package/dist/lib/session/digest.js +1 -1
- package/dist/lib/share/publish.js +24 -0
- package/dist/lib/startup/command-registry.d.ts +0 -1
- package/dist/lib/startup/command-registry.js +0 -2
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/types.d.ts +11 -2
- package/dist/lib/usage.d.ts +5 -0
- package/dist/lib/usage.js +3 -3
- package/package.json +1 -1
- package/dist/commands/activity.d.ts +0 -87
- package/dist/commands/activity.js +0 -346
|
@@ -78,3 +78,26 @@ export async function readFocusAreas(root, windowDays) {
|
|
|
78
78
|
return [];
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/** Compact count: 2329 → "2.3k", under 1000 stays exact. */
|
|
82
|
+
export function formatFocusCount(n) {
|
|
83
|
+
if (!Number.isFinite(n) || n < 0)
|
|
84
|
+
return '0';
|
|
85
|
+
if (n < 1000)
|
|
86
|
+
return String(Math.round(n));
|
|
87
|
+
const k = n / 1000;
|
|
88
|
+
const s = k >= 10 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '');
|
|
89
|
+
return `${s}k`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One scannable focus line: path + count, with a single unit trailer so the
|
|
93
|
+
* bare integer is never mistaken for commits or minutes.
|
|
94
|
+
*
|
|
95
|
+
* apps/cli/src 2.3k · apps/cli/docs 302 · apps/factory/src 245 file-touches (7d)
|
|
96
|
+
*/
|
|
97
|
+
export function formatFocusAreas(areas, windowDays) {
|
|
98
|
+
if (areas.length === 0)
|
|
99
|
+
return '';
|
|
100
|
+
const body = areas.map((a) => `${a.path} ${formatFocusCount(a.touches)}`).join(' · ');
|
|
101
|
+
const unit = `file-touches (${windowDays}d)`;
|
|
102
|
+
return `${body} ${unit}`;
|
|
103
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The one worktree-aware cwd -> project fold.
|
|
3
3
|
*
|
|
4
4
|
* Several surfaces bucket work "by project": the `agents sessions` overview,
|
|
5
|
-
* the `agents
|
|
5
|
+
* the `agents feed` timeline, and anything else that has a cwd and needs a
|
|
6
6
|
* stable repo-level key. They must agree, or the same session shows up under
|
|
7
7
|
* `agents-cli` in one view and `my-branch-slug` in another — so the rule lives
|
|
8
8
|
* here and every caller delegates.
|
package/dist/lib/project-key.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The one worktree-aware cwd -> project fold.
|
|
3
3
|
*
|
|
4
4
|
* Several surfaces bucket work "by project": the `agents sessions` overview,
|
|
5
|
-
* the `agents
|
|
5
|
+
* the `agents feed` timeline, and anything else that has a cwd and needs a
|
|
6
6
|
* stable repo-level key. They must agree, or the same session shows up under
|
|
7
7
|
* `agents-cli` in one view and `my-branch-slug` in another — so the rule lives
|
|
8
8
|
* here and every caller delegates.
|
|
@@ -73,3 +73,21 @@ export declare function formatWorkspaceLine(s: RepoWorkspaceStatus): string;
|
|
|
73
73
|
* `fleet` row label.
|
|
74
74
|
*/
|
|
75
75
|
export declare function formatFleetWorkspaces(statuses: HostWorkspaceStatus[]): string[];
|
|
76
|
+
/** Severity for a workspace/repo warning on the project card. */
|
|
77
|
+
export type WorkspaceWarningSeverity = 'critical' | 'continue';
|
|
78
|
+
export interface WorkspaceWarning {
|
|
79
|
+
severity: WorkspaceWarningSeverity;
|
|
80
|
+
text: string;
|
|
81
|
+
remediation?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Turn probed workspace rows into card-footer warnings.
|
|
85
|
+
*
|
|
86
|
+
* - missing / unreadable git → critical (agents there cannot share a tree)
|
|
87
|
+
* - behind upstream → critical when ≥10 commits, continue otherwise
|
|
88
|
+
* - dirty tree → continue (local work is fine; just note it)
|
|
89
|
+
* - ahead-only is not a warning (that is progress waiting to push)
|
|
90
|
+
*
|
|
91
|
+
* Pure. Caller decides whether the rows came from `--fleet` or a local probe.
|
|
92
|
+
*/
|
|
93
|
+
export declare function workspaceWarnings(statuses: HostWorkspaceStatus[]): WorkspaceWarning[];
|
|
@@ -158,3 +158,49 @@ export function formatFleetWorkspaces(statuses) {
|
|
|
158
158
|
return multi ? `${chalk.dim(`${p} · `)}${body}` : body;
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Turn probed workspace rows into card-footer warnings.
|
|
163
|
+
*
|
|
164
|
+
* - missing / unreadable git → critical (agents there cannot share a tree)
|
|
165
|
+
* - behind upstream → critical when ≥10 commits, continue otherwise
|
|
166
|
+
* - dirty tree → continue (local work is fine; just note it)
|
|
167
|
+
* - ahead-only is not a warning (that is progress waiting to push)
|
|
168
|
+
*
|
|
169
|
+
* Pure. Caller decides whether the rows came from `--fleet` or a local probe.
|
|
170
|
+
*/
|
|
171
|
+
export function workspaceWarnings(statuses) {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const s of [...statuses].sort((a, b) => a.host.localeCompare(b.host) || a.path.localeCompare(b.path))) {
|
|
174
|
+
const where = s.host ? `${s.host}` : 'local';
|
|
175
|
+
const pathBit = s.path ? ` (${s.path})` : '';
|
|
176
|
+
if (!s.present) {
|
|
177
|
+
out.push({
|
|
178
|
+
severity: 'critical',
|
|
179
|
+
text: `${where}: checkout missing${pathBit}`,
|
|
180
|
+
remediation: 'clone or sync the project root on that host before landing agents there',
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (s.error) {
|
|
185
|
+
out.push({
|
|
186
|
+
severity: 'critical',
|
|
187
|
+
text: `${where}: ${s.error}${pathBit}`,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (s.behind !== undefined && s.behind > 0) {
|
|
192
|
+
out.push({
|
|
193
|
+
severity: s.behind >= 10 ? 'critical' : 'continue',
|
|
194
|
+
text: `${where} is ${s.behind} commit${s.behind === 1 ? '' : 's'} behind ${s.upstream ?? 'upstream'}${pathBit}`,
|
|
195
|
+
remediation: 'pull (or rebase) before agents on this host open PRs against a stale base',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (s.dirty !== undefined && s.dirty > 0) {
|
|
199
|
+
out.push({
|
|
200
|
+
severity: 'continue',
|
|
201
|
+
text: `${where} has ${s.dirty} uncommitted change${s.dirty === 1 ? '' : 's'}${pathBit}`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
@@ -53,6 +53,13 @@ export interface ProjectSessionRollup {
|
|
|
53
53
|
* containing only projects with at least one matched session — callers merge
|
|
54
54
|
* with the full definition list to show zero-agent projects.
|
|
55
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* Ensure every session carries a host for the card roster. Local
|
|
58
|
+
* `getActiveSessions()` omits `machine`; remotes already set it. Pure.
|
|
59
|
+
*/
|
|
60
|
+
export declare function withDefaultMachine<T extends {
|
|
61
|
+
machine?: string;
|
|
62
|
+
}>(sessions: T[], defaultHost: string): T[];
|
|
56
63
|
export declare function rollupSessionsByProject(defs: ProjectDef[], sessions: ActiveSession[]): Map<string, ProjectSessionRollup>;
|
|
57
64
|
/**
|
|
58
65
|
* True when a session's status means it is over. Exported so the card can keep
|
|
@@ -84,6 +91,19 @@ export declare function liveDeadSplit(byStatus: Partial<Record<ActiveStatus, num
|
|
|
84
91
|
export declare function sortProjectMembers(members: ProjectMember[]): ProjectMember[];
|
|
85
92
|
/** Cap for the members line before it collapses to `+N more`. */
|
|
86
93
|
export declare const MEMBERS_LINE_LIMIT = 6;
|
|
94
|
+
/** Cap for host groups on the multi-line agents roster. */
|
|
95
|
+
export declare const MEMBERS_HOST_LIMIT = 8;
|
|
96
|
+
/**
|
|
97
|
+
* Collapse members into distinct state cells (`agent · status · ticket[@host]`),
|
|
98
|
+
* counting duplicates as `×N`. Pure.
|
|
99
|
+
*/
|
|
100
|
+
export declare function collapseMemberCells(members: ProjectMember[], opts?: {
|
|
101
|
+
includeHostOnCell?: boolean;
|
|
102
|
+
}): Array<{
|
|
103
|
+
cell: string;
|
|
104
|
+
n: number;
|
|
105
|
+
members: number;
|
|
106
|
+
}>;
|
|
87
107
|
/**
|
|
88
108
|
* The `agents` line under `live`: one cell per DISTINCT member state —
|
|
89
109
|
* `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
|
|
@@ -91,8 +111,44 @@ export declare const MEMBERS_LINE_LIMIT = 6;
|
|
|
91
111
|
* truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
|
|
92
112
|
* `+N more` tail counting members, not cells. Pure (chalk styling only); the
|
|
93
113
|
* caller adds the label.
|
|
114
|
+
*
|
|
115
|
+
* Prefer {@link formatProjectMembersByHost} on the card: a flat line hides which
|
|
116
|
+
* machine is running the work when the same harness×status spans hosts.
|
|
94
117
|
*/
|
|
95
118
|
export declare function formatProjectMembers(members: ProjectMember[], limit?: number): string;
|
|
119
|
+
/**
|
|
120
|
+
* Host-grouped agents roster. One content line per host:
|
|
121
|
+
* `@zion claude · running ×9 · claude · idle ×4`
|
|
122
|
+
*
|
|
123
|
+
* When no member carries a host (local-only rollup with no machine stamp), falls
|
|
124
|
+
* back to a single flat line via {@link formatProjectMembers}. Pure.
|
|
125
|
+
*/
|
|
126
|
+
export declare function formatProjectMembersByHost(members: ProjectMember[], opts?: {
|
|
127
|
+
cellLimit?: number;
|
|
128
|
+
hostLimit?: number;
|
|
129
|
+
}): string[];
|
|
130
|
+
/** A card-level warning collected for the footer. */
|
|
131
|
+
export type ProjectWarningSeverity = 'critical' | 'continue';
|
|
132
|
+
export interface ProjectWarning {
|
|
133
|
+
severity: ProjectWarningSeverity;
|
|
134
|
+
/** One human line. */
|
|
135
|
+
text: string;
|
|
136
|
+
/** Optional fix or next step. */
|
|
137
|
+
remediation?: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Severity markers for the warnings footer. User-facing by design: critical
|
|
141
|
+
* stops you (wrong repo, missing checkout, large drift); continue is a soft
|
|
142
|
+
* nudge (dirty tree, schedule not measurable).
|
|
143
|
+
*/
|
|
144
|
+
export declare function warningEmoji(severity: ProjectWarningSeverity): string;
|
|
145
|
+
/** Stable sort: critical first, then continue; stable within a tier. */
|
|
146
|
+
export declare function sortProjectWarnings(warnings: ProjectWarning[]): ProjectWarning[];
|
|
147
|
+
/**
|
|
148
|
+
* Format one or more warning lines for the card footer. Pure (chalk only).
|
|
149
|
+
* Returns empty when there is nothing to say.
|
|
150
|
+
*/
|
|
151
|
+
export declare function formatProjectWarnings(warnings: ProjectWarning[]): string[];
|
|
96
152
|
/** Harvested signals not on the session list: repo-global merged PRs + releases, local artifacts, in a time window. */
|
|
97
153
|
export interface ProjectRemoteSignals {
|
|
98
154
|
windowDays: number;
|
|
@@ -41,6 +41,13 @@ function blank(name) {
|
|
|
41
41
|
* containing only projects with at least one matched session — callers merge
|
|
42
42
|
* with the full definition list to show zero-agent projects.
|
|
43
43
|
*/
|
|
44
|
+
/**
|
|
45
|
+
* Ensure every session carries a host for the card roster. Local
|
|
46
|
+
* `getActiveSessions()` omits `machine`; remotes already set it. Pure.
|
|
47
|
+
*/
|
|
48
|
+
export function withDefaultMachine(sessions, defaultHost) {
|
|
49
|
+
return sessions.map((s) => (s.machine ? s : { ...s, machine: defaultHost }));
|
|
50
|
+
}
|
|
44
51
|
export function rollupSessionsByProject(defs, sessions) {
|
|
45
52
|
const map = new Map();
|
|
46
53
|
const prSeen = new Map();
|
|
@@ -148,25 +155,20 @@ export function sortProjectMembers(members) {
|
|
|
148
155
|
}
|
|
149
156
|
/** Cap for the members line before it collapses to `+N more`. */
|
|
150
157
|
export const MEMBERS_LINE_LIMIT = 6;
|
|
158
|
+
/** Cap for host groups on the multi-line agents roster. */
|
|
159
|
+
export const MEMBERS_HOST_LIMIT = 8;
|
|
151
160
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* `×N` count (35 same-harness sessions in one state are one fact, not six
|
|
155
|
-
* truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
|
|
156
|
-
* `+N more` tail counting members, not cells. Pure (chalk styling only); the
|
|
157
|
-
* caller adds the label.
|
|
161
|
+
* Collapse members into distinct state cells (`agent · status · ticket[@host]`),
|
|
162
|
+
* counting duplicates as `×N`. Pure.
|
|
158
163
|
*/
|
|
159
|
-
export function
|
|
160
|
-
|
|
161
|
-
return '';
|
|
162
|
-
// Collapse identical cells — 35 same-harness sessions in the same state are
|
|
163
|
-
// one fact (`claude · running ×16`), not six truncated duplicates.
|
|
164
|
+
export function collapseMemberCells(members, opts = {}) {
|
|
165
|
+
const includeHost = opts.includeHostOnCell !== false;
|
|
164
166
|
const counts = new Map();
|
|
165
167
|
for (const m of sortProjectMembers(members)) {
|
|
166
168
|
const parts = [m.agent, m.status];
|
|
167
169
|
if (m.ticket)
|
|
168
170
|
parts.push(m.ticket);
|
|
169
|
-
const cell = parts.join(' · ') + (m.host ? ` @${m.host}` : '');
|
|
171
|
+
const cell = parts.join(' · ') + (includeHost && m.host ? ` @${m.host}` : '');
|
|
170
172
|
const key = cell.toLowerCase();
|
|
171
173
|
const entry = counts.get(key);
|
|
172
174
|
if (entry)
|
|
@@ -174,12 +176,117 @@ export function formatProjectMembers(members, limit = MEMBERS_LINE_LIMIT) {
|
|
|
174
176
|
else
|
|
175
177
|
counts.set(key, { cell, n: 1 });
|
|
176
178
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
return [...counts.values()].map(({ cell, n }) => ({ cell, n, members: n }));
|
|
180
|
+
}
|
|
181
|
+
function formatCollapsedCells(cells, memberTotal, limit) {
|
|
182
|
+
if (cells.length === 0)
|
|
183
|
+
return '';
|
|
184
|
+
const shown = cells.slice(0, Math.max(1, limit));
|
|
185
|
+
const shownMembers = shown.reduce((acc, e) => acc + e.members, 0);
|
|
186
|
+
const more = memberTotal - shownMembers;
|
|
187
|
+
const parts = shown.map(({ cell, n }) => (n > 1 ? `${cell} ×${n}` : cell));
|
|
188
|
+
return parts.join(chalk.dim(' · ')) + (more > 0 ? chalk.dim(` · +${more} more`) : '');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The `agents` line under `live`: one cell per DISTINCT member state —
|
|
192
|
+
* `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
|
|
193
|
+
* `×N` count (35 same-harness sessions in one state are one fact, not six
|
|
194
|
+
* truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
|
|
195
|
+
* `+N more` tail counting members, not cells. Pure (chalk styling only); the
|
|
196
|
+
* caller adds the label.
|
|
197
|
+
*
|
|
198
|
+
* Prefer {@link formatProjectMembersByHost} on the card: a flat line hides which
|
|
199
|
+
* machine is running the work when the same harness×status spans hosts.
|
|
200
|
+
*/
|
|
201
|
+
export function formatProjectMembers(members, limit = MEMBERS_LINE_LIMIT) {
|
|
202
|
+
if (members.length === 0)
|
|
203
|
+
return '';
|
|
204
|
+
return formatCollapsedCells(collapseMemberCells(members, { includeHostOnCell: true }), members.length, limit);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Host-grouped agents roster. One content line per host:
|
|
208
|
+
* `@zion claude · running ×9 · claude · idle ×4`
|
|
209
|
+
*
|
|
210
|
+
* When no member carries a host (local-only rollup with no machine stamp), falls
|
|
211
|
+
* back to a single flat line via {@link formatProjectMembers}. Pure.
|
|
212
|
+
*/
|
|
213
|
+
export function formatProjectMembersByHost(members, opts = {}) {
|
|
214
|
+
if (members.length === 0)
|
|
215
|
+
return [];
|
|
216
|
+
const cellLimit = opts.cellLimit ?? MEMBERS_LINE_LIMIT;
|
|
217
|
+
const hostLimit = opts.hostLimit ?? MEMBERS_HOST_LIMIT;
|
|
218
|
+
const byHost = new Map();
|
|
219
|
+
let anyHost = false;
|
|
220
|
+
for (const m of members) {
|
|
221
|
+
if (m.host)
|
|
222
|
+
anyHost = true;
|
|
223
|
+
const key = m.host ?? '';
|
|
224
|
+
const list = byHost.get(key);
|
|
225
|
+
if (list)
|
|
226
|
+
list.push(m);
|
|
227
|
+
else
|
|
228
|
+
byHost.set(key, [m]);
|
|
229
|
+
}
|
|
230
|
+
// Local-only (no host stamps at all) — keep the compact one-liner.
|
|
231
|
+
if (!anyHost) {
|
|
232
|
+
const line = formatProjectMembers(members, cellLimit);
|
|
233
|
+
return line ? [line] : [];
|
|
234
|
+
}
|
|
235
|
+
// Hosts with the most members first, then name; unstamped ("") last.
|
|
236
|
+
const hosts = [...byHost.entries()].sort((a, b) => {
|
|
237
|
+
if (a[0] === '' && b[0] !== '')
|
|
238
|
+
return 1;
|
|
239
|
+
if (b[0] === '' && a[0] !== '')
|
|
240
|
+
return -1;
|
|
241
|
+
if (b[1].length !== a[1].length)
|
|
242
|
+
return b[1].length - a[1].length;
|
|
243
|
+
return a[0].localeCompare(b[0]);
|
|
244
|
+
});
|
|
245
|
+
const shownHosts = hosts.slice(0, Math.max(1, hostLimit));
|
|
246
|
+
const hiddenMembers = hosts.slice(hostLimit).reduce((acc, [, ms]) => acc + ms.length, 0);
|
|
247
|
+
const hostWidth = Math.max(...shownHosts.map(([h]) => (h ? `@${h}` : '@local').length), 1);
|
|
248
|
+
const lines = shownHosts.map(([host, ms]) => {
|
|
249
|
+
const label = (host ? `@${host}` : '@local').padEnd(hostWidth);
|
|
250
|
+
// Host is the row key — do not repeat @host on every cell.
|
|
251
|
+
const cells = collapseMemberCells(ms, { includeHostOnCell: false });
|
|
252
|
+
const body = formatCollapsedCells(cells, ms.length, cellLimit);
|
|
253
|
+
return `${chalk.cyan(label)} ${body}`;
|
|
254
|
+
});
|
|
255
|
+
if (hiddenMembers > 0) {
|
|
256
|
+
const restHosts = hosts.length - shownHosts.length;
|
|
257
|
+
lines.push(chalk.dim(`+${hiddenMembers} more on ${restHosts} host${restHosts === 1 ? '' : 's'}`));
|
|
258
|
+
}
|
|
259
|
+
return lines;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Severity markers for the warnings footer. User-facing by design: critical
|
|
263
|
+
* stops you (wrong repo, missing checkout, large drift); continue is a soft
|
|
264
|
+
* nudge (dirty tree, schedule not measurable).
|
|
265
|
+
*/
|
|
266
|
+
export function warningEmoji(severity) {
|
|
267
|
+
return severity === 'critical' ? '🔴' : '⚠️';
|
|
268
|
+
}
|
|
269
|
+
/** Stable sort: critical first, then continue; stable within a tier. */
|
|
270
|
+
export function sortProjectWarnings(warnings) {
|
|
271
|
+
const rank = { critical: 0, continue: 1 };
|
|
272
|
+
return [...warnings].sort((a, b) => rank[a.severity] - rank[b.severity] || a.text.localeCompare(b.text));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Format one or more warning lines for the card footer. Pure (chalk only).
|
|
276
|
+
* Returns empty when there is nothing to say.
|
|
277
|
+
*/
|
|
278
|
+
export function formatProjectWarnings(warnings) {
|
|
279
|
+
if (warnings.length === 0)
|
|
280
|
+
return [];
|
|
281
|
+
const lines = [];
|
|
282
|
+
for (const w of sortProjectWarnings(warnings)) {
|
|
283
|
+
const mark = warningEmoji(w.severity);
|
|
284
|
+
const color = w.severity === 'critical' ? chalk.red : chalk.yellow;
|
|
285
|
+
lines.push(` ${mark} ${color(w.text)}`);
|
|
286
|
+
if (w.remediation)
|
|
287
|
+
lines.push(` ${chalk.dim(w.remediation)}`);
|
|
288
|
+
}
|
|
289
|
+
return lines;
|
|
183
290
|
}
|
|
184
291
|
/**
|
|
185
292
|
* Harvest the signals that don't live on the active-session list: recently
|
|
@@ -124,6 +124,9 @@ export function getMcpConfigPath(agent, versionHome) {
|
|
|
124
124
|
return path.join(versionHome, '.grok', 'mcp.json');
|
|
125
125
|
case 'hermes':
|
|
126
126
|
return path.join(versionHome, '.hermes', 'config.yaml');
|
|
127
|
+
case 'pi':
|
|
128
|
+
// omp reads user-scope MCP from ~/.omp/agent/.mcp.json (Claude schema).
|
|
129
|
+
return path.join(versionHome, '.omp', 'agent', '.mcp.json');
|
|
127
130
|
default:
|
|
128
131
|
return null;
|
|
129
132
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Union: All resources from all layers are combined
|
|
6
6
|
* - Override on name conflict: Higher layer wins (project > user > system)
|
|
7
7
|
*/
|
|
8
|
-
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes';
|
|
8
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
|
|
9
9
|
export type Layer = 'system' | 'user' | 'project';
|
|
10
10
|
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
|
|
11
11
|
/** A resolved resource with its origin layer. */
|
|
Binary file
|
|
Binary file
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* The event vocabulary, all audit-level and non-milestone (so they surface in
|
|
21
21
|
* `agents events` and the persisted audit trail, but are NOT required in the
|
|
22
|
-
* curated `agents
|
|
22
|
+
* curated `agents feed` surface):
|
|
23
23
|
* - `secrets.get` — a value was READ (exec inject, export, `view --reveal`,
|
|
24
24
|
* raw `get <item>`, remote resolve, sync push,
|
|
25
25
|
* `run --secrets`, and every other bundle read).
|
|
@@ -121,6 +121,7 @@ export declare function withRawKeychainServiceNames<T>(fn: () => T): T;
|
|
|
121
121
|
* the re-key migration and tests; runtime callers go through the primitives,
|
|
122
122
|
* which apply this transparently. */
|
|
123
123
|
export declare function hashedServiceName(item: string, key: Buffer): string;
|
|
124
|
+
export declare function readHmacKeyRecord(): HmacKeyRecord | null;
|
|
124
125
|
/**
|
|
125
126
|
* Heal a `hmackey` item that an OLD helper (pre the metadata/hmackey no-ACL
|
|
126
127
|
* migration fix) re-stamped with a biometry ACL. Such an item makes EVERY hashed
|
|
@@ -235,7 +235,7 @@ function parseHmacKeyRecord(raw) {
|
|
|
235
235
|
}
|
|
236
236
|
return null;
|
|
237
237
|
}
|
|
238
|
-
function readHmacKeyRecord() {
|
|
238
|
+
export function readHmacKeyRecord() {
|
|
239
239
|
// HMAC_KEY_ITEM is exempt from the transform, so this routes to the helper
|
|
240
240
|
// (or the test backend) under its literal name. The item is no-ACL, so the
|
|
241
241
|
// read is silent — attest that to the storm guard so a headless hashed-name
|
|
@@ -247,7 +247,30 @@ function readHmacKeyRecord() {
|
|
|
247
247
|
catch {
|
|
248
248
|
return null;
|
|
249
249
|
}
|
|
250
|
-
|
|
250
|
+
const record = parseHmacKeyRecord(raw);
|
|
251
|
+
// Converge a stale-ACL'd hmackey to silent, on the HOT read path. An old helper
|
|
252
|
+
// (pre the metadata/hmackey no-ACL migration fix) re-stamped this
|
|
253
|
+
// contractually-no-ACL item with a biometry ACL, so the read just above pops the
|
|
254
|
+
// generic "Agents CLI needs to authenticate" sheet on EVERY hashed lookup — the
|
|
255
|
+
// `agents devices list` stats probe the SessionStart hook runs, and every other
|
|
256
|
+
// background hashed read. `maybeAutoRekey`'s one-shot heal only fires on a
|
|
257
|
+
// cleartext-bundle resolve and is bypassed for the hmackey/hashed-name path
|
|
258
|
+
// (prepareServiceName returns early for HMAC_KEY_ITEM before maybeAutoRekey), so
|
|
259
|
+
// it never converged exactly these reads and the machine prompted forever.
|
|
260
|
+
// Re-store the record no-ACL once per machine here (the read that produced it has
|
|
261
|
+
// already happened — and already prompted if the item was ACL'd); every
|
|
262
|
+
// subsequent read, in this process and all future ones, is silent.
|
|
263
|
+
if (record && !record.healedNoAcl) {
|
|
264
|
+
try {
|
|
265
|
+
healHmacKeyNoAclOnce(record);
|
|
266
|
+
record.healedNoAcl = true;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// A failed no-ACL re-store leaves the item still ACL'd (a still-prompting
|
|
270
|
+
// read) rather than a silent wrong state; the next process retries.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return record;
|
|
251
274
|
}
|
|
252
275
|
function writeHmacKeyRecord(rec) {
|
|
253
276
|
// JSON.stringify drops undefined fields (used to clear pendingDeletes).
|
|
@@ -423,19 +446,10 @@ function maybeAutoRekey() {
|
|
|
423
446
|
return;
|
|
424
447
|
const st = resolveHashState();
|
|
425
448
|
if (st.active) {
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
if (st.record && !st.record.healedNoAcl) {
|
|
431
|
-
try {
|
|
432
|
-
healHmacKeyNoAclOnce(st.record);
|
|
433
|
-
st.record.healedNoAcl = true;
|
|
434
|
-
}
|
|
435
|
-
catch {
|
|
436
|
-
/* next process retries */
|
|
437
|
-
}
|
|
438
|
-
}
|
|
449
|
+
// The stale-ACL'd-hmackey heal now runs on the hot read path
|
|
450
|
+
// (readHmacKeyRecord), which the resolveHashState() above just went through —
|
|
451
|
+
// so st.record is already healed here regardless of how this machine reached
|
|
452
|
+
// "hashing active". Nothing to do but finish any pending deletes.
|
|
439
453
|
if (st.record?.pendingDeletes?.length) {
|
|
440
454
|
try {
|
|
441
455
|
finishPendingDeletes(st.record);
|
|
@@ -46,7 +46,7 @@ export declare function resolveHostSshTarget(nameOrAlias: string): Promise<strin
|
|
|
46
46
|
* Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
|
|
47
47
|
* aliases) into an ordered, de-duplicated list. All four flags compose; any alone
|
|
48
48
|
* works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
|
|
49
|
-
* fleet-wide `--device` vocabulary (see `agents
|
|
49
|
+
* fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
|
|
50
50
|
* works on the secrets remote commands too. Empty when none is set.
|
|
51
51
|
*/
|
|
52
52
|
export declare function parseHostsOption(opts: {
|
|
@@ -77,7 +77,7 @@ export async function resolveHostSshTarget(nameOrAlias) {
|
|
|
77
77
|
* Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
|
|
78
78
|
* aliases) into an ordered, de-duplicated list. All four flags compose; any alone
|
|
79
79
|
* works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
|
|
80
|
-
* fleet-wide `--device` vocabulary (see `agents
|
|
80
|
+
* fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
|
|
81
81
|
* works on the secrets remote commands too. Empty when none is set.
|
|
82
82
|
*/
|
|
83
83
|
export function parseHostsOption(opts) {
|
|
@@ -58,6 +58,8 @@ export interface ActiveSession {
|
|
|
58
58
|
pid?: number;
|
|
59
59
|
sessionId?: string;
|
|
60
60
|
cwd?: string;
|
|
61
|
+
/** Project/repo key derived from cwd, when known. */
|
|
62
|
+
project?: string | null;
|
|
61
63
|
/** User-given name from /rename command. */
|
|
62
64
|
label?: string;
|
|
63
65
|
/** Durable `agents run --name` launch handle, when the run was named. */
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses the raw command strings agents pass to Bash tool calls into structured
|
|
3
3
|
* metadata: the executable, category, subcommand, and a display summary. Used by
|
|
4
|
-
* session rendering and the activity-log hook so `agents sessions`
|
|
5
|
-
*
|
|
6
|
-
* wall of shell.
|
|
4
|
+
* session rendering and the activity-log hook so `agents sessions` can
|
|
5
|
+
* summarize what actually happened instead of printing a wall of shell.
|
|
7
6
|
*/
|
|
8
7
|
export type BashCategory = 'vcs' | 'build-test' | 'install' | 'remote' | 'http' | 'media' | 'upscaling' | 'metadata' | 'probe' | 'search' | 'shell' | 'wait' | 'other';
|
|
9
8
|
export interface BashToolInfo {
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -12,7 +12,14 @@ import { type IndexedToolCall } from './tool-calls.js';
|
|
|
12
12
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
13
13
|
* assert against the constant instead of hardcoding a number that every bump
|
|
14
14
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
15
|
-
export declare const SCHEMA_VERSION =
|
|
15
|
+
export declare const SCHEMA_VERSION = 31;
|
|
16
|
+
/**
|
|
17
|
+
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
18
|
+
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
19
|
+
* rows with a lower version are treated as stale — the same mechanism
|
|
20
|
+
* TOOL_INDEX_VERSION gives the tool backfill).
|
|
21
|
+
*/
|
|
22
|
+
export declare const RESOURCE_INDEX_VERSION = 1;
|
|
16
23
|
/** Raw row shape returned from the sessions table. */
|
|
17
24
|
export interface SessionRow {
|
|
18
25
|
id: string;
|
|
@@ -75,6 +82,8 @@ export interface QueryOptions {
|
|
|
75
82
|
/** Match any session whose cwd equals this or is a descendant of it. */
|
|
76
83
|
cwdPrefix?: string;
|
|
77
84
|
project?: string;
|
|
85
|
+
/** Only sessions recorded on this machine (host), case-insensitive. */
|
|
86
|
+
machine?: string;
|
|
78
87
|
/** Match the full session id or short id, case-insensitively (exact). */
|
|
79
88
|
idExact?: string;
|
|
80
89
|
/** Match sessions whose id or short id begins with this (case-insensitive prefix). */
|
|
@@ -324,6 +333,88 @@ export declare function queryAffinityRollup(options: {
|
|
|
324
333
|
export declare function queryUsageRollup(options: QueryOptions & {
|
|
325
334
|
groupBy: UsageRollupGroup;
|
|
326
335
|
}): UsageRollupRow[];
|
|
336
|
+
/** One aggregated resource (skill or slash-command) in a usage-stats rollup. */
|
|
337
|
+
export interface ResourceStatRow {
|
|
338
|
+
/** 'skill' or 'command' (singular, as stored in session_resource_usage.kind). */
|
|
339
|
+
kind: string;
|
|
340
|
+
/** Stored resource name — bare, or `plugin:short` for a plugin-owned resource. */
|
|
341
|
+
name: string;
|
|
342
|
+
/** Owning plugin, or null for a flat (non-namespaced) resource. */
|
|
343
|
+
plugin: string | null;
|
|
344
|
+
/** DotAgents layer or plugin marketplace the resource resolved to at write time. */
|
|
345
|
+
source: string | null;
|
|
346
|
+
/** Distinct sessions that invoked this resource within the filter window. */
|
|
347
|
+
sessions: number;
|
|
348
|
+
/** Total invocations (sum of per-session counts) within the window. */
|
|
349
|
+
invocations: number;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Roll up skill / slash-command usage from session_resource_usage, joined to
|
|
353
|
+
* `sessions` for attribution so the same filter shape as querySessions
|
|
354
|
+
* (agent / project / since / machine) narrows WHICH sessions count. Grouped by
|
|
355
|
+
* resource identity (kind + name + plugin + source) and ordered by invocation
|
|
356
|
+
* volume — the read side of "which skills/commands do I actually use, and which
|
|
357
|
+
* are dead weight". `order: 'bottom'` ranks least-used first (the one-time
|
|
358
|
+
* skills); `limit` caps the returned rows.
|
|
359
|
+
*
|
|
360
|
+
* The signal only captures EXPLICIT invocations (slash commands and `Skill`
|
|
361
|
+
* tool calls). An auto-triggered skill (loaded by description match) emits no
|
|
362
|
+
* event, so it reads as zero here — a 0 means "never explicitly invoked", not
|
|
363
|
+
* "never loaded". Skill invocations are recorded for Claude and Kimi (the
|
|
364
|
+
* `Skill`-tool harnesses); slash-commands are Claude-only.
|
|
365
|
+
*
|
|
366
|
+
* `kind` / `pluginFilter` filter the RESOURCE rows directly (r.kind / r.plugin),
|
|
367
|
+
* distinct from QueryOptions.skill / QueryOptions.plugin, which filter SESSIONS
|
|
368
|
+
* — deliberately not routed through buildSessionWhere so `--plugin rush` shows
|
|
369
|
+
* only rush's resources rather than every resource used by a rush-touching
|
|
370
|
+
* session.
|
|
371
|
+
*/
|
|
372
|
+
export declare function queryResourceUsageStats(options: QueryOptions & {
|
|
373
|
+
kind?: 'skill' | 'command';
|
|
374
|
+
pluginFilter?: string;
|
|
375
|
+
order?: 'top' | 'bottom';
|
|
376
|
+
limit?: number;
|
|
377
|
+
}): ResourceStatRow[];
|
|
378
|
+
/**
|
|
379
|
+
* Coverage of the resource-usage signal: how many distinct sessions carry any
|
|
380
|
+
* row in session_resource_usage vs. the total indexed. A low ratio means the
|
|
381
|
+
* historical backfill (`agents sessions backfill resources`) hasn't run — the
|
|
382
|
+
* stats surface uses this to tell the user their zero-counts may just be
|
|
383
|
+
* un-scanned history, not genuine non-use.
|
|
384
|
+
*/
|
|
385
|
+
export declare function resourceUsageCoverage(): {
|
|
386
|
+
covered: number;
|
|
387
|
+
total: number;
|
|
388
|
+
};
|
|
389
|
+
/** Outcome of a resource-usage backfill run. */
|
|
390
|
+
export interface ResourceBackfillResult {
|
|
391
|
+
/** Sessions considered (matched the filter, had a real transcript). */
|
|
392
|
+
scanned: number;
|
|
393
|
+
/** Sessions (re)parsed and written this run. */
|
|
394
|
+
updated: number;
|
|
395
|
+
/** Sessions already current at this extractor version, skipped. */
|
|
396
|
+
skipped: number;
|
|
397
|
+
/** Sessions whose transcript could not be stat'd or parsed. */
|
|
398
|
+
failed: number;
|
|
399
|
+
/** Total session_resource_usage rows written across updated sessions. */
|
|
400
|
+
resourceRows: number;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* One-shot historical backfill of session_resource_usage (#12). The normal
|
|
404
|
+
* incremental scan writes resource usage only for sessions whose transcript it
|
|
405
|
+
* (re)parses; a session indexed before this feature shipped keeps a fresh
|
|
406
|
+
* scan_ledger row and is never re-derived, so its skill/slash-command tallies
|
|
407
|
+
* were never recorded. This walks the session index, re-parses each transcript
|
|
408
|
+
* FROM BYTE 0 (parseSession fully materializes — no resumable cursor, so
|
|
409
|
+
* claude/codex re-derive their tallies from scratch), writes the usage, and
|
|
410
|
+
* stamps resource_scan_ledger so reruns skip completed transcripts — the same
|
|
411
|
+
* independent-ledger shape `agents sessions backfill tools` uses.
|
|
412
|
+
*
|
|
413
|
+
* Harness-agnostic: parseSession + extractSkills/extractSlashCommands cover
|
|
414
|
+
* every agent uniformly, so there is no per-harness branch to keep in parity.
|
|
415
|
+
* Synthetic rows without a transcript (OpenClaw channels/cron) are skipped.
|
|
416
|
+
*/
|
|
417
|
+
export declare function backfillResourceUsage(filter?: QueryOptions, onProgress?: (done: number, total: number) => void): ResourceBackfillResult;
|
|
327
418
|
/** Who spawned a team: the orchestrator session, from its transcript. */
|
|
328
419
|
export interface TeamSpawner {
|
|
329
420
|
sessionId: string;
|