@phnx-labs/agents-cli 1.21.1 → 1.21.2
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 +172 -0
- package/README.md +1 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.js +5 -2
- package/dist/commands/feed.js +28 -19
- package/dist/commands/hooks.js +9 -45
- package/dist/commands/menubar.js +24 -24
- package/dist/commands/message.js +23 -3
- package/dist/commands/perf.d.ts +13 -0
- package/dist/commands/perf.js +80 -23
- package/dist/commands/projects.d.ts +11 -0
- package/dist/commands/projects.js +153 -21
- package/dist/commands/routines.js +46 -1
- package/dist/commands/ssh.js +69 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +158 -0
- package/dist/commands/usage.d.ts +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +90 -45
- package/dist/index.js +14 -1
- package/dist/lib/agents.js +2 -2
- package/dist/lib/analytics/dashboard.d.ts +11 -0
- package/dist/lib/analytics/dashboard.js +31 -0
- package/dist/lib/analytics/recipes.d.ts +32 -0
- package/dist/lib/analytics/recipes.js +316 -0
- package/dist/lib/analytics/usage-db.d.ts +84 -0
- package/dist/lib/analytics/usage-db.js +301 -0
- package/dist/lib/browser/service.js +18 -0
- package/dist/lib/cli-resources.d.ts +20 -0
- package/dist/lib/cli-resources.js +48 -1
- package/dist/lib/daemon.js +51 -14
- package/dist/lib/devices/health-report.d.ts +5 -0
- package/dist/lib/devices/health-report.js +3 -0
- package/dist/lib/feed-broadcast.d.ts +52 -7
- package/dist/lib/feed-broadcast.js +125 -18
- package/dist/lib/fleet-cache.d.ts +37 -0
- package/dist/lib/fleet-cache.js +40 -0
- package/dist/lib/fleet-status.d.ts +53 -0
- package/dist/lib/fleet-status.js +120 -0
- package/dist/lib/friction-heuristics.d.ts +32 -0
- package/dist/lib/friction-heuristics.js +47 -0
- package/dist/lib/hooks/cache.js +28 -6
- package/dist/lib/hooks/profile.d.ts +8 -0
- package/dist/lib/hooks/profile.js +14 -4
- package/dist/lib/hooks.js +72 -17
- package/dist/lib/linear-cache.d.ts +63 -0
- package/dist/lib/linear-cache.js +146 -0
- package/dist/lib/linear-project-counts.d.ts +35 -5
- package/dist/lib/linear-project-counts.js +61 -16
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +7 -0
- package/dist/lib/menubar/install-menubar.js +36 -6
- package/dist/lib/perf/db.d.ts +6 -1
- package/dist/lib/perf/db.js +35 -5
- package/dist/lib/perf/types.d.ts +10 -0
- package/dist/lib/project-doctor.d.ts +36 -0
- package/dist/lib/project-doctor.js +45 -0
- package/dist/lib/project-import.d.ts +11 -1
- package/dist/lib/project-import.js +17 -3
- package/dist/lib/project-status.d.ts +25 -5
- package/dist/lib/project-status.js +48 -6
- package/dist/lib/rotate.d.ts +27 -0
- package/dist/lib/rotate.js +44 -17
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +39 -0
- package/dist/lib/runner.js +34 -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/usage-db.d.ts +3 -63
- package/dist/lib/secrets/usage-db.js +46 -186
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +14 -3
- package/dist/lib/session/discover.d.ts +3 -0
- package/dist/lib/session/discover.js +8 -0
- package/dist/lib/session/types.d.ts +1 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +31 -3
- package/dist/lib/state.js +53 -10
- package/dist/lib/types.d.ts +8 -4
- package/dist/lib/usage-refresh.d.ts +106 -0
- package/dist/lib/usage-refresh.js +238 -0
- package/dist/lib/usage.d.ts +152 -17
- package/dist/lib/usage.js +393 -79
- package/package.json +1 -1
package/dist/lib/routines.js
CHANGED
|
@@ -964,6 +964,45 @@ export function getLatestCompletedRun(jobName) {
|
|
|
964
964
|
}
|
|
965
965
|
return null;
|
|
966
966
|
}
|
|
967
|
+
/** Percentile of a sorted-ascending array (linear interpolation). p in [0,100]. */
|
|
968
|
+
function percentile(sorted, p) {
|
|
969
|
+
if (sorted.length === 0)
|
|
970
|
+
return 0;
|
|
971
|
+
if (sorted.length === 1)
|
|
972
|
+
return sorted[0];
|
|
973
|
+
const rank = (p / 100) * (sorted.length - 1);
|
|
974
|
+
const lo = Math.floor(rank);
|
|
975
|
+
const hi = Math.ceil(rank);
|
|
976
|
+
if (lo === hi)
|
|
977
|
+
return sorted[lo];
|
|
978
|
+
const frac = rank - lo;
|
|
979
|
+
return sorted[lo] * (1 - frac) + sorted[hi] * frac;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Fold a job's run history (`listRuns`) into a duration + outcome summary.
|
|
983
|
+
* `missed` fires (no process ever ran) carry no `duration` and are excluded
|
|
984
|
+
* from the latency percentiles but still counted in `count`/`missed`.
|
|
985
|
+
*/
|
|
986
|
+
export function routineStats(jobName) {
|
|
987
|
+
const runs = listRuns(jobName);
|
|
988
|
+
const failed = runs.filter((r) => r.status === 'failed' || r.status === 'timeout').length;
|
|
989
|
+
const missed = runs.filter((r) => r.status === 'missed').length;
|
|
990
|
+
const durations = runs
|
|
991
|
+
.map((r) => r.duration)
|
|
992
|
+
.filter((d) => typeof d === 'number' && Number.isFinite(d))
|
|
993
|
+
.sort((a, b) => a - b);
|
|
994
|
+
const avgMs = durations.length > 0
|
|
995
|
+
? Math.round(durations.reduce((sum, d) => sum + d, 0) / durations.length)
|
|
996
|
+
: 0;
|
|
997
|
+
return {
|
|
998
|
+
count: runs.length,
|
|
999
|
+
failed,
|
|
1000
|
+
missed,
|
|
1001
|
+
avgMs,
|
|
1002
|
+
p50: Math.round(percentile(durations, 50)),
|
|
1003
|
+
p95: Math.round(percentile(durations, 95)),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
967
1006
|
/** Persist run metadata to its run directory as meta.json. */
|
|
968
1007
|
export function writeRunMeta(meta) {
|
|
969
1008
|
ensureAgentsDir();
|
package/dist/lib/runner.js
CHANGED
|
@@ -46,10 +46,28 @@ const AGENT_COMMANDS = {
|
|
|
46
46
|
/** Agents the daemon can actually run, derived from the command table above
|
|
47
47
|
* so the `--agent` help and any validation can never drift from it. */
|
|
48
48
|
export const ROUTINE_AGENT_IDS = Object.freeze(Object.keys(AGENT_COMMANDS));
|
|
49
|
+
/**
|
|
50
|
+
* Where each agent's transcript files live under an overlay HOME, mirroring
|
|
51
|
+
* `SESSION_ROOT_SPECS` (session/discover.ts) — the CLI's own source of truth
|
|
52
|
+
* for which on-disk trees hold live session files. Kept in this shape (not a
|
|
53
|
+
* shared import) because `archiveRoutineTranscripts` only needs a flat
|
|
54
|
+
* root+ext pair to `walkForFiles`, not the version-home/backup fan-out
|
|
55
|
+
* `getAgentSessionDirs` does for live discovery.
|
|
56
|
+
*
|
|
57
|
+
* `opencode` is deliberately absent: `SESSION_ROOT_SPECS` itself has no entry
|
|
58
|
+
* for it — its transcripts live in one incrementally-scanned SQLite db
|
|
59
|
+
* (`scanOpenCodeIncremental`), not a per-session file tree — so there is
|
|
60
|
+
* nothing here to mirror without inventing a new discovery path.
|
|
61
|
+
*/
|
|
49
62
|
const ROUTINE_TRANSCRIPT_SPECS = {
|
|
50
63
|
claude: [{ root: ['.claude', 'projects'], ext: '.jsonl' }],
|
|
51
64
|
codex: [{ root: ['.codex', 'sessions'], ext: '.jsonl' }],
|
|
52
65
|
cursor: [{ root: ['.cursor', 'projects'], ext: '.jsonl' }],
|
|
66
|
+
gemini: [{ root: ['.gemini', 'tmp'], ext: '.json' }],
|
|
67
|
+
antigravity: [{ root: ['.gemini', 'antigravity-cli', 'conversations'], ext: '.db' }],
|
|
68
|
+
droid: [{ root: ['.factory', 'sessions'], ext: '.jsonl' }],
|
|
69
|
+
kimi: [{ root: ['.kimi-code', 'sessions'], ext: '.json' }],
|
|
70
|
+
grok: [{ root: ['.grok', 'sessions'], ext: '.json' }],
|
|
53
71
|
};
|
|
54
72
|
/** Stable working directory for routine children, independent of the daemon's launch cwd. */
|
|
55
73
|
export function routineSpawnCwd(config, configuredRoot = getProjectRoot()) {
|
|
@@ -1080,6 +1098,14 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1080
1098
|
// wait); the next schedule tick re-selects if this attempt still fails.
|
|
1081
1099
|
const launch = await resolveRoutineLaunch(config);
|
|
1082
1100
|
const version = launch.chain[0]?.version ?? config.version;
|
|
1101
|
+
const timer = createTimer('agent.run', {
|
|
1102
|
+
agent: config.agent,
|
|
1103
|
+
version,
|
|
1104
|
+
jobName: config.name,
|
|
1105
|
+
mode: config.mode,
|
|
1106
|
+
...redactPrompt(config.prompt),
|
|
1107
|
+
schedule: config.schedule,
|
|
1108
|
+
});
|
|
1083
1109
|
const resolvedPrompt = resolveJobPrompt(config);
|
|
1084
1110
|
let cmd = buildJobCommand(config, resolvedPrompt);
|
|
1085
1111
|
// workflow AND resume dispatch through `agents run` — never binary-pin them (pinning
|
|
@@ -1143,6 +1169,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1143
1169
|
finalizeRunMeta(meta, 'failed', 1, { errorMessage: reason });
|
|
1144
1170
|
writeRunMeta(meta);
|
|
1145
1171
|
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1172
|
+
timer.end({ status: 'failed', exitCode: 1, runId, error: reason });
|
|
1146
1173
|
return meta;
|
|
1147
1174
|
}
|
|
1148
1175
|
}
|
|
@@ -1164,6 +1191,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1164
1191
|
const isAuthFailure = !!errorMessage && errorMessage.startsWith('auth_failed:');
|
|
1165
1192
|
if (status !== 'timeout' && !isAuthFailure)
|
|
1166
1193
|
extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
1194
|
+
timer.end({ status, exitCode: exitCode ?? undefined, runId, ...(errorMessage ? { error: errorMessage } : {}) });
|
|
1167
1195
|
// Fire the finish/output notification AFTER the report is written so the hook
|
|
1168
1196
|
// can read report.md (RUSH-2030). Best-effort; never breaks finalization.
|
|
1169
1197
|
safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
|
|
@@ -1214,6 +1242,11 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1214
1242
|
* `monitorRunningJobs` reaps the record on the next tick.
|
|
1215
1243
|
*/
|
|
1216
1244
|
function executeCommandJobDetached(config, hooks) {
|
|
1245
|
+
const timer = createTimer('agent.run', {
|
|
1246
|
+
jobName: config.name,
|
|
1247
|
+
mode: config.mode,
|
|
1248
|
+
schedule: config.schedule,
|
|
1249
|
+
});
|
|
1217
1250
|
const runId = generateRunId();
|
|
1218
1251
|
const runDir = getRunDir(config.name, runId);
|
|
1219
1252
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -1259,6 +1292,7 @@ function executeCommandJobDetached(config, hooks) {
|
|
|
1259
1292
|
settled = true;
|
|
1260
1293
|
finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
|
|
1261
1294
|
writeRunMeta(meta);
|
|
1295
|
+
timer.end({ status, exitCode, runId, ...(errorMessage ? { error: errorMessage } : {}) });
|
|
1262
1296
|
// Finish notification (RUSH-2030). For command routines the threshold only
|
|
1263
1297
|
// surfaces failures, decided in routine-notify.ts. Best-effort.
|
|
1264
1298
|
safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
|
|
Binary file
|
|
Binary file
|
|
@@ -1,69 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* value-free row for every secret lifecycle/access event a bundle accrues over
|
|
6
|
-
* its life — created, imported, exported, viewed, accessed (read for injection),
|
|
7
|
-
* unlocked. It is the queryable, per-bundle counterpart to the append-only
|
|
8
|
-
* ~/.agents/events.jsonl audit log: the SAME chokepoint (`emitSecretAudit`,
|
|
9
|
-
* lib/secrets/audit.ts) feeds both, and this store answers "how often / how
|
|
10
|
-
* recently / by whom was THIS bundle used?" without scanning the whole event
|
|
11
|
-
* stream. It is a DERIVED index fed off the real access flow — the way
|
|
12
|
-
* sessions.db indexes session metadata — never a second write path an operation
|
|
13
|
-
* has to remember to call.
|
|
14
|
-
*
|
|
15
|
-
* Contract, mirroring the audit log: NEVER a secret value. Only metadata — the
|
|
16
|
-
* bundle name, the event kind, key counts, the resolving agent/host, a status.
|
|
17
|
-
*
|
|
18
|
-
* Every write is best-effort: a failure here (missing runtime SQLite, a locked
|
|
19
|
-
* db, a read-only fs) is swallowed so usage telemetry can never break secret
|
|
20
|
-
* resolution. Set AGENTS_NO_USAGE_TRACK=1 to disable recording entirely (used by
|
|
21
|
-
* tests and by callers that must stay perfectly silent).
|
|
2
|
+
* Secrets usage read-model — thin adapter over the analytics usage warehouse
|
|
3
|
+
* (`kind=secret`). Kept so `secrets view` / `list` / `activity` keep their API
|
|
4
|
+
* while the durable store lives at ~/.agents/.history/analytics/usage.db.
|
|
22
5
|
*/
|
|
23
|
-
/** The lifecycle/access events a bundle accrues over its life. */
|
|
24
6
|
export type SecretUsageEvent = 'access' | 'unlock' | 'import' | 'export' | 'create' | 'view';
|
|
25
|
-
/** All event kinds, in the order `view` prints them. */
|
|
26
7
|
export declare const SECRET_USAGE_EVENTS: readonly SecretUsageEvent[];
|
|
27
8
|
export interface RecordUsageParams {
|
|
28
|
-
/** Bundle the event applies to (required — usage is always per-bundle). */
|
|
29
9
|
bundle: string;
|
|
30
|
-
/** What happened. */
|
|
31
10
|
event: SecretUsageEvent;
|
|
32
|
-
/** Resolving agent/harness identity, when known (`*` = a global grant). */
|
|
33
11
|
agent?: string;
|
|
34
|
-
/** Remote host the value was pulled from / pushed to, when applicable. */
|
|
35
12
|
host?: string;
|
|
36
|
-
/** Free-form origin label, e.g. 'agent', 'reveal', 'ssh', '1password'. */
|
|
37
13
|
source?: string;
|
|
38
|
-
/** Outcome; defaults to 'success'. */
|
|
39
14
|
status?: 'success' | 'error';
|
|
40
|
-
/** How many keys the event touched (names only are ever known here). */
|
|
41
15
|
keyCount?: number;
|
|
42
16
|
}
|
|
43
|
-
/** One event kind's rollup for a bundle. */
|
|
44
17
|
export interface UsageStat {
|
|
45
18
|
count: number;
|
|
46
|
-
/** ISO 8601 timestamp of the most recent occurrence, or null if never. */
|
|
47
19
|
last: string | null;
|
|
48
20
|
}
|
|
49
|
-
/** Per-bundle usage summary for the `view` / `list` surfaces. */
|
|
50
21
|
export interface BundleUsageSummary {
|
|
51
22
|
bundle: string;
|
|
52
|
-
/** Every recorded event, all kinds. */
|
|
53
23
|
total: number;
|
|
54
|
-
/** Rollup per event kind (every kind present, zeroed when unused). */
|
|
55
24
|
events: Record<SecretUsageEvent, UsageStat>;
|
|
56
|
-
/** Most recent event across all kinds, or null. */
|
|
57
25
|
lastUsedAt: string | null;
|
|
58
|
-
/** Earliest event across all kinds, or null. */
|
|
59
26
|
firstUsedAt: string | null;
|
|
60
|
-
/** Event count grouped by resolving agent, most-first. `*` = a global grant. */
|
|
61
27
|
byAgent: Array<{
|
|
62
28
|
agent: string;
|
|
63
29
|
count: number;
|
|
64
30
|
}>;
|
|
65
31
|
}
|
|
66
|
-
/** One recorded event, for the `secrets activity` timeline. */
|
|
67
32
|
export interface SecretUsageHistoryEntry {
|
|
68
33
|
ts: string;
|
|
69
34
|
bundle: string;
|
|
@@ -74,33 +39,8 @@ export interface SecretUsageHistoryEntry {
|
|
|
74
39
|
status: string | null;
|
|
75
40
|
keyCount: number | null;
|
|
76
41
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Record one usage event. Best-effort and value-free — swallows every error and
|
|
79
|
-
* honors AGENTS_NO_USAGE_TRACK so telemetry never blocks or slows a read. Rows
|
|
80
|
-
* with an empty bundle name are ignored (usage is per-bundle by definition).
|
|
81
|
-
*
|
|
82
|
-
* This is called from ONE place only — `emitSecretAudit` (lib/secrets/audit.ts)
|
|
83
|
-
* — so every recorded event has already been written to the events.jsonl audit
|
|
84
|
-
* log through the same chokepoint. Do not call it from a command handler; emit
|
|
85
|
-
* the audit event instead.
|
|
86
|
-
*/
|
|
87
42
|
export declare function recordSecretUsage(p: RecordUsageParams): void;
|
|
88
|
-
/**
|
|
89
|
-
* Usage summary for one bundle, or undefined when nothing has ever been
|
|
90
|
-
* recorded (or the DB is unavailable). Never throws.
|
|
91
|
-
*/
|
|
92
43
|
export declare function getBundleUsage(bundle: string): BundleUsageSummary | undefined;
|
|
93
|
-
/**
|
|
94
|
-
* Usage summaries for every bundle that has any recorded event, keyed by bundle
|
|
95
|
-
* name. Powers `secrets list --sort uses|used`. Empty map when the DB is
|
|
96
|
-
* unavailable or has no rows. Never throws.
|
|
97
|
-
*/
|
|
98
44
|
export declare function getAllBundleUsage(): Map<string, BundleUsageSummary>;
|
|
99
|
-
/**
|
|
100
|
-
* Recent events for the `secrets activity` timeline — one bundle when named,
|
|
101
|
-
* else across all bundles — newest first. Empty when the DB is unavailable.
|
|
102
|
-
* Never throws.
|
|
103
|
-
*/
|
|
104
45
|
export declare function getUsageHistory(bundle: string | undefined, limit?: number): SecretUsageHistoryEntry[];
|
|
105
|
-
/** Close the cached handle. Used by tests between temp-db swaps. */
|
|
106
46
|
export declare function closeSecretsUsageDb(): void;
|
|
@@ -1,30 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* value-free row for every secret lifecycle/access event a bundle accrues over
|
|
6
|
-
* its life — created, imported, exported, viewed, accessed (read for injection),
|
|
7
|
-
* unlocked. It is the queryable, per-bundle counterpart to the append-only
|
|
8
|
-
* ~/.agents/events.jsonl audit log: the SAME chokepoint (`emitSecretAudit`,
|
|
9
|
-
* lib/secrets/audit.ts) feeds both, and this store answers "how often / how
|
|
10
|
-
* recently / by whom was THIS bundle used?" without scanning the whole event
|
|
11
|
-
* stream. It is a DERIVED index fed off the real access flow — the way
|
|
12
|
-
* sessions.db indexes session metadata — never a second write path an operation
|
|
13
|
-
* has to remember to call.
|
|
14
|
-
*
|
|
15
|
-
* Contract, mirroring the audit log: NEVER a secret value. Only metadata — the
|
|
16
|
-
* bundle name, the event kind, key counts, the resolving agent/host, a status.
|
|
17
|
-
*
|
|
18
|
-
* Every write is best-effort: a failure here (missing runtime SQLite, a locked
|
|
19
|
-
* db, a read-only fs) is swallowed so usage telemetry can never break secret
|
|
20
|
-
* resolution. Set AGENTS_NO_USAGE_TRACK=1 to disable recording entirely (used by
|
|
21
|
-
* tests and by callers that must stay perfectly silent).
|
|
2
|
+
* Secrets usage read-model — thin adapter over the analytics usage warehouse
|
|
3
|
+
* (`kind=secret`). Kept so `secrets view` / `list` / `activity` keep their API
|
|
4
|
+
* while the durable store lives at ~/.agents/.history/analytics/usage.db.
|
|
22
5
|
*/
|
|
23
|
-
import
|
|
24
|
-
import * as path from 'path';
|
|
25
|
-
import Database from '../sqlite.js';
|
|
26
|
-
import { getSecretsDbPath } from '../state.js';
|
|
27
|
-
/** All event kinds, in the order `view` prints them. */
|
|
6
|
+
import { recordUsage, getSecretBundleRollup, getSecretBundleAgents, getAllSecretBundleRollups, getSecretHistory, closeUsageDb, } from '../analytics/usage-db.js';
|
|
28
7
|
export const SECRET_USAGE_EVENTS = [
|
|
29
8
|
'access',
|
|
30
9
|
'unlock',
|
|
@@ -33,28 +12,6 @@ export const SECRET_USAGE_EVENTS = [
|
|
|
33
12
|
'create',
|
|
34
13
|
'view',
|
|
35
14
|
];
|
|
36
|
-
/** Events older than this are pruned on open so the history table stays bounded. */
|
|
37
|
-
const EVENT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
38
|
-
const SCHEMA = `
|
|
39
|
-
CREATE TABLE IF NOT EXISTS usage_events (
|
|
40
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
41
|
-
ts TEXT NOT NULL,
|
|
42
|
-
bundle TEXT NOT NULL,
|
|
43
|
-
event TEXT NOT NULL,
|
|
44
|
-
agent TEXT,
|
|
45
|
-
host TEXT,
|
|
46
|
-
source TEXT,
|
|
47
|
-
status TEXT,
|
|
48
|
-
key_count INTEGER
|
|
49
|
-
);
|
|
50
|
-
CREATE INDEX IF NOT EXISTS idx_usage_bundle ON usage_events(bundle);
|
|
51
|
-
CREATE INDEX IF NOT EXISTS idx_usage_bundle_event ON usage_events(bundle, event);
|
|
52
|
-
CREATE INDEX IF NOT EXISTS idx_usage_ts ON usage_events(ts DESC);
|
|
53
|
-
`;
|
|
54
|
-
// Cached handle keyed by the resolved path, so a test that redirects
|
|
55
|
-
// AGENTS_SECRETS_DB to a fresh temp file transparently reopens instead of
|
|
56
|
-
// reusing a stale handle pointed at the previous path.
|
|
57
|
-
let cached = null;
|
|
58
15
|
function emptyEvents() {
|
|
59
16
|
return {
|
|
60
17
|
access: { count: 0, last: null },
|
|
@@ -65,68 +22,6 @@ function emptyEvents() {
|
|
|
65
22
|
view: { count: 0, last: null },
|
|
66
23
|
};
|
|
67
24
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Open (creating if needed) the usage DB, returning null on any failure so
|
|
70
|
-
* every caller degrades to a no-op rather than throwing into secret resolution.
|
|
71
|
-
*/
|
|
72
|
-
function open() {
|
|
73
|
-
const dbPath = getSecretsDbPath();
|
|
74
|
-
if (cached && cached.path === dbPath)
|
|
75
|
-
return cached.db;
|
|
76
|
-
if (cached) {
|
|
77
|
-
try {
|
|
78
|
-
cached.db.close();
|
|
79
|
-
}
|
|
80
|
-
catch { /* ignore */ }
|
|
81
|
-
cached = null;
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
85
|
-
const db = new Database(dbPath);
|
|
86
|
-
// WAL + a busy timeout so concurrent agent runs writing usage rows don't
|
|
87
|
-
// fail each other under load; every write is still best-effort besides.
|
|
88
|
-
db.pragma('journal_mode = WAL');
|
|
89
|
-
db.pragma('busy_timeout = 2000');
|
|
90
|
-
db.exec(SCHEMA);
|
|
91
|
-
// Bounded retention: this is a usage history for operators, not a compliance
|
|
92
|
-
// log (that is events.jsonl). Prune once per open on a 90-day window.
|
|
93
|
-
try {
|
|
94
|
-
db.prepare(`DELETE FROM usage_events WHERE ts < ?`).run(new Date(Date.now() - EVENT_RETENTION_MS).toISOString());
|
|
95
|
-
}
|
|
96
|
-
catch { /* prune is best-effort */ }
|
|
97
|
-
cached = { path: dbPath, db };
|
|
98
|
-
return db;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Record one usage event. Best-effort and value-free — swallows every error and
|
|
106
|
-
* honors AGENTS_NO_USAGE_TRACK so telemetry never blocks or slows a read. Rows
|
|
107
|
-
* with an empty bundle name are ignored (usage is per-bundle by definition).
|
|
108
|
-
*
|
|
109
|
-
* This is called from ONE place only — `emitSecretAudit` (lib/secrets/audit.ts)
|
|
110
|
-
* — so every recorded event has already been written to the events.jsonl audit
|
|
111
|
-
* log through the same chokepoint. Do not call it from a command handler; emit
|
|
112
|
-
* the audit event instead.
|
|
113
|
-
*/
|
|
114
|
-
export function recordSecretUsage(p) {
|
|
115
|
-
if (process.env.AGENTS_NO_USAGE_TRACK)
|
|
116
|
-
return;
|
|
117
|
-
if (!p.bundle)
|
|
118
|
-
return;
|
|
119
|
-
const db = open();
|
|
120
|
-
if (!db)
|
|
121
|
-
return;
|
|
122
|
-
try {
|
|
123
|
-
db.prepare(`INSERT INTO usage_events (ts, bundle, event, agent, host, source, status, key_count)
|
|
124
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(new Date().toISOString(), p.bundle, p.event, p.agent ?? null, p.host ?? null, p.source ?? null, p.status ?? 'success', p.keyCount ?? null);
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// Telemetry must never break secret resolution.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
25
|
function toSummary(bundle, rows, byAgent) {
|
|
131
26
|
const events = emptyEvents();
|
|
132
27
|
let total = 0;
|
|
@@ -146,91 +41,56 @@ function toSummary(bundle, rows, byAgent) {
|
|
|
146
41
|
}
|
|
147
42
|
return { bundle, total, events, lastUsedAt, firstUsedAt, byAgent };
|
|
148
43
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
44
|
+
export function recordSecretUsage(p) {
|
|
45
|
+
if (!p.bundle)
|
|
46
|
+
return;
|
|
47
|
+
const meta = {};
|
|
48
|
+
if (p.keyCount != null)
|
|
49
|
+
meta.keyCount = p.keyCount;
|
|
50
|
+
if (p.host)
|
|
51
|
+
meta.host = p.host;
|
|
52
|
+
recordUsage({
|
|
53
|
+
kind: 'secret',
|
|
54
|
+
name: p.bundle,
|
|
55
|
+
event: p.event,
|
|
56
|
+
agent: p.agent,
|
|
57
|
+
source: p.source,
|
|
58
|
+
status: p.status,
|
|
59
|
+
meta: Object.keys(meta).length ? meta : undefined,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
153
62
|
export function getBundleUsage(bundle) {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
63
|
+
const rows = getSecretBundleRollup(bundle);
|
|
64
|
+
if (rows.length === 0)
|
|
156
65
|
return undefined;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
.prepare(`SELECT event, COUNT(*) AS n, MAX(ts) AS last, MIN(ts) AS first
|
|
160
|
-
FROM usage_events WHERE bundle = ? GROUP BY event`)
|
|
161
|
-
.all(bundle);
|
|
162
|
-
if (rows.length === 0)
|
|
163
|
-
return undefined;
|
|
164
|
-
const agents = db
|
|
165
|
-
.prepare(`SELECT agent, COUNT(*) AS n FROM usage_events
|
|
166
|
-
WHERE bundle = ? AND agent IS NOT NULL GROUP BY agent ORDER BY n DESC`)
|
|
167
|
-
.all(bundle);
|
|
168
|
-
return toSummary(bundle, rows, agents.map((a) => ({ agent: a.agent, count: a.n })));
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
66
|
+
const agents = getSecretBundleAgents(bundle);
|
|
67
|
+
return toSummary(bundle, rows, agents.map((a) => ({ agent: a.agent, count: a.n })));
|
|
173
68
|
}
|
|
174
|
-
/**
|
|
175
|
-
* Usage summaries for every bundle that has any recorded event, keyed by bundle
|
|
176
|
-
* name. Powers `secrets list --sort uses|used`. Empty map when the DB is
|
|
177
|
-
* unavailable or has no rows. Never throws.
|
|
178
|
-
*/
|
|
179
69
|
export function getAllBundleUsage() {
|
|
180
70
|
const out = new Map();
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
FROM usage_events GROUP BY bundle, event`)
|
|
188
|
-
.all();
|
|
189
|
-
const byBundle = new Map();
|
|
190
|
-
for (const r of rows) {
|
|
191
|
-
const list = byBundle.get(r.bundle) ?? [];
|
|
192
|
-
list.push(r);
|
|
193
|
-
byBundle.set(r.bundle, list);
|
|
194
|
-
}
|
|
195
|
-
for (const [bundle, list] of byBundle)
|
|
196
|
-
out.set(bundle, toSummary(bundle, list, []));
|
|
197
|
-
return out;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
return out;
|
|
71
|
+
const rows = getAllSecretBundleRollups();
|
|
72
|
+
const byBundle = new Map();
|
|
73
|
+
for (const r of rows) {
|
|
74
|
+
const list = byBundle.get(r.name) ?? [];
|
|
75
|
+
list.push(r);
|
|
76
|
+
byBundle.set(r.name, list);
|
|
201
77
|
}
|
|
78
|
+
for (const [bundle, list] of byBundle)
|
|
79
|
+
out.set(bundle, toSummary(bundle, list, []));
|
|
80
|
+
return out;
|
|
202
81
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Recent events for the `secrets activity` timeline — one bundle when named,
|
|
205
|
-
* else across all bundles — newest first. Empty when the DB is unavailable.
|
|
206
|
-
* Never throws.
|
|
207
|
-
*/
|
|
208
82
|
export function getUsageHistory(bundle, limit = 20) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
? db.prepare(sql).all(bundle, limit)
|
|
220
|
-
: db.prepare(sql).all(limit);
|
|
221
|
-
return rows;
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
return [];
|
|
225
|
-
}
|
|
83
|
+
return getSecretHistory(bundle, limit).map((r) => ({
|
|
84
|
+
ts: r.ts,
|
|
85
|
+
bundle: r.bundle,
|
|
86
|
+
event: r.event,
|
|
87
|
+
agent: r.agent,
|
|
88
|
+
host: r.host,
|
|
89
|
+
source: r.source,
|
|
90
|
+
status: r.status,
|
|
91
|
+
keyCount: r.keyCount,
|
|
92
|
+
}));
|
|
226
93
|
}
|
|
227
|
-
/** Close the cached handle. Used by tests between temp-db swaps. */
|
|
228
94
|
export function closeSecretsUsageDb() {
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
cached.db.close();
|
|
232
|
-
}
|
|
233
|
-
catch { /* ignore */ }
|
|
234
|
-
cached = null;
|
|
235
|
-
}
|
|
95
|
+
closeUsageDb();
|
|
236
96
|
}
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { SessionAgentId, SessionMeta } from './types.js';
|
|
|
11
11
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
12
12
|
* assert against the constant instead of hardcoding a number that every bump
|
|
13
13
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
14
|
-
export declare const SCHEMA_VERSION =
|
|
14
|
+
export declare const SCHEMA_VERSION = 22;
|
|
15
15
|
/** Raw row shape returned from the sessions table. */
|
|
16
16
|
export interface SessionRow {
|
|
17
17
|
id: string;
|
|
@@ -35,6 +35,7 @@ export interface SessionRow {
|
|
|
35
35
|
cost_usd: number | null;
|
|
36
36
|
duration_ms: number | null;
|
|
37
37
|
model: string | null;
|
|
38
|
+
tool_call_count: number | null;
|
|
38
39
|
file_path: string;
|
|
39
40
|
file_mtime_ms: number | null;
|
|
40
41
|
file_size: number | null;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -19,7 +19,7 @@ const DB_PATH = getSessionsDbPath();
|
|
|
19
19
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
20
20
|
* assert against the constant instead of hardcoding a number that every bump
|
|
21
21
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
22
|
-
export const SCHEMA_VERSION =
|
|
22
|
+
export const SCHEMA_VERSION = 22;
|
|
23
23
|
/**
|
|
24
24
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
25
25
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
67
67
|
cost_usd REAL,
|
|
68
68
|
duration_ms INTEGER,
|
|
69
69
|
model TEXT,
|
|
70
|
+
tool_call_count INTEGER,
|
|
70
71
|
file_path TEXT NOT NULL,
|
|
71
72
|
file_mtime_ms INTEGER,
|
|
72
73
|
file_size INTEGER,
|
|
@@ -399,6 +400,12 @@ function migrateSchema(db, fromVersion) {
|
|
|
399
400
|
db.exec(`ALTER TABLE sessions ADD COLUMN spawned_team TEXT`);
|
|
400
401
|
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
401
402
|
}
|
|
403
|
+
if (fromVersion < 22) {
|
|
404
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
405
|
+
if (!cols.some(c => c.name === 'tool_call_count'))
|
|
406
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN tool_call_count INTEGER`);
|
|
407
|
+
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
408
|
+
}
|
|
402
409
|
}
|
|
403
410
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
404
411
|
export function getDB() {
|
|
@@ -758,7 +765,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
758
765
|
id, short_id, agent, origin, routine_name, routine_run_id,
|
|
759
766
|
version, account, timestamp, last_activity,
|
|
760
767
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
761
|
-
output_tokens, cost_usd, duration_ms, model,
|
|
768
|
+
output_tokens, cost_usd, duration_ms, model, tool_call_count,
|
|
762
769
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
763
770
|
pr_url, pr_number, worktree_slug, ticket_id, spawned_team, plan, todos,
|
|
764
771
|
recent_directories_touched, linear_project, linear_project_url, machine,
|
|
@@ -767,7 +774,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
767
774
|
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
768
775
|
@version, @account, @timestamp, @last_activity,
|
|
769
776
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
770
|
-
@output_tokens, @cost_usd, @duration_ms, @model,
|
|
777
|
+
@output_tokens, @cost_usd, @duration_ms, @model, @tool_call_count,
|
|
771
778
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
772
779
|
@pr_url, @pr_number, @worktree_slug, @ticket_id, @spawned_team, @plan, @todos,
|
|
773
780
|
@recent_directories_touched, @linear_project, @linear_project_url, @machine,
|
|
@@ -802,6 +809,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
802
809
|
cost_usd = excluded.cost_usd,
|
|
803
810
|
duration_ms = excluded.duration_ms,
|
|
804
811
|
model = excluded.model,
|
|
812
|
+
tool_call_count = excluded.tool_call_count,
|
|
805
813
|
file_path = excluded.file_path,
|
|
806
814
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
807
815
|
file_size = excluded.file_size,
|
|
@@ -919,6 +927,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
919
927
|
cost_usd: meta.costUsd ?? null,
|
|
920
928
|
duration_ms: meta.durationMs ?? null,
|
|
921
929
|
model: meta.model ?? null,
|
|
930
|
+
tool_call_count: meta.toolCallCount ?? null,
|
|
922
931
|
file_path: meta.filePath,
|
|
923
932
|
file_mtime_ms: scan?.fileMtimeMs ?? null,
|
|
924
933
|
file_size: scan?.fileSize ?? null,
|
|
@@ -1040,6 +1049,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
1040
1049
|
cost_usd: meta.costUsd ?? null,
|
|
1041
1050
|
duration_ms: meta.durationMs ?? null,
|
|
1042
1051
|
model: meta.model ?? null,
|
|
1052
|
+
tool_call_count: meta.toolCallCount ?? null,
|
|
1043
1053
|
file_path: meta.filePath,
|
|
1044
1054
|
file_mtime_ms: scan?.fileMtimeMs ?? null,
|
|
1045
1055
|
file_size: scan?.fileSize ?? null,
|
|
@@ -1229,6 +1239,7 @@ function rowToMeta(row) {
|
|
|
1229
1239
|
costUsd: row.cost_usd ?? undefined,
|
|
1230
1240
|
durationMs: row.duration_ms ?? undefined,
|
|
1231
1241
|
model: row.model ?? undefined,
|
|
1242
|
+
toolCallCount: row.tool_call_count ?? undefined,
|
|
1232
1243
|
version: row.version ?? undefined,
|
|
1233
1244
|
account: row.account ?? undefined,
|
|
1234
1245
|
topic: row.topic ?? undefined,
|
|
@@ -66,6 +66,7 @@ interface ClaudeSessionScan {
|
|
|
66
66
|
durationMs?: number;
|
|
67
67
|
/** ISO time of the last timestamped event — the session's last activity. */
|
|
68
68
|
lastActivity?: string;
|
|
69
|
+
toolCallCount?: number;
|
|
69
70
|
/**
|
|
70
71
|
* Value of the JSONL `entrypoint` field on the first event that carries it.
|
|
71
72
|
* 'cli' for real interactive sessions, 'sdk-cli' for team-spawned ones.
|
|
@@ -374,6 +375,7 @@ export interface ClaudeParseState {
|
|
|
374
375
|
aiTitle?: string;
|
|
375
376
|
entrypoint?: string;
|
|
376
377
|
messageCount: number;
|
|
378
|
+
toolCallCount: number;
|
|
377
379
|
tokenCount: number;
|
|
378
380
|
outputTokens: number;
|
|
379
381
|
sawTokenCount: boolean;
|
|
@@ -436,6 +438,7 @@ export interface ClaudeParserState {
|
|
|
436
438
|
plan?: string;
|
|
437
439
|
lastTsMs?: number;
|
|
438
440
|
messageCount: number;
|
|
441
|
+
toolCallCount: number;
|
|
439
442
|
tokenCount: number;
|
|
440
443
|
outputTokens: number;
|
|
441
444
|
sawTokenCount: boolean;
|