@agent-finops/core 0.8.1 → 0.9.0

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.
Files changed (44) hide show
  1. package/README.md +5 -3
  2. package/dist/actionPlanner.d.ts +140 -0
  3. package/dist/actionPlanner.js +938 -0
  4. package/dist/actionVerification.d.ts +1240 -0
  5. package/dist/actionVerification.js +1028 -0
  6. package/dist/activitySnapshot.d.ts +142 -50
  7. package/dist/activitySnapshot.js +145 -6
  8. package/dist/activitySnapshotCache.d.ts +8 -1
  9. package/dist/activitySnapshotCache.js +103 -7
  10. package/dist/agentEconomicsReceipt.d.ts +74 -74
  11. package/dist/glance.d.ts +27 -1
  12. package/dist/glance.js +151 -12
  13. package/dist/index.d.ts +11 -2
  14. package/dist/index.js +10 -1
  15. package/dist/localAgentFormats/gemini.js +2 -2
  16. package/dist/localAgentFormats/registry.js +6 -2
  17. package/dist/localAgentFormats/runtimeRegistry.js +5 -2
  18. package/dist/localAgentFormats/types.d.ts +2 -1
  19. package/dist/localAgentLogs.d.ts +362 -3
  20. package/dist/localAgentLogs.js +1964 -165
  21. package/dist/modelPricing.d.ts +1 -1
  22. package/dist/modelPricing.js +1 -1
  23. package/dist/projectEconomics.d.ts +617 -0
  24. package/dist/projectEconomics.js +620 -0
  25. package/dist/projectEconomicsBuilder.d.ts +89 -0
  26. package/dist/projectEconomicsBuilder.js +473 -0
  27. package/dist/projectIndexStore.d.ts +545 -0
  28. package/dist/projectIndexStore.js +606 -0
  29. package/dist/providerConnectors.d.ts +59 -1
  30. package/dist/providerConnectors.js +175 -11
  31. package/dist/qualitativeIndexCache.d.ts +494 -0
  32. package/dist/qualitativeIndexCache.js +930 -0
  33. package/dist/resultCard.d.ts +350 -0
  34. package/dist/resultCard.js +604 -0
  35. package/dist/runtimeCommands.d.ts +21 -0
  36. package/dist/runtimeCommands.js +27 -0
  37. package/dist/scanGuard.d.ts +3 -1
  38. package/dist/scanGuard.js +164 -4
  39. package/dist/schema.d.ts +31 -31
  40. package/dist/sessionVitals.d.ts +145 -0
  41. package/dist/sessionVitals.js +521 -0
  42. package/dist/toolInvocations.d.ts +40 -1
  43. package/dist/toolInvocations.js +101 -20
  44. package/package.json +1 -1
package/dist/scanGuard.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { homedir } from "node:os";
2
2
  import { constants, realpathSync } from "node:fs";
3
+ import { execFile as execFileCallback } from "node:child_process";
3
4
  import { chmod, lstat, mkdir, open, realpath, rename, stat, unlink } from "node:fs/promises";
4
5
  import { randomUUID } from "node:crypto";
