@phnx-labs/agents-cli 1.22.6 → 1.22.8
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 +131 -1
- package/README.md +7 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +83 -16
- 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/harness.d.ts +0 -1
- package/dist/commands/harness.js +60 -4
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/projects.js +122 -107
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- 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/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/teams.js +2 -1
- package/dist/commands/view.d.ts +9 -1
- package/dist/commands/view.js +63 -12
- package/dist/index.js +4 -2
- 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/ipc.js +2 -0
- package/dist/lib/browser/profiles.d.ts +15 -7
- package/dist/lib/browser/profiles.js +53 -12
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -2
- 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/device-config.js +8 -0
- 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.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +23 -3
- package/dist/lib/hosts/remote-cmd.js +1 -0
- 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/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +73 -10
- package/dist/lib/profiles.js +133 -15
- 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/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- 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/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -1
- package/dist/lib/startup/command-registry.js +2 -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
package/dist/lib/session/db.js
CHANGED
|
@@ -25,7 +25,14 @@ const DB_PATH = getSessionsDbPath();
|
|
|
25
25
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
26
26
|
* assert against the constant instead of hardcoding a number that every bump
|
|
27
27
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
28
|
-
export const SCHEMA_VERSION =
|
|
28
|
+
export const SCHEMA_VERSION = 31;
|
|
29
|
+
/**
|
|
30
|
+
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
31
|
+
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
32
|
+
* rows with a lower version are treated as stale — the same mechanism
|
|
33
|
+
* TOOL_INDEX_VERSION gives the tool backfill).
|
|
34
|
+
*/
|
|
35
|
+
export const RESOURCE_INDEX_VERSION = 1;
|
|
29
36
|
/**
|
|
30
37
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
31
38
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -238,6 +245,25 @@ CREATE TABLE IF NOT EXISTS session_resource_usage (
|
|
|
238
245
|
);
|
|
239
246
|
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_kind_name ON session_resource_usage(kind, name);
|
|
240
247
|
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_plugin ON session_resource_usage(plugin);
|
|
248
|
+
|
|
249
|
+
-- One-shot historical backfill bookkeeping for session_resource_usage, mirroring
|
|
250
|
+
-- tool_scan_ledger. The normal incremental scan writes resource usage for every
|
|
251
|
+
-- session it (re)parses, but a session indexed before #12 shipped keeps a fresh
|
|
252
|
+
-- scan_ledger row and is never re-derived — so its skill/slash-command tallies
|
|
253
|
+
-- were never recorded. The "agents sessions backfill resources" command walks
|
|
254
|
+
-- history, re-parses each transcript from byte 0, and stamps coverage here
|
|
255
|
+
-- (mtime + size + extractor_version) so reruns skip completed transcripts. This
|
|
256
|
+
-- is a SCAN LEDGER, not a second copy of the usage data — the usage itself lives
|
|
257
|
+
-- only in session_resource_usage.
|
|
258
|
+
CREATE TABLE IF NOT EXISTS resource_scan_ledger (
|
|
259
|
+
session_id TEXT PRIMARY KEY,
|
|
260
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
261
|
+
file_mtime_ms INTEGER NOT NULL,
|
|
262
|
+
file_size INTEGER NOT NULL,
|
|
263
|
+
extractor_version INTEGER NOT NULL,
|
|
264
|
+
indexed_at INTEGER NOT NULL,
|
|
265
|
+
resource_count INTEGER NOT NULL
|
|
266
|
+
);
|
|
241
267
|
`;
|
|
242
268
|
let dbInstance = null;
|
|
243
269
|
/**
|
|
@@ -678,6 +704,23 @@ function migrateSchema(db, fromVersion) {
|
|
|
678
704
|
if (!cols.has('used_computer'))
|
|
679
705
|
db.exec(`ALTER TABLE sessions ADD COLUMN used_computer INTEGER`);
|
|
680
706
|
}
|
|
707
|
+
if (fromVersion < 31) {
|
|
708
|
+
// v30 → v31: independent ledger for the explicit resource-usage backfill
|
|
709
|
+
// (agents sessions backfill resources). Do NOT wipe scan_ledger — normal
|
|
710
|
+
// session listing stays warm; historical resource rows are filled once on
|
|
711
|
+
// demand, exactly like the tool-index ledger (v25).
|
|
712
|
+
db.exec(`
|
|
713
|
+
CREATE TABLE IF NOT EXISTS resource_scan_ledger (
|
|
714
|
+
session_id TEXT PRIMARY KEY,
|
|
715
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
716
|
+
file_mtime_ms INTEGER NOT NULL,
|
|
717
|
+
file_size INTEGER NOT NULL,
|
|
718
|
+
extractor_version INTEGER NOT NULL,
|
|
719
|
+
indexed_at INTEGER NOT NULL,
|
|
720
|
+
resource_count INTEGER NOT NULL
|
|
721
|
+
);
|
|
722
|
+
`);
|
|
723
|
+
}
|
|
681
724
|
}
|
|
682
725
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
683
726
|
export function getDB() {
|
|
@@ -1829,6 +1872,10 @@ function buildSessionWhere(options) {
|
|
|
1829
1872
|
where.push('LOWER(IFNULL(project, \'\')) LIKE ?');
|
|
1830
1873
|
params.push(`%${options.project.toLowerCase()}%`);
|
|
1831
1874
|
}
|
|
1875
|
+
if (options.machine) {
|
|
1876
|
+
where.push('machine = ? COLLATE NOCASE');
|
|
1877
|
+
params.push(options.machine);
|
|
1878
|
+
}
|
|
1832
1879
|
// id lookup. SQLite's LIKE is case-insensitive for ASCII, so a lowercased
|
|
1833
1880
|
// pattern matches mixed-case ids; the `=` exact compare adds COLLATE NOCASE
|
|
1834
1881
|
// for the same reason. short_id carries its own index (idx_sessions_short_id);
|
|
@@ -2006,6 +2053,188 @@ export function queryUsageRollup(options) {
|
|
|
2006
2053
|
`;
|
|
2007
2054
|
return db.prepare(sql).all(...params);
|
|
2008
2055
|
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Roll up skill / slash-command usage from session_resource_usage, joined to
|
|
2058
|
+
* `sessions` for attribution so the same filter shape as querySessions
|
|
2059
|
+
* (agent / project / since / machine) narrows WHICH sessions count. Grouped by
|
|
2060
|
+
* resource identity (kind + name + plugin + source) and ordered by invocation
|
|
2061
|
+
* volume — the read side of "which skills/commands do I actually use, and which
|
|
2062
|
+
* are dead weight". `order: 'bottom'` ranks least-used first (the one-time
|
|
2063
|
+
* skills); `limit` caps the returned rows.
|
|
2064
|
+
*
|
|
2065
|
+
* The signal only captures EXPLICIT invocations (slash commands and `Skill`
|
|
2066
|
+
* tool calls). An auto-triggered skill (loaded by description match) emits no
|
|
2067
|
+
* event, so it reads as zero here — a 0 means "never explicitly invoked", not
|
|
2068
|
+
* "never loaded". Skill invocations are recorded for Claude and Kimi (the
|
|
2069
|
+
* `Skill`-tool harnesses); slash-commands are Claude-only.
|
|
2070
|
+
*
|
|
2071
|
+
* `kind` / `pluginFilter` filter the RESOURCE rows directly (r.kind / r.plugin),
|
|
2072
|
+
* distinct from QueryOptions.skill / QueryOptions.plugin, which filter SESSIONS
|
|
2073
|
+
* — deliberately not routed through buildSessionWhere so `--plugin rush` shows
|
|
2074
|
+
* only rush's resources rather than every resource used by a rush-touching
|
|
2075
|
+
* session.
|
|
2076
|
+
*/
|
|
2077
|
+
export function queryResourceUsageStats(options) {
|
|
2078
|
+
const db = getDB();
|
|
2079
|
+
const { clause, params } = buildSessionWhere(options);
|
|
2080
|
+
// buildSessionWhere emits bare column names (agent, timestamp, machine, …) and
|
|
2081
|
+
// `id IN (…)` subqueries; in this join they resolve unambiguously to `sessions`
|
|
2082
|
+
// (session_resource_usage carries none of those columns, and its key is
|
|
2083
|
+
// session_id, not id). Strip the leading WHERE so resource predicates append.
|
|
2084
|
+
const base = clause.replace(/^WHERE\s+/, '');
|
|
2085
|
+
const preds = base ? [base] : [];
|
|
2086
|
+
const allParams = [...params];
|
|
2087
|
+
if (options.kind) {
|
|
2088
|
+
preds.push('r.kind = ?');
|
|
2089
|
+
allParams.push(options.kind);
|
|
2090
|
+
}
|
|
2091
|
+
if (options.pluginFilter) {
|
|
2092
|
+
preds.push('r.plugin = ? COLLATE NOCASE');
|
|
2093
|
+
allParams.push(options.pluginFilter);
|
|
2094
|
+
}
|
|
2095
|
+
const whereClause = preds.length ? `WHERE ${preds.join(' AND ')}` : '';
|
|
2096
|
+
const direction = options.order === 'bottom' ? 'ASC' : 'DESC';
|
|
2097
|
+
const limitClause = options.limit ? `LIMIT ${Math.max(1, Math.floor(options.limit))}` : '';
|
|
2098
|
+
// Group by resource IDENTITY = (kind, name) only, per SES-IF-4b. name already
|
|
2099
|
+
// embeds the `plugin:short` prefix for a plugin resource, so (kind, name) is
|
|
2100
|
+
// the true identity. plugin and source are PROVENANCE, not identity, and drift
|
|
2101
|
+
// per session — the same `rush:design` resolves plugin='rush' in a session
|
|
2102
|
+
// whose cwd discovered the plugin and plugin=NULL in one that didn't, and the
|
|
2103
|
+
// layer a flat resource resolved to varies (user vs system). Grouping on either
|
|
2104
|
+
// would split one resource into fractional rows. Aggregate both with MAX so the
|
|
2105
|
+
// row carries a representative (non-NULL when any session resolved it) label
|
|
2106
|
+
// while the counts stay whole.
|
|
2107
|
+
const sql = `
|
|
2108
|
+
SELECT
|
|
2109
|
+
r.kind AS kind,
|
|
2110
|
+
r.name AS name,
|
|
2111
|
+
MAX(r.plugin) AS plugin,
|
|
2112
|
+
MAX(r.source) AS source,
|
|
2113
|
+
COUNT(DISTINCT r.session_id) AS sessions,
|
|
2114
|
+
SUM(r.count) AS invocations
|
|
2115
|
+
FROM session_resource_usage r
|
|
2116
|
+
JOIN sessions s ON s.id = r.session_id
|
|
2117
|
+
${whereClause}
|
|
2118
|
+
GROUP BY r.kind, r.name
|
|
2119
|
+
ORDER BY invocations ${direction}, sessions ${direction}, r.name ASC
|
|
2120
|
+
${limitClause}
|
|
2121
|
+
`;
|
|
2122
|
+
return db.prepare(sql).all(...allParams);
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Coverage of the resource-usage signal: how many distinct sessions carry any
|
|
2126
|
+
* row in session_resource_usage vs. the total indexed. A low ratio means the
|
|
2127
|
+
* historical backfill (`agents sessions backfill resources`) hasn't run — the
|
|
2128
|
+
* stats surface uses this to tell the user their zero-counts may just be
|
|
2129
|
+
* un-scanned history, not genuine non-use.
|
|
2130
|
+
*/
|
|
2131
|
+
export function resourceUsageCoverage() {
|
|
2132
|
+
const db = getDB();
|
|
2133
|
+
const covered = db.prepare(`SELECT COUNT(DISTINCT session_id) AS n FROM session_resource_usage`).get().n;
|
|
2134
|
+
const total = db.prepare(`SELECT COUNT(*) AS n FROM sessions`).get().n;
|
|
2135
|
+
return { covered, total };
|
|
2136
|
+
}
|
|
2137
|
+
/** Has this session's resource usage been derived at the current extractor version for this exact file? */
|
|
2138
|
+
function needsResourceIndex(db, sessionId, stamp) {
|
|
2139
|
+
const row = db
|
|
2140
|
+
.prepare(`SELECT file_mtime_ms, file_size, extractor_version FROM resource_scan_ledger WHERE session_id = ?`)
|
|
2141
|
+
.get(sessionId);
|
|
2142
|
+
return !row
|
|
2143
|
+
|| row.file_mtime_ms !== stamp.fileMtimeMs
|
|
2144
|
+
|| row.file_size !== stamp.fileSize
|
|
2145
|
+
|| row.extractor_version !== RESOURCE_INDEX_VERSION;
|
|
2146
|
+
}
|
|
2147
|
+
/** Record that a session's resource usage is current at RESOURCE_INDEX_VERSION for this file stamp. */
|
|
2148
|
+
function stampResourceLedger(db, sessionId, filePath, stamp, resourceCount) {
|
|
2149
|
+
db.prepare(`
|
|
2150
|
+
INSERT INTO resource_scan_ledger
|
|
2151
|
+
(session_id, file_path, file_mtime_ms, file_size, extractor_version, indexed_at, resource_count)
|
|
2152
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2153
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
2154
|
+
file_path = excluded.file_path,
|
|
2155
|
+
file_mtime_ms = excluded.file_mtime_ms,
|
|
2156
|
+
file_size = excluded.file_size,
|
|
2157
|
+
extractor_version = excluded.extractor_version,
|
|
2158
|
+
indexed_at = excluded.indexed_at,
|
|
2159
|
+
resource_count = excluded.resource_count
|
|
2160
|
+
`).run(sessionId, filePath, stamp.fileMtimeMs, stamp.fileSize, RESOURCE_INDEX_VERSION, Date.now(), resourceCount);
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* One-shot historical backfill of session_resource_usage (#12). The normal
|
|
2164
|
+
* incremental scan writes resource usage only for sessions whose transcript it
|
|
2165
|
+
* (re)parses; a session indexed before this feature shipped keeps a fresh
|
|
2166
|
+
* scan_ledger row and is never re-derived, so its skill/slash-command tallies
|
|
2167
|
+
* were never recorded. This walks the session index, re-parses each transcript
|
|
2168
|
+
* FROM BYTE 0 (parseSession fully materializes — no resumable cursor, so
|
|
2169
|
+
* claude/codex re-derive their tallies from scratch), writes the usage, and
|
|
2170
|
+
* stamps resource_scan_ledger so reruns skip completed transcripts — the same
|
|
2171
|
+
* independent-ledger shape `agents sessions backfill tools` uses.
|
|
2172
|
+
*
|
|
2173
|
+
* Harness-agnostic: parseSession + extractSkills/extractSlashCommands cover
|
|
2174
|
+
* every agent uniformly, so there is no per-harness branch to keep in parity.
|
|
2175
|
+
* Synthetic rows without a transcript (OpenClaw channels/cron) are skipped.
|
|
2176
|
+
*/
|
|
2177
|
+
export function backfillResourceUsage(filter = {}, onProgress) {
|
|
2178
|
+
const db = getDB();
|
|
2179
|
+
// No LIMIT: the backfill covers the whole matching history. skipExistenceCheck
|
|
2180
|
+
// stays off so vanished transcripts are dropped, matching querySessions.
|
|
2181
|
+
const sessions = querySessions({ ...filter, limit: undefined });
|
|
2182
|
+
const result = { scanned: 0, updated: 0, skipped: 0, failed: 0, resourceRows: 0 };
|
|
2183
|
+
let done = 0;
|
|
2184
|
+
for (const meta of sessions) {
|
|
2185
|
+
if (!meta.filePath) {
|
|
2186
|
+
done++;
|
|
2187
|
+
onProgress?.(done, sessions.length);
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
result.scanned++;
|
|
2191
|
+
let stamp;
|
|
2192
|
+
try {
|
|
2193
|
+
const st = fs.statSync(meta.filePath);
|
|
2194
|
+
stamp = { fileMtimeMs: st.mtimeMs, fileSize: st.size };
|
|
2195
|
+
}
|
|
2196
|
+
catch {
|
|
2197
|
+
result.failed++;
|
|
2198
|
+
done++;
|
|
2199
|
+
onProgress?.(done, sessions.length);
|
|
2200
|
+
continue;
|
|
2201
|
+
}
|
|
2202
|
+
if (!needsResourceIndex(db, meta.id, stamp)) {
|
|
2203
|
+
result.skipped++;
|
|
2204
|
+
done++;
|
|
2205
|
+
onProgress?.(done, sessions.length);
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
try {
|
|
2209
|
+
const events = parseSession(meta.filePath, meta.agent);
|
|
2210
|
+
// Fail loud on a bad read. writeResourceUsage DELETEs the session's rows
|
|
2211
|
+
// before it re-inserts, so a transcript that parses to ZERO events (a
|
|
2212
|
+
// truncated / mid-sync / corrupt file — parseSession returns [] instead of
|
|
2213
|
+
// throwing) would silently wipe real historical usage and then stamp the
|
|
2214
|
+
// ledger "current", masking the loss. A completed historical transcript
|
|
2215
|
+
// always has turns, so an empty parse is never a legitimate "this session
|
|
2216
|
+
// used nothing" — it is an unreadable file. Skip it (count as failed, leave
|
|
2217
|
+
// the ledger unstamped) so a later good read retries.
|
|
2218
|
+
if (events.length === 0) {
|
|
2219
|
+
result.failed++;
|
|
2220
|
+
done++;
|
|
2221
|
+
onProgress?.(done, sessions.length);
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
writeResourceUsage(meta.id, events, meta.cwd);
|
|
2225
|
+
const rows = db.prepare(`SELECT COUNT(*) AS n FROM session_resource_usage WHERE session_id = ?`).get(meta.id).n;
|
|
2226
|
+
stampResourceLedger(db, meta.id, meta.filePath, stamp, rows);
|
|
2227
|
+
result.updated++;
|
|
2228
|
+
result.resourceRows += rows;
|
|
2229
|
+
}
|
|
2230
|
+
catch {
|
|
2231
|
+
result.failed++;
|
|
2232
|
+
}
|
|
2233
|
+
done++;
|
|
2234
|
+
onProgress?.(done, sessions.length);
|
|
2235
|
+
}
|
|
2236
|
+
return result;
|
|
2237
|
+
}
|
|
2009
2238
|
/**
|
|
2010
2239
|
* Map every team name to the session that ran `agents teams create/add` for it.
|
|
2011
2240
|
*
|
|
@@ -45,7 +45,7 @@ export declare function toolHistogram(toolCounts: Record<string, number>, top?:
|
|
|
45
45
|
/**
|
|
46
46
|
* Aggregate recognized external tools invoked via Bash across a session.
|
|
47
47
|
* Unknown one-off commands are bucketed as `other` so the ranking surface
|
|
48
|
-
*
|
|
48
|
+
* only surfaces repeatable tools.
|
|
49
49
|
*/
|
|
50
50
|
export declare function bashToolCounts(events: SessionEvent[]): Record<string, number>;
|
|
51
51
|
export interface TestResult {
|
|
@@ -131,7 +131,7 @@ export function toolHistogram(toolCounts, top = 8) {
|
|
|
131
131
|
/**
|
|
132
132
|
* Aggregate recognized external tools invoked via Bash across a session.
|
|
133
133
|
* Unknown one-off commands are bucketed as `other` so the ranking surface
|
|
134
|
-
*
|
|
134
|
+
* only surfaces repeatable tools.
|
|
135
135
|
*/
|
|
136
136
|
export function bashToolCounts(events) {
|
|
137
137
|
const counts = {};
|
|
@@ -81,6 +81,30 @@ function guessContentType(filePath) {
|
|
|
81
81
|
return 'application/json';
|
|
82
82
|
if (/\.svg$/i.test(filePath))
|
|
83
83
|
return 'image/svg+xml';
|
|
84
|
+
// Raster images + video: agents publish screenshots and screen recordings as PR
|
|
85
|
+
// evidence, and GitHub's image proxy (camo) only renders an inline `` when
|
|
86
|
+
// the asset is served with a real image/video content-type — octet-stream is
|
|
87
|
+
// refused. Type them so the share URL embeds instead of downloading.
|
|
88
|
+
if (/\.png$/i.test(filePath))
|
|
89
|
+
return 'image/png';
|
|
90
|
+
if (/\.jpe?g$/i.test(filePath))
|
|
91
|
+
return 'image/jpeg';
|
|
92
|
+
if (/\.gif$/i.test(filePath))
|
|
93
|
+
return 'image/gif';
|
|
94
|
+
if (/\.webp$/i.test(filePath))
|
|
95
|
+
return 'image/webp';
|
|
96
|
+
if (/\.avif$/i.test(filePath))
|
|
97
|
+
return 'image/avif';
|
|
98
|
+
if (/\.ico$/i.test(filePath))
|
|
99
|
+
return 'image/x-icon';
|
|
100
|
+
if (/\.mp4$/i.test(filePath))
|
|
101
|
+
return 'video/mp4';
|
|
102
|
+
if (/\.mov$/i.test(filePath))
|
|
103
|
+
return 'video/quicktime';
|
|
104
|
+
if (/\.webm$/i.test(filePath))
|
|
105
|
+
return 'video/webm';
|
|
106
|
+
if (/\.pdf$/i.test(filePath))
|
|
107
|
+
return 'application/pdf';
|
|
84
108
|
if (/\.txt$|\.md$/i.test(filePath))
|
|
85
109
|
return 'text/plain; charset=utf-8';
|
|
86
110
|
return 'application/octet-stream';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents snapshot` — one-process fleet consumer snapshot.
|
|
3
|
+
*
|
|
4
|
+
* Consumers (Factory watchdog, menubar, fleet scripts) used to fork:
|
|
5
|
+
* agents view <agent> --json × N harnesses
|
|
6
|
+
* agents sessions --active --json
|
|
7
|
+
* agents feed --json (sometimes)
|
|
8
|
+
*
|
|
9
|
+
* That is N+2 process starts per poll tick. This module gathers the same
|
|
10
|
+
* shapes in one invocation so poll count drops to 1 without redefining
|
|
11
|
+
* `agents status` (which stays the UnifiedSyncStatus sync contract).
|
|
12
|
+
*
|
|
13
|
+
* Stores are not merged — inventory still comes from view, active rows from
|
|
14
|
+
* sessions, blocks from feed. Only the reader is consolidated.
|
|
15
|
+
*/
|
|
16
|
+
import { type OpenBlock } from './feed.js';
|
|
17
|
+
import { type FleetAgentCounts } from './fleet-status.js';
|
|
18
|
+
import type { AgentId } from './types.js';
|
|
19
|
+
import type { UnifiedSyncStatus } from './sync-status.js';
|
|
20
|
+
import type { ViewJsonAgent } from '../commands/view.js';
|
|
21
|
+
/** One open-block row in the optional feed summary (no full question bodies). */
|
|
22
|
+
export interface SnapshotFeedBlock {
|
|
23
|
+
blockId: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
host: string;
|
|
26
|
+
runtime: string;
|
|
27
|
+
kind?: OpenBlock['kind'];
|
|
28
|
+
ticket?: string;
|
|
29
|
+
pr?: string;
|
|
30
|
+
questionCount: number;
|
|
31
|
+
ts: string;
|
|
32
|
+
}
|
|
33
|
+
/** Compact feed slice for needs-you polls. */
|
|
34
|
+
export interface SnapshotFeedSummary {
|
|
35
|
+
openBlocks: number;
|
|
36
|
+
blocks: SnapshotFeedBlock[];
|
|
37
|
+
}
|
|
38
|
+
/** Active-session row as emitted by `sessions --active --json`. */
|
|
39
|
+
export type SnapshotSessionRow = {
|
|
40
|
+
ticketId: string | null;
|
|
41
|
+
project: string | null;
|
|
42
|
+
prLink: string | null;
|
|
43
|
+
viewingIn: string | null;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Stable machine-readable contract for `agents snapshot --json`.
|
|
48
|
+
* Bump `version` only on breaking shape changes.
|
|
49
|
+
*/
|
|
50
|
+
export interface FleetSnapshot {
|
|
51
|
+
version: 1;
|
|
52
|
+
/** Host that produced this snapshot (machineId). */
|
|
53
|
+
host: string;
|
|
54
|
+
/** ISO-8601 capture time. */
|
|
55
|
+
capturedAt: string;
|
|
56
|
+
/** Installed agent inventory — same shape as `agents view --json`. */
|
|
57
|
+
inventory: ViewJsonAgent[];
|
|
58
|
+
/** Live sessions — same row shape as `agents sessions --active --json`. */
|
|
59
|
+
sessions: SnapshotSessionRow[];
|
|
60
|
+
/** How many remote devices contributed sessions (0 when --local). */
|
|
61
|
+
remoteDeviceCount: number;
|
|
62
|
+
/** Running/live tallies derived from `sessions` (same as fleet-status). */
|
|
63
|
+
agents: FleetAgentCounts;
|
|
64
|
+
/** Present when --with-feed. */
|
|
65
|
+
feed?: SnapshotFeedSummary;
|
|
66
|
+
/** Present when --with-sync (UnifiedSyncStatus; does not replace `agents status`). */
|
|
67
|
+
sync?: UnifiedSyncStatus;
|
|
68
|
+
}
|
|
69
|
+
export interface ComputeSnapshotOptions {
|
|
70
|
+
/** Restrict inventory to one agent id. */
|
|
71
|
+
agent?: AgentId;
|
|
72
|
+
/** Local sessions only — no cross-machine SSH fan-out. Default true for cheap polls. */
|
|
73
|
+
local?: boolean;
|
|
74
|
+
/** Explicit host filter for the sessions gather (same semantics as sessions --active). */
|
|
75
|
+
hosts?: string[];
|
|
76
|
+
/** Include open feed-block summary. */
|
|
77
|
+
withFeed?: boolean;
|
|
78
|
+
/** Include UnifiedSyncStatus (opt-in; can be slower). */
|
|
79
|
+
withSync?: boolean;
|
|
80
|
+
/** Cap feed.blocks length (default 50). */
|
|
81
|
+
feedLimit?: number;
|
|
82
|
+
}
|
|
83
|
+
/** Summarize open blocks for the snapshot feed slice. Pure / unit-testable. */
|
|
84
|
+
export declare function summarizeFeedBlocks(blocks: ReadonlyArray<OpenBlock>, limit?: number): SnapshotFeedSummary;
|
|
85
|
+
/**
|
|
86
|
+
* Assemble a snapshot payload from already-gathered pieces. Pure so tests do
|
|
87
|
+
* not need live process scans or network.
|
|
88
|
+
*/
|
|
89
|
+
export declare function assembleSnapshot(parts: {
|
|
90
|
+
host: string;
|
|
91
|
+
capturedAt: string;
|
|
92
|
+
inventory: ViewJsonAgent[];
|
|
93
|
+
sessions: SnapshotSessionRow[];
|
|
94
|
+
remoteDeviceCount: number;
|
|
95
|
+
feed?: SnapshotFeedSummary;
|
|
96
|
+
sync?: UnifiedSyncStatus;
|
|
97
|
+
}): FleetSnapshot;
|
|
98
|
+
/**
|
|
99
|
+
* Gather inventory + active sessions (+ optional feed/sync) in one process.
|
|
100
|
+
* Default `local: true` keeps the common poll path free of SSH fan-out; pass
|
|
101
|
+
* `local: false` (or hosts) to match full `sessions --active` fleet scope.
|
|
102
|
+
*/
|
|
103
|
+
export declare function computeSnapshot(opts?: ComputeSnapshotOptions): Promise<FleetSnapshot>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents snapshot` — one-process fleet consumer snapshot.
|
|
3
|
+
*
|
|
4
|
+
* Consumers (Factory watchdog, menubar, fleet scripts) used to fork:
|
|
5
|
+
* agents view <agent> --json × N harnesses
|
|
6
|
+
* agents sessions --active --json
|
|
7
|
+
* agents feed --json (sometimes)
|
|
8
|
+
*
|
|
9
|
+
* That is N+2 process starts per poll tick. This module gathers the same
|
|
10
|
+
* shapes in one invocation so poll count drops to 1 without redefining
|
|
11
|
+
* `agents status` (which stays the UnifiedSyncStatus sync contract).
|
|
12
|
+
*
|
|
13
|
+
* Stores are not merged — inventory still comes from view, active rows from
|
|
14
|
+
* sessions, blocks from feed. Only the reader is consolidated.
|
|
15
|
+
*/
|
|
16
|
+
import { machineId } from './machine-id.js';
|
|
17
|
+
import { listBlocks } from './feed.js';
|
|
18
|
+
import { computeAgentCounts } from './fleet-status.js';
|
|
19
|
+
/** Summarize open blocks for the snapshot feed slice. Pure / unit-testable. */
|
|
20
|
+
export function summarizeFeedBlocks(blocks, limit = 50) {
|
|
21
|
+
const sorted = [...blocks].sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0));
|
|
22
|
+
const slice = sorted.slice(0, Math.max(0, limit));
|
|
23
|
+
return {
|
|
24
|
+
openBlocks: blocks.length,
|
|
25
|
+
blocks: slice.map((b) => ({
|
|
26
|
+
blockId: b.blockId,
|
|
27
|
+
sessionId: b.sessionId,
|
|
28
|
+
host: b.host,
|
|
29
|
+
runtime: b.runtime,
|
|
30
|
+
kind: b.kind,
|
|
31
|
+
ticket: b.ticket,
|
|
32
|
+
pr: b.pr,
|
|
33
|
+
questionCount: b.questions?.length ?? 0,
|
|
34
|
+
ts: b.ts,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Assemble a snapshot payload from already-gathered pieces. Pure so tests do
|
|
40
|
+
* not need live process scans or network.
|
|
41
|
+
*/
|
|
42
|
+
export function assembleSnapshot(parts) {
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
host: parts.host,
|
|
46
|
+
capturedAt: parts.capturedAt,
|
|
47
|
+
inventory: parts.inventory,
|
|
48
|
+
sessions: parts.sessions,
|
|
49
|
+
remoteDeviceCount: parts.remoteDeviceCount,
|
|
50
|
+
agents: computeAgentCounts(parts.sessions.map((s) => ({
|
|
51
|
+
status: typeof s.status === 'string' ? s.status : undefined,
|
|
52
|
+
context: typeof s.context === 'string' ? s.context : undefined,
|
|
53
|
+
kind: typeof s.kind === 'string' ? s.kind : undefined,
|
|
54
|
+
}))),
|
|
55
|
+
...(parts.feed ? { feed: parts.feed } : {}),
|
|
56
|
+
...(parts.sync ? { sync: parts.sync } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Gather inventory + active sessions (+ optional feed/sync) in one process.
|
|
61
|
+
* Default `local: true` keeps the common poll path free of SSH fan-out; pass
|
|
62
|
+
* `local: false` (or hosts) to match full `sessions --active` fleet scope.
|
|
63
|
+
*/
|
|
64
|
+
export async function computeSnapshot(opts = {}) {
|
|
65
|
+
// Default local-only sessions (cheap poll). Explicit hosts → scoped fan-out.
|
|
66
|
+
// local: false (from --all-hosts) → full sessions --active fan-out.
|
|
67
|
+
const localOnly = opts.hosts?.length ? false : opts.local !== false;
|
|
68
|
+
const [{ collectAgentsJson }, sessionsMod] = await Promise.all([
|
|
69
|
+
import('../commands/view.js'),
|
|
70
|
+
import('../commands/sessions.js'),
|
|
71
|
+
]);
|
|
72
|
+
const inventoryP = collectAgentsJson(opts.agent);
|
|
73
|
+
const sessionsP = sessionsMod.gatherActiveSessions({
|
|
74
|
+
local: localOnly,
|
|
75
|
+
hosts: opts.hosts,
|
|
76
|
+
});
|
|
77
|
+
const feedP = opts.withFeed
|
|
78
|
+
? Promise.resolve(summarizeFeedBlocks(listBlocks(), opts.feedLimit ?? 50))
|
|
79
|
+
: Promise.resolve(undefined);
|
|
80
|
+
const syncP = opts.withSync
|
|
81
|
+
? import('./sync-status.js').then((m) => m.computeSyncStatus())
|
|
82
|
+
: Promise.resolve(undefined);
|
|
83
|
+
const [inventory, gathered, feed, sync] = await Promise.all([
|
|
84
|
+
inventoryP,
|
|
85
|
+
sessionsP,
|
|
86
|
+
feedP,
|
|
87
|
+
syncP,
|
|
88
|
+
]);
|
|
89
|
+
const sessions = sessionsMod.serializeActiveSessionsForJson(gathered.sessions);
|
|
90
|
+
return assembleSnapshot({
|
|
91
|
+
host: machineId(),
|
|
92
|
+
capturedAt: new Date().toISOString(),
|
|
93
|
+
inventory,
|
|
94
|
+
sessions,
|
|
95
|
+
remoteDeviceCount: gathered.remoteDeviceCount,
|
|
96
|
+
feed,
|
|
97
|
+
sync,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -56,6 +56,7 @@ export declare const loadRestore: ModuleLoader;
|
|
|
56
56
|
export declare const loadDoctor: ModuleLoader;
|
|
57
57
|
export declare const loadApply: ModuleLoader;
|
|
58
58
|
export declare const loadStatus: ModuleLoader;
|
|
59
|
+
export declare const loadSnapshot: ModuleLoader;
|
|
59
60
|
export declare const loadProfiles: ModuleLoader;
|
|
60
61
|
export declare const loadHarness: ModuleLoader;
|
|
61
62
|
export declare const loadSecrets: ModuleLoader;
|
|
@@ -97,7 +98,6 @@ export declare const loadCloud: ModuleLoader;
|
|
|
97
98
|
export declare const loadMessage: ModuleLoader;
|
|
98
99
|
export declare const loadSend: ModuleLoader;
|
|
99
100
|
export declare const loadFeed: ModuleLoader;
|
|
100
|
-
export declare const loadActivity: ModuleLoader;
|
|
101
101
|
export declare const loadMailboxes: ModuleLoader;
|
|
102
102
|
export declare const loadServe: ModuleLoader;
|
|
103
103
|
export declare const loadShare: ModuleLoader;
|
|
@@ -34,6 +34,7 @@ export const loadRestore = async () => (await import('../../commands/trash.js'))
|
|
|
34
34
|
export const loadDoctor = async () => (await import('../../commands/doctor.js')).registerDoctorCommand;
|
|
35
35
|
export const loadApply = async () => (await import('../../commands/apply.js')).registerApplyCommand;
|
|
36
36
|
export const loadStatus = async () => (await import('../../commands/status.js')).registerStatusCommand;
|
|
37
|
+
export const loadSnapshot = async () => (await import('../../commands/snapshot.js')).registerSnapshotCommand;
|
|
37
38
|
export const loadProfiles = async () => (await import('../../commands/profiles.js')).registerProfilesCommands;
|
|
38
39
|
export const loadHarness = async () => (await import('../../commands/harness.js')).registerHarnessCommands;
|
|
39
40
|
export const loadSecrets = async () => (await import('../../commands/secrets.js')).registerSecretsCommands;
|
|
@@ -75,7 +76,6 @@ export const loadCloud = async () => (await import('../../commands/cloud.js')).r
|
|
|
75
76
|
export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
|
|
76
77
|
export const loadSend = async () => (await import('../../commands/send.js')).registerSendCommand;
|
|
77
78
|
export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
|
|
78
|
-
export const loadActivity = async () => (await import('../../commands/activity.js')).registerActivityCommand;
|
|
79
79
|
export const loadMailboxes = async () => (await import('../../commands/mailboxes.js')).registerMailboxesCommand;
|
|
80
80
|
export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
|
|
81
81
|
export const loadShare = async () => (await import('../../commands/share.js')).registerShareCommands;
|
|
@@ -145,6 +145,7 @@ export const COMMAND_LOADERS = {
|
|
|
145
145
|
doctor: [loadDoctor],
|
|
146
146
|
apply: [loadApply],
|
|
147
147
|
status: [loadStatus],
|
|
148
|
+
snapshot: [loadSnapshot],
|
|
148
149
|
profile: [loadProfiles],
|
|
149
150
|
profiles: [loadProfiles],
|
|
150
151
|
harness: [loadHarness],
|
|
@@ -199,7 +200,6 @@ export const COMMAND_LOADERS = {
|
|
|
199
200
|
send: [loadSend],
|
|
200
201
|
notify: [loadSend],
|
|
201
202
|
feed: [loadFeed],
|
|
202
|
-
activity: [loadActivity],
|
|
203
203
|
mailboxes: [loadMailboxes],
|
|
204
204
|
mailbox: [loadMailboxes],
|
|
205
205
|
serve: [loadServe],
|
|
@@ -252,6 +252,9 @@ export const SUBAGENT_TARGETS = {
|
|
|
252
252
|
// Tier 1 -- flat markdown, Claude-compatible flatten.
|
|
253
253
|
claude: flatFile({ subdir: ['.claude', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
|
|
254
254
|
grok: flatFile({ subdir: ['.grok', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
|
|
255
|
+
// Oh My Pi discovers Claude-shaped agent markdown from ~/.omp/agent/agents/
|
|
256
|
+
// (`omp agents unpack` writes there); frontmatter name/description + body.
|
|
257
|
+
pi: flatFile({ subdir: ['.omp', 'agent', 'agents'], ext: '.md', transform: transformSubagentForClaude, readMeta: metaFrontmatterFallback }),
|
|
255
258
|
droid: flatFile({ subdir: ['.factory', 'droids'], ext: '.md', transform: transformSubagentForDroid }),
|
|
256
259
|
// Bespoke frontmatter/format, still one flat file.
|
|
257
260
|
codex: flatFile({ subdir: ['.codex', 'agents'], ext: '.toml', transform: transformSubagentForCodex }),
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { CloudProviderId } from './cloud/types.js';
|
|
9
9
|
import type { FeedBroadcastConfig } from './feed-broadcast.js';
|
|
10
10
|
/** Unique identifier for a current or legacy AI coding agent. */
|
|
11
|
-
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes';
|
|
11
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
|
|
12
12
|
/** How `agents run <agent>` chooses an installed version when none is pinned. */
|
|
13
13
|
export type RunStrategy = 'pinned' | 'available' | 'balanced';
|
|
14
14
|
/**
|
|
@@ -196,6 +196,15 @@ export interface AgentConfig {
|
|
|
196
196
|
* rules file (inline all @-imports) when syncing it into the version home.
|
|
197
197
|
*/
|
|
198
198
|
rulesImports?: boolean;
|
|
199
|
+
/**
|
|
200
|
+
* Whether the agent can open an interactive REPL session when launched with
|
|
201
|
+
* NO prompt (bare invocation). Agents whose CLI exits immediately without a
|
|
202
|
+
* prompt (e.g. cursor-agent) must declare `false` here; agents that open a
|
|
203
|
+
* TUI/REPL with no args declare `true`. Used by the `auto` harness picker
|
|
204
|
+
* to avoid routing a prompt-less interactive run to a harness that would
|
|
205
|
+
* exit silently (RUSH-2185, EXEC-23a).
|
|
206
|
+
*/
|
|
207
|
+
interactiveRepl?: Capability;
|
|
199
208
|
};
|
|
200
209
|
}
|
|
201
210
|
/**
|
|
@@ -213,7 +222,7 @@ export type RulesCapability = false | {
|
|
|
213
222
|
file: string;
|
|
214
223
|
};
|
|
215
224
|
/** Names of every gateable capability on AgentConfig. */
|
|
216
|
-
export type CapabilityName = 'hooks' | 'mcp' | 'mcpHttp' | 'mcpHeaders' | 'allowlist' | 'skills' | 'commands' | 'plugins' | 'subagents' | 'rules' | 'workflows' | 'memory';
|
|
225
|
+
export type CapabilityName = 'hooks' | 'mcp' | 'mcpHttp' | 'mcpHeaders' | 'allowlist' | 'skills' | 'commands' | 'plugins' | 'subagents' | 'rules' | 'workflows' | 'memory' | 'interactiveRepl';
|
|
217
226
|
/**
|
|
218
227
|
* Permission modes controlling agent autonomy.
|
|
219
228
|
* plan read-only investigation; no writes, no shell side-effects
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -177,6 +177,7 @@ export declare const USAGE_FETCH_CONCURRENCY = 3;
|
|
|
177
177
|
* minutes; that re-fired too often when delayed responses stacked.
|
|
178
178
|
*/
|
|
179
179
|
export declare const USAGE_CACHE_FRESH_MS: number;
|
|
180
|
+
export declare const USAGE_CACHE_SWR_MS: number;
|
|
180
181
|
/**
|
|
181
182
|
* Unified entry for every multi-account usage lookup (`agents view`, rotation,
|
|
182
183
|
* JSON export). Deduplicates by usage identity, then fans out through the
|
|
@@ -491,6 +492,10 @@ export declare function writeClaudeUsageCache(usageKey: string, snapshot: UsageS
|
|
|
491
492
|
* Returns true only when a valid access token can be obtained.
|
|
492
493
|
*/
|
|
493
494
|
export declare function isClaudeAuthValid(home?: string): Promise<boolean>;
|
|
495
|
+
/** Render a colored block-character progress bar. */
|
|
496
|
+
export declare function renderBar(usedPercent: number, length: number, minimumVisible?: number): string;
|
|
497
|
+
/** Return a chalk color function based on the usage percentage threshold. */
|
|
498
|
+
export declare function getUsageColor(usedPercent: number): (text: string) => string;
|
|
494
499
|
/** Parse the latest billing info from Grok's unified log. */
|
|
495
500
|
declare function getGrokUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
|
|
496
501
|
/** Per-model bucket in Cursor's /api/usage response. */
|
package/dist/lib/usage.js
CHANGED
|
@@ -219,7 +219,7 @@ const USAGE_BG_REFRESH_CONCURRENCY = 2;
|
|
|
219
219
|
* minutes; that re-fired too often when delayed responses stacked.
|
|
220
220
|
*/
|
|
221
221
|
export const USAGE_CACHE_FRESH_MS = 5 * 60 * 1000;
|
|
222
|
-
const USAGE_CACHE_SWR_MS = 24 * 60 * 60 * 1000; // 24 hours — beyond this, block on live fetch.
|
|
222
|
+
export const USAGE_CACHE_SWR_MS = 24 * 60 * 60 * 1000; // 24 hours — beyond this, block on live fetch.
|
|
223
223
|
/**
|
|
224
224
|
* Unified entry for every multi-account usage lookup (`agents view`, rotation,
|
|
225
225
|
* JSON export). Deduplicates by usage identity, then fans out through the
|
|
@@ -1696,7 +1696,7 @@ function renderCompactUsageBar(usedPercent) {
|
|
|
1696
1696
|
return renderBar(usedPercent, COMPACT_BAR_LEN, usedPercent > 0 ? 1 : 0);
|
|
1697
1697
|
}
|
|
1698
1698
|
/** Render a colored block-character progress bar. */
|
|
1699
|
-
function renderBar(usedPercent, length, minimumVisible = 0) {
|
|
1699
|
+
export function renderBar(usedPercent, length, minimumVisible = 0) {
|
|
1700
1700
|
const rounded = Math.round((usedPercent / 100) * length);
|
|
1701
1701
|
const filled = Math.max(minimumVisible, Math.max(0, Math.min(length, rounded)));
|
|
1702
1702
|
const color = getUsageColor(usedPercent);
|
|
@@ -1707,7 +1707,7 @@ function colorUsage(text, usedPercent) {
|
|
|
1707
1707
|
return getUsageColor(usedPercent)(text);
|
|
1708
1708
|
}
|
|
1709
1709
|
/** Return a chalk color function based on the usage percentage threshold. */
|
|
1710
|
-
function getUsageColor(usedPercent) {
|
|
1710
|
+
export function getUsageColor(usedPercent) {
|
|
1711
1711
|
if (usedPercent >= 100)
|
|
1712
1712
|
return chalk.red;
|
|
1713
1713
|
if (usedPercent >= 80)
|