@agent-finops/core 0.5.7 → 0.5.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @agent-finops/core
2
2
 
3
- The canonical local-first data and decision engine for
3
+ The canonical local-first evidence and decision engine for
4
4
  [aibill](https://github.com/futurastudio/ai-spend-agent): Claude Code/Codex
5
5
  activity ingestion, provider cost semantics, attribution, provenance, runway,
6
6
  Context Health, and the shared Glance contract.
@@ -8,6 +8,10 @@ Context Health, and the shared Glance contract.
8
8
  Most users should run `npx aibill`. This package is for integrations that
9
9
  need the same evidence-labeled calculations as the CLI and MCP server.
10
10
 
11
+ This is the open foundation for aibill's financial-accountability mission. It
12
+ does not yet implement company-wide ownership, accepted outcomes, approvals,
13
+ invoice reconciliation, or ROI.
14
+
11
15
  Local API-equivalent estimates, subscription context, and official
12
16
  provider-reported cost are separate concepts and must not be added together.
13
17
 
package/dist/analyze.js CHANGED
@@ -147,7 +147,7 @@ export function generateRecommendations(records) {
147
147
  id: "model-downgrade",
148
148
  title: "Review expensive model workloads for downgrade candidates",
149
149
  rationale: `${topModel.key} is the largest cost driver in the current local sample.`,
150
- whyItMatters: "Premium model usage tends to become invisible once agents are running in the background. Board owners need a clear rule for which jobs deserve the expensive model.",
150
+ whyItMatters: "Premium model usage tends to become invisible once agents are running in the background. Spend owners need a clear rule for which jobs deserve the expensive model.",
151
151
  nextAction: `Audit the top ${topModel.key} operations and move low-risk summarization, extraction, and draft work to a cheaper model tier first.`,
152
152
  priority: "high",
153
153
  estimatedImpactUsd: roundMoney(topModel.amountUsd * impactRatios.modelDowngrade),
@@ -26,7 +26,7 @@ export function attributeUsageRecords(records) {
26
26
  }
27
27
  function buildCandidates(record) {
28
28
  const candidates = [];
29
- if (record.projectId) {
29
+ if (record.projectId && !isPlaceholderEntityId(record.projectId)) {
30
30
  candidates.push({
31
31
  entityType: "project",
32
32
  entityId: record.projectId,
@@ -34,7 +34,7 @@ function buildCandidates(record) {
34
34
  evidence: [`usage record includes projectId ${record.projectId}`]
35
35
  });
36
36
  }
37
- if (record.clientId) {
37
+ if (record.clientId && !isPlaceholderEntityId(record.clientId)) {
38
38
  candidates.push({
39
39
  entityType: "client",
40
40
  entityId: record.clientId,
@@ -42,7 +42,7 @@ function buildCandidates(record) {
42
42
  evidence: [`usage record includes clientId ${record.clientId}`]
43
43
  });
44
44
  }
45
- if (record.agentId) {
45
+ if (record.agentId && !isPlaceholderEntityId(record.agentId)) {
46
46
  candidates.push({
47
47
  entityType: "agent",
48
48
  entityId: record.agentId,
@@ -50,7 +50,7 @@ function buildCandidates(record) {
50
50
  evidence: [`usage record includes agentId ${record.agentId}`]
51
51
  });
52
52
  }
53
- if (record.userId) {
53
+ if (record.userId && !isPlaceholderEntityId(record.userId)) {
54
54
  candidates.push({
55
55
  entityType: "user",
56
56
  entityId: record.userId,
@@ -58,7 +58,7 @@ function buildCandidates(record) {
58
58
  evidence: [`usage record includes userId ${record.userId}`]
59
59
  });
60
60
  }
61
- if (record.workspaceId) {
61
+ if (record.workspaceId && !isPlaceholderEntityId(record.workspaceId)) {
62
62
  candidates.push({
63
63
  entityType: "workspace",
64
64
  entityId: record.workspaceId,
@@ -66,7 +66,7 @@ function buildCandidates(record) {
66
66
  evidence: [`usage record includes workspaceId ${record.workspaceId}`]
67
67
  });
68
68
  }
69
- if (record.apiKeyId) {
69
+ if (record.apiKeyId && !isPlaceholderEntityId(record.apiKeyId)) {
70
70
  candidates.push({
71
71
  entityType: "api_key",
72
72
  entityId: record.apiKeyId,
@@ -103,6 +103,9 @@ function buildCandidates(record) {
103
103
  }
104
104
  return dedupeCandidates(candidates);
105
105
  }
106
+ function isPlaceholderEntityId(value) {
107
+ return ["(home)", "home", "unknown", "unattributed", "unmapped", "(unmapped)"].includes(value.trim().toLowerCase());
108
+ }
106
109
  function dedupeCandidates(candidates) {
107
110
  const byKey = new Map();
108
111
  for (const candidate of candidates) {
@@ -146,6 +146,12 @@ export function parseCodexRollout(content) {
146
146
  const normalized = resolve(workdir);
147
147
  toolWorkdirs.set(normalized, (toolWorkdirs.get(normalized) ?? 0) + 1);
148
148
  }
149
+ // Current Codex Desktop records the orchestration wrapper as a custom
150
+ // `exec` call whose input is JavaScript containing nested tool calls.
151
+ // Extract only quoted absolute workdir/cwd values; never evaluate it.
152
+ if (payload.type === "custom_tool_call" && stringOf(payload.name) === "exec") {
153
+ collectEmbeddedToolWorkdirs(stringOf(payload.input), toolWorkdirs);
154
+ }
149
155
  collectToolFiles(args, fileCounts);
150
156
  collectPatchFiles(stringOf(args?.patch) ?? stringOf(args?.input) ?? stringOf(payload.input), fileCounts);
151
157
  }
@@ -199,6 +205,24 @@ export function parseCodexRollout(content) {
199
205
  }
200
206
  }];
201
207
  }
208
+ function collectEmbeddedToolWorkdirs(input, toolWorkdirs) {
209
+ if (!input)
210
+ return;
211
+ const fieldPattern = /(?:^|[^A-Za-z0-9_])["']?(?:workdir|cwd)["']?\s*:\s*("(?:\\.|[^"\\])*")/g;
212
+ for (const match of input.matchAll(fieldPattern)) {
213
+ let value;
214
+ try {
215
+ value = JSON.parse(match[1]);
216
+ }
217
+ catch {
218
+ continue;
219
+ }
220
+ if (typeof value !== "string" || !isAbsolute(value))
221
+ continue;
222
+ const normalized = resolve(value);
223
+ toolWorkdirs.set(normalized, (toolWorkdirs.get(normalized) ?? 0) + 1);
224
+ }
225
+ }
202
226
  function parseCodexRateLimits(value, observedAt) {
203
227
  if (!isRecord(value) || !observedAt)
204
228
  return undefined;
@@ -314,7 +338,10 @@ export function aggregateCalls(calls) {
314
338
  outputTokens: usage.outputTokens,
315
339
  amountUsd: priced ? amountUsd : null,
316
340
  costConfidence: priced ? "estimated" : "missing",
317
- projectId: project === "unattributed" ? undefined : project,
341
+ // `(home)` is an attribution fallback, not a real project. Keep it on
342
+ // LocalAgentCall for Glance/session context, but do not promote it to a
343
+ // high-confidence project id in receipts or the attribution engine.
344
+ projectId: project === "unattributed" || project === "(home)" ? undefined : project,
318
345
  agentId: agent,
319
346
  providerCostType: "local_agent_logs",
320
347
  quantity: groupCalls.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-finops/core",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
20
20
  "!dist/**/*.tsbuildinfo"
21
21
  ],
22
22
  "license": "MIT",
23
- "description": "Canonical aibill engine for local agent activity, billing semantics, attribution, provenance, runway, and Context Health.",
23
+ "description": "Canonical aibill evidence engine for agent work, billing semantics, attribution, provenance, runway, and Context Health.",
24
24
  "keywords": [
25
25
  "ai",
26
26
  "finops",