5
- import { basename, join, resolve, sep } from "node:path";
6
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { promisify } from "node:util";
6
8
  /**
7
9
  * Shared unsafe-scan-root policy for EVERY scan entrypoint (CLI `scan`, MCP
8
10
  * `scan_ai_spend`, and any future surface). Scanning the home directory, the
@@ -29,6 +31,7 @@ const systemRootDirectories = new Set([
29
31
  "/sys",
30
32
  "/dev"
31
33
  ]);
34
+ const execFile = promisify(execFileCallback);
32
35
  export class UnsafeScanRootError extends Error {
33
36
  rootPath;
34
37
  constructor(rootPath, reason) {
@@ -127,9 +130,11 @@ export async function resolveSafeStateDirectory(rootPath, options = {}) {
127
130
  return undefined;
128
131
  throw error;
129
132
  });
133
+ let createdStateDirectory = false;
130
134
  if (!stateInfo && options.create) {
131
135
  try {
132
136
  await mkdir(statePath, { mode: 0o700 });
137
+ createdStateDirectory = true;
133
138
  }
134
139
  catch (error) {
135
140
  // A concurrent creator is safe only after the same lstat checks below.
@@ -153,17 +158,145 @@ export async function resolveSafeStateDirectory(rootPath, options = {}) {
153
158
  if (canonicalState !== statePath) {
154
159
  throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent does not resolve directly inside the approved root");
155
160
  }
156
- if (options.create) {
161
+ // Project state can contain private receipts and experiment evidence. A
162
+ // repository-authored marker or tracked state is not a trust boundary: the
163
+ // exact marker, private permissions, untracked status, and effective Git
164
+ // ignore rule must all be proven before either reads or writes proceed.
165
+ await assertPrivateStateGitBoundary(canonicalRoot, canonicalState, options.create === true);
166
+ const directoryMode = stateInfo.mode & 0o777;
167
+ if ((directoryMode & 0o077) !== 0) {
168
+ if (options.create !== true) {
169
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent permissions expose private state metadata to other users");
170
+ }
171
+ // Explicit create/write calls may safely migrate a legacy aibill-owned
172
+ // directory only after the Git privacy boundary above proved that neither
173
+ // it nor a child is tracked and that the exact private marker is effective.
174
+ // Read-only callers never change permissions implicitly.
175
+ await chmod(statePath, 0o700);
176
+ const securedInfo = await lstat(statePath);
177
+ if (!securedInfo.isDirectory() || securedInfo.isSymbolicLink() ||
178
+ securedInfo.dev !== stateInfo.dev || securedInfo.ino !== stateInfo.ino ||
179
+ (securedInfo.mode & 0o077) !== 0) {
180
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent permissions could not be secured without changing directory identity");
181
+ }
182
+ stateInfo = securedInfo;
183
+ }
184
+ else if (options.create && createdStateDirectory && directoryMode !== 0o700) {
185
+ // An unusually restrictive umask is still normalized for stable private
186
+ // access only on the directory this call created.
157
187
  await chmod(statePath, 0o700);
158
188
  }
159
189
  return statePath;
160
190
  }
191
+ async function assertPrivateStateGitBoundary(canonicalRoot, statePath, create) {
192
+ const gitEntryRoot = await findEnclosingGitEntryRoot(canonicalRoot);
193
+ const gitRoot = await resolveGitRoot(canonicalRoot, gitEntryRoot);
194
+ // Outside a Git worktree, the state files' own 0600 permissions and safe
195
+ // no-follow I/O are the privacy boundary. The ignore marker is specifically
196
+ // a repository-leak guard and is required only when Git can see this root.
197
+ if (!gitRoot)
198
+ return;
199
+ const relativeState = relative(gitRoot, statePath);
200
+ if (!relativeState || relativeState === ".." || relativeState.startsWith(`..${sep}`) ||
201
+ isAbsolute(relativeState)) {
202
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent is outside the resolved Git worktree");
203
+ }
204
+ const tracked = await execFile("git", ["-C", gitRoot, "ls-files", "--", relativeState], { encoding: "utf8", maxBuffer: 64 * 1024 }).then(({ stdout }) => stdout.trim()).catch(() => {
205
+ throw new UnsafeStateDirectoryError(statePath, "Git tracking status for .ai-spend-agent could not be verified");
206
+ });
207
+ if (tracked) {
208
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent or one of its children is already tracked by Git");
209
+ }
210
+ const markerPath = join(statePath, ".gitignore");
211
+ let handle;
212
+ try {
213
+ handle = await open(markerPath, constants.O_RDONLY | noFollowFlag());
214
+ }
215
+ catch (error) {
216
+ if (!isNodeError(error, "ENOENT") || !create) {
217
+ if (isNodeError(error, "ELOOP")) {
218
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent/.gitignore is a symbolic link");
219
+ }
220
+ throw error;
221
+ }
222
+ let writer;
223
+ try {
224
+ writer = await open(markerPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), 0o600);
225
+ await writer.writeFile("*\n", "utf8");
226
+ await writer.sync();
227
+ }
228
+ catch (createError) {
229
+ if (isNodeError(createError, "ELOOP")) {
230
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent/.gitignore is a symbolic link");
231
+ }
232
+ if (!isNodeError(createError, "EEXIST"))
233
+ throw createError;
234
+ }
235
+ finally {
236
+ await writer?.close().catch(() => undefined);
237
+ }
238
+ handle = await open(markerPath, constants.O_RDONLY | noFollowFlag());
239
+ }
240
+ try {
241
+ const info = await handle.stat();
242
+ if (!info.isFile() || (info.mode & 0o077) !== 0 || info.size !== 2) {
243
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent/.gitignore is not the private aibill marker");
244
+ }
245
+ const bytes = Buffer.alloc(2);
246
+ const { bytesRead } = await handle.read(bytes, 0, 2, 0);
247
+ if (bytesRead !== 2 || bytes.toString("utf8") !== "*\n") {
248
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent/.gitignore is not the exact private aibill marker");
249
+ }
250
+ }
251
+ finally {
252
+ await handle.close().catch(() => undefined);
253
+ }
254
+ const relativeProbe = join(relativeState, "privacy-probe.json");
255
+ const ignored = await execFile("git", ["-C", gitRoot, "check-ignore", "--quiet", "--no-index", "--", relativeProbe], { maxBuffer: 64 * 1024 }).then(() => true).catch(() => false);
256
+ if (!ignored) {
257
+ throw new UnsafeStateDirectoryError(statePath, ".ai-spend-agent is not proven ignored by Git");
258
+ }
259
+ }
260
+ async function findEnclosingGitEntryRoot(path) {
261
+ let current = resolve(path);
262
+ while (true) {
263
+ const entry = await lstat(join(current, ".git")).catch((error) => {
264
+ if (isNodeError(error, "ENOENT") || isNodeError(error, "ENOTDIR"))
265
+ return undefined;
266
+ throw error;
267
+ });
268
+ if (entry)
269
+ return current;
270
+ const parent = dirname(current);
271
+ if (parent === current)
272
+ return undefined;
273
+ current = parent;
274
+ }
275
+ }
276
+ async function resolveGitRoot(canonicalRoot, gitEntryRoot) {
277
+ try {
278
+ const { stdout } = await execFile("git", ["-C", canonicalRoot, "rev-parse", "--show-toplevel"], { encoding: "utf8", maxBuffer: 64 * 1024 });
279
+ const reported = stdout.trim();
280
+ if (!reported || !isAbsolute(reported)) {
281
+ throw new UnsafeStateDirectoryError(canonicalRoot, "Git returned an invalid worktree root");
282
+ }
283
+ return await realpath(reported);
284
+ }
285
+ catch (error) {
286
+ if (error instanceof UnsafeStateDirectoryError)
287
+ throw error;
288
+ if (gitEntryRoot) {
289
+ throw new UnsafeStateDirectoryError(canonicalRoot, "the enclosing Git worktree could not be verified");
290
+ }
291
+ return undefined;
292
+ }
293
+ }
161
294
  /**
162
295
  * Read one regular child file without following a symbolic link. State can sit
163
296
  * inside a cloned repository, so validating only the parent directory is not
164
297
  * enough: a committed child symlink must never expose an arbitrary local file.
165
298
  */
166
- export async function readSafeStateText(stateDir, fileName) {
299
+ export async function readSafeStateText(stateDir, fileName, options = {}) {
167
300
  const filePath = await resolveSafeStateChild(stateDir, fileName, false);
168
301
  let handle;
169
302
  try {
@@ -172,7 +305,34 @@ export async function readSafeStateText(stateDir, fileName) {
172
305
  if (!info.isFile()) {
173
306
  throw new UnsafeStateFileError(filePath, "the state entry is not a regular file");
174
307
  }
175
- return await handle.readFile("utf8");
308
+ const maxBytes = options.maxBytes;
309
+ if (maxBytes === undefined)
310
+ return await handle.readFile("utf8");
311
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
312
+ throw new RangeError("maxBytes must be a non-negative safe integer");
313
+ }
314
+ if (info.size > maxBytes) {
315
+ throw new UnsafeStateFileError(filePath, `the state entry exceeds ${maxBytes} bytes`);
316
+ }
317
+ // Never use readFile for attacker-authored bounded state: a same-inode
318
+ // growth race could otherwise allocate the new whole size after fstat.
319
+ const buffer = Buffer.alloc(Math.min(maxBytes + 1, Math.max(1, info.size + 1)));
320
+ let offset = 0;
321
+ while (offset < buffer.length) {
322
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
323
+ if (bytesRead === 0)
324
+ break;
325
+ offset += bytesRead;
326
+ }
327
+ const after = await handle.stat();
328
+ if (offset > maxBytes || after.size > maxBytes) {
329
+ throw new UnsafeStateFileError(filePath, `the state entry exceeds ${maxBytes} bytes`);
330
+ }
331
+ if (after.size !== info.size || after.mtimeMs !== info.mtimeMs ||
332
+ after.ctimeMs !== info.ctimeMs) {
333
+ throw new UnsafeStateFileError(filePath, "the state entry changed while it was read");
334
+ }
335
+ return buffer.subarray(0, offset).toString("utf8");
176
336
  }
177
337
  catch (error) {
178
338
  if (isNodeError(error, "ELOOP")) {
package/dist/schema.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  export declare const costConfidenceValues: readonly ["verified", "estimated", "detected_unverified", "missing"];
3
3
  export declare const costConfidenceSchema: z.ZodEnum<{
4
- verified: "verified";
5
4
  estimated: "estimated";
5
+ verified: "verified";
6
6
  detected_unverified: "detected_unverified";
7
7
  missing: "missing";
8
8
  }>;
@@ -12,8 +12,8 @@ export declare const spendSourceSchema: z.ZodObject<{
12
12
  name: z.ZodString;
13
13
  provider: z.ZodString;
14
14
  confidence: z.ZodEnum<{
15
- verified: "verified";
16
15
  estimated: "estimated";
16
+ verified: "verified";
17
17
  detected_unverified: "detected_unverified";
18
18
  missing: "missing";
19
19
  }>;
@@ -30,9 +30,9 @@ export type SpendSource = z.infer<typeof spendSourceSchema>;
30
30
  */
31
31
  export declare const usageGranularityValues: readonly ["call", "invocation", "session", "daily_aggregate", "usage_bucket", "billing_bucket", "seat", "user_aggregate"];
32
32
  export declare const usageGranularitySchema: z.ZodEnum<{
33
+ session: "session";
33
34
  call: "call";
34
35
  invocation: "invocation";
35
- session: "session";
36
36
  daily_aggregate: "daily_aggregate";
37
37
  usage_bucket: "usage_bucket";
38
38
  billing_bucket: "billing_bucket";
@@ -59,8 +59,8 @@ export declare const usageRecordSchema: z.ZodObject<{
59
59
  name: z.ZodString;
60
60
  provider: z.ZodString;
61
61
  confidence: z.ZodEnum<{
62
- verified: "verified";
63
62
  estimated: "estimated";
63
+ verified: "verified";
64
64
  detected_unverified: "detected_unverified";
65
65
  missing: "missing";
66
66
  }>;
@@ -89,8 +89,8 @@ export declare const usageRecordSchema: z.ZodObject<{
89
89
  sourceVersions: z.ZodOptional<z.ZodArray<z.ZodString>>;
90
90
  amountUsd: z.ZodNullable<z.ZodNumber>;
91
91
  costConfidence: z.ZodEnum<{
92
- verified: "verified";
93
92
  estimated: "estimated";
93
+ verified: "verified";
94
94
  detected_unverified: "detected_unverified";
95
95
  missing: "missing";
96
96
  }>;
@@ -104,9 +104,9 @@ export declare const usageRecordSchema: z.ZodObject<{
104
104
  agentId: z.ZodOptional<z.ZodString>;
105
105
  operation: z.ZodOptional<z.ZodString>;
106
106
  usageGranularity: z.ZodOptional<z.ZodEnum<{
107
+ session: "session";
107
108
  call: "call";
108
109
  invocation: "invocation";
109
- session: "session";
110
110
  daily_aggregate: "daily_aggregate";
111
111
  usage_bucket: "usage_bucket";
112
112
  billing_bucket: "billing_bucket";
@@ -152,10 +152,10 @@ export declare function downgradeSampleUsageEvidence(records: UsageRecord[]): Us
152
152
  export declare function spendComparisonKey(record: UsageRecord): string | undefined;
153
153
  export declare const attributionCandidateSchema: z.ZodObject<{
154
154
  entityType: z.ZodEnum<{
155
- client: "client";
155
+ user: "user";
156
156
  project: "project";
157
157
  agent: "agent";
158
- user: "user";
158
+ client: "client";
159
159
  workspace: "workspace";
160
160
  api_key: "api_key";
161
161
  }>;
@@ -168,10 +168,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
168
168
  usageRecordId: z.ZodString;
169
169
  candidates: z.ZodArray<z.ZodObject<{
170
170
  entityType: z.ZodEnum<{
171
- client: "client";
171
+ user: "user";
172
172
  project: "project";
173
173
  agent: "agent";
174
- user: "user";
174
+ client: "client";
175
175
  workspace: "workspace";
176
176
  api_key: "api_key";
177
177
  }>;
@@ -181,10 +181,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
181
181
  }, z.core.$strip>>;
182
182
  selected: z.ZodOptional<z.ZodObject<{
183
183
  entityType: z.ZodEnum<{
184
- client: "client";
184
+ user: "user";
185
185
  project: "project";
186
186
  agent: "agent";
187
- user: "user";
187
+ client: "client";
188
188
  workspace: "workspace";
189
189
  api_key: "api_key";
190
190
  }>;
@@ -206,8 +206,8 @@ export declare const spendBreakdownEntrySchema: z.ZodObject<{
206
206
  amountUsd: z.ZodNumber;
207
207
  recordCount: z.ZodNumber;
208
208
  confidence: z.ZodEnum<{
209
- verified: "verified";
210
209
  estimated: "estimated";
210
+ verified: "verified";
211
211
  detected_unverified: "detected_unverified";
212
212
  missing: "missing";
213
213
  }>;
@@ -224,8 +224,8 @@ export declare const spendAnomalySchema: z.ZodObject<{
224
224
  currentAmountUsd: z.ZodNumber;
225
225
  multiplier: z.ZodNumber;
226
226
  confidence: z.ZodEnum<{
227
- verified: "verified";
228
227
  estimated: "estimated";
228
+ verified: "verified";
229
229
  detected_unverified: "detected_unverified";
230
230
  missing: "missing";
231
231
  }>;
@@ -241,8 +241,8 @@ export declare const workflowWatchEntrySchema: z.ZodObject<{
241
241
  shareOfSpend: z.ZodNumber;
242
242
  recordCount: z.ZodNumber;
243
243
  confidence: z.ZodEnum<{
244
- verified: "verified";
245
244
  estimated: "estimated";
245
+ verified: "verified";
246
246
  detected_unverified: "detected_unverified";
247
247
  missing: "missing";
248
248
  }>;
@@ -266,8 +266,8 @@ export declare const recommendationSchema: z.ZodObject<{
266
266
  }>;
267
267
  estimatedImpactUsd: z.ZodNumber;
268
268
  confidence: z.ZodEnum<{
269
- verified: "verified";
270
269
  estimated: "estimated";
270
+ verified: "verified";
271
271
  detected_unverified: "detected_unverified";
272
272
  missing: "missing";
273
273
  }>;
@@ -311,8 +311,8 @@ export declare const spendInsightSchema: z.ZodObject<{
311
311
  affectedModels: z.ZodArray<z.ZodString>;
312
312
  estimatedImpactUsd: z.ZodNumber;
313
313
  confidence: z.ZodEnum<{
314
- verified: "verified";
315
314
  estimated: "estimated";
315
+ verified: "verified";
316
316
  detected_unverified: "detected_unverified";
317
317
  missing: "missing";
318
318
  }>;
@@ -324,14 +324,14 @@ export declare const spendSummarySchema: z.ZodObject<{
324
324
  totalUsd: z.ZodNumber;
325
325
  recordCount: z.ZodNumber;
326
326
  confidence: z.ZodEnum<{
327
- verified: "verified";
328
327
  estimated: "estimated";
328
+ verified: "verified";
329
329
  detected_unverified: "detected_unverified";
330
330
  missing: "missing";
331
331
  }>;
332
332
  confidenceBreakdown: z.ZodRecord<z.ZodEnum<{
333
- verified: "verified";
334
333
  estimated: "estimated";
334
+ verified: "verified";
335
335
  detected_unverified: "detected_unverified";
336
336
  missing: "missing";
337
337
  }>, z.ZodNumber>;
@@ -340,8 +340,8 @@ export declare const spendSummarySchema: z.ZodObject<{
340
340
  amountUsd: z.ZodNumber;
341
341
  recordCount: z.ZodNumber;
342
342
  confidence: z.ZodEnum<{
343
- verified: "verified";
344
343
  estimated: "estimated";
344
+ verified: "verified";
345
345
  detected_unverified: "detected_unverified";
346
346
  missing: "missing";
347
347
  }>;
@@ -351,8 +351,8 @@ export declare const spendSummarySchema: z.ZodObject<{
351
351
  amountUsd: z.ZodNumber;
352
352
  recordCount: z.ZodNumber;
353
353
  confidence: z.ZodEnum<{
354
- verified: "verified";
355
354
  estimated: "estimated";
355
+ verified: "verified";
356
356
  detected_unverified: "detected_unverified";
357
357
  missing: "missing";
358
358
  }>;
@@ -362,8 +362,8 @@ export declare const spendSummarySchema: z.ZodObject<{
362
362
  amountUsd: z.ZodNumber;
363
363
  recordCount: z.ZodNumber;
364
364
  confidence: z.ZodEnum<{
365
- verified: "verified";
366
365
  estimated: "estimated";
366
+ verified: "verified";
367
367
  detected_unverified: "detected_unverified";
368
368
  missing: "missing";
369
369
  }>;
@@ -373,8 +373,8 @@ export declare const spendSummarySchema: z.ZodObject<{
373
373
  amountUsd: z.ZodNumber;
374
374
  recordCount: z.ZodNumber;
375
375
  confidence: z.ZodEnum<{
376
- verified: "verified";
377
376
  estimated: "estimated";
377
+ verified: "verified";
378
378
  detected_unverified: "detected_unverified";
379
379
  missing: "missing";
380
380
  }>;
@@ -384,8 +384,8 @@ export declare const spendSummarySchema: z.ZodObject<{
384
384
  amountUsd: z.ZodNumber;
385
385
  recordCount: z.ZodNumber;
386
386
  confidence: z.ZodEnum<{
387
- verified: "verified";
388
387
  estimated: "estimated";
388
+ verified: "verified";
389
389
  detected_unverified: "detected_unverified";
390
390
  missing: "missing";
391
391
  }>;
@@ -395,8 +395,8 @@ export declare const spendSummarySchema: z.ZodObject<{
395
395
  amountUsd: z.ZodNumber;
396
396
  recordCount: z.ZodNumber;
397
397
  confidence: z.ZodEnum<{
398
- verified: "verified";
399
398
  estimated: "estimated";
399
+ verified: "verified";
400
400
  detected_unverified: "detected_unverified";
401
401
  missing: "missing";
402
402
  }>;
@@ -406,8 +406,8 @@ export declare const spendSummarySchema: z.ZodObject<{
406
406
  amountUsd: z.ZodNumber;
407
407
  recordCount: z.ZodNumber;
408
408
  confidence: z.ZodEnum<{
409
- verified: "verified";
410
409
  estimated: "estimated";
410
+ verified: "verified";
411
411
  detected_unverified: "detected_unverified";
412
412
  missing: "missing";
413
413
  }>;
@@ -417,8 +417,8 @@ export declare const spendSummarySchema: z.ZodObject<{
417
417
  amountUsd: z.ZodNumber;
418
418
  recordCount: z.ZodNumber;
419
419
  confidence: z.ZodEnum<{
420
- verified: "verified";
421
420
  estimated: "estimated";
421
+ verified: "verified";
422
422
  detected_unverified: "detected_unverified";
423
423
  missing: "missing";
424
424
  }>;
@@ -433,8 +433,8 @@ export declare const spendSummarySchema: z.ZodObject<{
433
433
  shareOfSpend: z.ZodNumber;
434
434
  recordCount: z.ZodNumber;
435
435
  confidence: z.ZodEnum<{
436
- verified: "verified";
437
436
  estimated: "estimated";
437
+ verified: "verified";
438
438
  detected_unverified: "detected_unverified";
439
439
  missing: "missing";
440
440
  }>;
@@ -455,8 +455,8 @@ export declare const spendSummarySchema: z.ZodObject<{
455
455
  currentAmountUsd: z.ZodNumber;
456
456
  multiplier: z.ZodNumber;
457
457
  confidence: z.ZodEnum<{
458
- verified: "verified";
459
458
  estimated: "estimated";
459
+ verified: "verified";
460
460
  detected_unverified: "detected_unverified";
461
461
  missing: "missing";
462
462
  }>;
@@ -474,8 +474,8 @@ export declare const spendSummarySchema: z.ZodObject<{
474
474
  }>;
475
475
  estimatedImpactUsd: z.ZodNumber;
476
476
  confidence: z.ZodEnum<{
477
- verified: "verified";
478
477
  estimated: "estimated";
478
+ verified: "verified";
479
479
  detected_unverified: "detected_unverified";
480
480
  missing: "missing";
481
481
  }>;
@@ -512,8 +512,8 @@ export declare const spendSummarySchema: z.ZodObject<{
512
512
  affectedModels: z.ZodArray<z.ZodString>;
513
513
  estimatedImpactUsd: z.ZodNumber;
514
514
  confidence: z.ZodEnum<{
515
- verified: "verified";
516
515
  estimated: "estimated";
516
+ verified: "verified";
517
517
  detected_unverified: "detected_unverified";
518
518
  missing: "missing";
519
519
  }>;
@@ -0,0 +1,145 @@
1
+ import { type LocalAgentActivity, type LocalAgentCall, type LocalAgentRateLimitWindow, type LocalAgentTurnUsage } from "./localAgentLogs.js";
2
+ /**
3
+ * Additive, privacy-reduced session evidence for future before/after tests.
4
+ *
5
+ * V0 deliberately accepts already-parsed LocalAgentCall values rather than
6
+ * reading transcripts itself. It never carries prompts, responses, absolute
7
+ * paths, filenames, or raw provider session identifiers.
8
+ */
9
+ export type SessionVitalsAgentV0 = "claude-code" | "codex";
10
+ export type SessionVitalsTokenEvidenceV0 = {
11
+ status: "observed";
12
+ basis: "turn_sum" | "session_cumulative";
13
+ inputTokens: number;
14
+ outputTokens: number;
15
+ cacheReadTokens?: number;
16
+ cacheWrite5mTokens?: number;
17
+ cacheWrite1hTokens?: number;
18
+ thoughtTokens?: number;
19
+ toolTokens?: number;
20
+ componentTotalTokens: number;
21
+ reportedTotalTokens?: number;
22
+ componentEvidence: {
23
+ inputTokens: "observed";
24
+ outputTokens: "observed";
25
+ cacheReadTokens: "observed" | "not_separately_reported";
26
+ cacheWriteTokens: "observed" | "partial" | "not_separately_reported";
27
+ thoughtTokens: "observed" | "not_separately_reported";
28
+ toolTokens: "observed" | "not_separately_reported";
29
+ componentTotalTokens: "calculated_complete" | "calculated_partial";
30
+ reportedTotalTokens: "provider_reported" | "not_reported";
31
+ };
32
+ } | {
33
+ status: "missing";
34
+ reason: "unsupported_token_shape" | "mixed_usage_scope" | "invalid_token_evidence";
35
+ };
36
+ export type SessionVitalsLatestTurnV0 = {
37
+ inputTokens: number;
38
+ outputTokens: number;
39
+ cacheReadTokens?: number;
40
+ cacheWrite5mTokens?: number;
41
+ cacheWrite1hTokens?: number;
42
+ thoughtTokens?: number;
43
+ toolTokens?: number;
44
+ contextTokens: number;
45
+ totalTokens: number;
46
+ source: LocalAgentTurnUsage["source"];
47
+ };
48
+ export type SessionVitalsRateLimitWindowV0 = {
49
+ kind: LocalAgentRateLimitWindow["kind"];
50
+ name: string;
51
+ usedPercent: number;
52
+ windowMinutes: number;
53
+ resetsAt: string;
54
+ };
55
+ export type SessionVitalsCompletionV0 = {
56
+ status: "completed";
57
+ /**
58
+ * A completed session snapshot, not permanent transcript closure.
59
+ * `claude_task_result` is the host-recorded Task tool result
60
+ * (`status: "completed"`) written into the owning transcript when a
61
+ * subagent run finishes — the only explicit completion marker Claude
62
+ * Code produces for subagent transcript files.
63
+ */
64
+ evidence: "claude_turn_duration" | "codex_task_complete" | "claude_task_result";
65
+ observedAt: string;
66
+ } | {
67
+ status: "missing";
68
+ evidence: "missing";
69
+ reason: "completion_marker_not_observed" | "inconsistent_completion_evidence";
70
+ };
71
+ export type SessionVitalV0 = {
72
+ /** Stable AV-compatible pseudonym. The raw transcript session id is never returned. */
73
+ sessionRef: string;
74
+ /**
75
+ * Pseudonym of the owning parent session, present only on rows split out of
76
+ * a shared-sessionId subagent transcript. Matches the parent row's
77
+ * `sessionRef`; raw identifiers are never returned.
78
+ */
79
+ parentSessionRef?: string;
80
+ agent: SessionVitalsAgentV0;
81
+ /** Unknown remains ineligible for automatic before/after cohort matching. */
82
+ sessionType: "parent" | "subagent" | "unknown";
83
+ /** Existing privacy-reduced project label; paths and suspicious values are omitted. */
84
+ project?: string;
85
+ /** Stable opaque identity derived from one consistent native working directory. */
86
+ projectRef?: string;
87
+ models: string[];
88
+ /** Empty means the parser did not report a safe version; never inferred. */
89
+ sourceVersions: string[];
90
+ observedFrom: string;
91
+ observedTo: string;
92
+ /** Omitted unless a distinct, valid start and end are present. */
93
+ observedDurationMs?: number;
94
+ /** Explicit host completion boundary for this snapshot; inactivity is never treated as completion. */
95
+ completion: SessionVitalsCompletionV0;
96
+ tokenEvidence: SessionVitalsTokenEvidenceV0;
97
+ latestTurn?: SessionVitalsLatestTurnV0;
98
+ activity?: {
99
+ kind: LocalAgentActivity["kind"];
100
+ action: LocalAgentActivity["action"];
101
+ promptCount: number;
102
+ toolCallCount: number;
103
+ };
104
+ rateLimits?: {
105
+ observedAt: string;
106
+ planType?: string;
107
+ windows: SessionVitalsRateLimitWindowV0[];
108
+ };
109
+ provenance: {
110
+ source: "parsed_local_agent_calls";
111
+ confidence: "observed";
112
+ uploaded: false;
113
+ };
114
+ };
115
+ export type SessionVitalsV0 = {
116
+ schemaVersion: 0;
117
+ sessions: SessionVitalV0[];
118
+ coverage: {
119
+ inputCalls: number;
120
+ deduplicatedCalls: number;
121
+ eligibleCalls: number;
122
+ emittedSessions: number;
123
+ sessionsWithObservedTokens: number;
124
+ sessionsWithMissingTokens: number;
125
+ excludedCalls: {
126
+ unsupportedAgent: number;
127
+ missingSessionIdentity: number;
128
+ invalidTimestamp: number;
129
+ };
130
+ };
131
+ privacy: {
132
+ rawSessionIds: false;
133
+ promptOrResponseText: false;
134
+ absolutePaths: false;
135
+ uploaded: false;
136
+ };
137
+ };
138
+ /**
139
+ * Build one deterministic V0 row per Claude Code or Codex session.
140
+ *
141
+ * Token evidence fails closed at the session boundary: one unsupported,
142
+ * invalid, or mixed-scope call prevents a partial sum from looking complete.
143
+ */
144
+ export declare function extractSessionVitalsV0(calls: readonly LocalAgentCall[]): SessionVitalsV0;
145
+ //# sourceMappingURL=sessionVitals.d.ts.map