@agent-finops/core 0.1.1 → 0.1.3
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/dist/agentInventory.d.ts +86 -0
- package/dist/agentInventory.js +324 -0
- package/dist/analyze.js +39 -8
- package/dist/cutList.d.ts +31 -0
- package/dist/cutList.js +34 -0
- package/dist/deadContext.d.ts +72 -0
- package/dist/deadContext.js +174 -0
- package/dist/discovery.d.ts +2 -0
- package/dist/discovery.js +56 -9
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/providerConnectors.d.ts +5 -1
- package/dist/providerConnectors.js +120 -21
- package/dist/scanGuard.d.ts +8 -0
- package/dist/scanGuard.js +65 -0
- package/dist/schema.d.ts +29 -29
- package/dist/toolInvocations.d.ts +61 -0
- package/dist/toolInvocations.js +186 -0
- package/package.json +8 -3
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type AgentInventoryOptions, type InventoryItem } from "./agentInventory.js";
|
|
2
|
+
import { type InvocationSummary, type ToolInvocationOptions } from "./toolInvocations.js";
|
|
3
|
+
import type { CutAction } from "./cutList.js";
|
|
4
|
+
export type DeadContextItem = {
|
|
5
|
+
kind: InventoryItem["kind"];
|
|
6
|
+
name: string;
|
|
7
|
+
alwaysLoadedTokens: number;
|
|
8
|
+
weightConfidence: InventoryItem["weightConfidence"];
|
|
9
|
+
};
|
|
10
|
+
export type DeadContextResult = {
|
|
11
|
+
/** True when we found inventory + transcripts AND at least one dead item. */
|
|
12
|
+
hasData: boolean;
|
|
13
|
+
/** True when these are illustrative sample numbers, not the user's real data. */
|
|
14
|
+
isSample?: boolean;
|
|
15
|
+
/** Prunable inventory items considered (built-ins excluded upstream). */
|
|
16
|
+
loadedCount: number;
|
|
17
|
+
/** Items never invoked across the parsed window (the defensible headline). */
|
|
18
|
+
deadCount: number;
|
|
19
|
+
/** Dead items whose token weight we MEASURED (skills/subagents/commands). */
|
|
20
|
+
measuredDeadCount: number;
|
|
21
|
+
/** Dead items whose weight we could NOT measure (MCP servers, no schemas). */
|
|
22
|
+
unmeasuredDeadCount: number;
|
|
23
|
+
/** Measured-only always-loaded tokens across dead items (per turn). */
|
|
24
|
+
deadTokens: number;
|
|
25
|
+
/** Measured-only dead tokens loaded into context over a month (projected). */
|
|
26
|
+
monthlyDeadTokens: number;
|
|
27
|
+
/** deadCount / loadedCount, 0..1. */
|
|
28
|
+
wastePercent: number;
|
|
29
|
+
/** Cache-aware monthly $, MEASURED items only (0 when only MCP is dead). */
|
|
30
|
+
monthlyUsd: number;
|
|
31
|
+
/** Upper bound (no prompt caching), measured items only. */
|
|
32
|
+
monthlyUsdUpperBound: number;
|
|
33
|
+
/** The never-used items, heaviest first. */
|
|
34
|
+
deadItems: DeadContextItem[];
|
|
35
|
+
sessions: number;
|
|
36
|
+
totalTurns: number;
|
|
37
|
+
/** Model whose rates priced the measured estimate. */
|
|
38
|
+
pricingModel: string;
|
|
39
|
+
/** Days the parsed transcripts were assumed to represent. */
|
|
40
|
+
windowDays: number;
|
|
41
|
+
};
|
|
42
|
+
export type DeadContextOptions = AgentInventoryOptions & ToolInvocationOptions & {
|
|
43
|
+
/** Days the parsed transcripts represent (for the /mo projection). Default 30. */
|
|
44
|
+
windowDays?: number;
|
|
45
|
+
/** Representative model for cache/input rates. Default claude-sonnet-4. */
|
|
46
|
+
pricingModel?: string;
|
|
47
|
+
/** Inject inventory/invocations (tests); otherwise loaded from disk. */
|
|
48
|
+
inventory?: {
|
|
49
|
+
items: InventoryItem[];
|
|
50
|
+
};
|
|
51
|
+
invocations?: InvocationSummary;
|
|
52
|
+
};
|
|
53
|
+
/** Load inventory + invocations from disk (or use injected ones) and price the waste. */
|
|
54
|
+
export declare function loadDeadContext(options?: DeadContextOptions): Promise<DeadContextResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Illustrative dead-context numbers for the first-run / demo card, so the
|
|
57
|
+
* feature is always visible even when a user has nothing loaded yet. Clearly
|
|
58
|
+
* flagged isSample. Shows the measured-skills case (count + a small honest $).
|
|
59
|
+
*/
|
|
60
|
+
export declare function sampleDeadContext(): DeadContextResult;
|
|
61
|
+
/** Pure core: compare inventory vs. invocations; count all dead, price only measured. */
|
|
62
|
+
export declare function computeDeadContext(items: InventoryItem[], invocations: InvocationSummary, config: {
|
|
63
|
+
windowDays: number;
|
|
64
|
+
pricingModel: string;
|
|
65
|
+
}): DeadContextResult;
|
|
66
|
+
/**
|
|
67
|
+
* Adapt a dead-context result to a {@link CutAction} so it can flow into the
|
|
68
|
+
* ranked cut list / AI Receipt. Returns null unless there is MEASURED waste
|
|
69
|
+
* worth a dollar figure (MCP-only waste is shown as a count, not a cut $).
|
|
70
|
+
*/
|
|
71
|
+
export declare function deadContextCutAction(result: DeadContextResult): CutAction | null;
|
|
72
|
+
//# sourceMappingURL=deadContext.d.ts.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { loadAgentInventory } from "./agentInventory.js";
|
|
2
|
+
import { findPricingRule } from "./modelPricing.js";
|
|
3
|
+
import { loadToolInvocations } from "./toolInvocations.js";
|
|
4
|
+
/**
|
|
5
|
+
* Dead-context: the tools an agent LOADS into context but NEVER calls. Compares
|
|
6
|
+
* the local Claude Code inventory (skills, subagents, slash commands, MCP
|
|
7
|
+
* servers — {@link loadAgentInventory}) against what real transcripts show was
|
|
8
|
+
* invoked ({@link loadToolInvocations}).
|
|
9
|
+
*
|
|
10
|
+
* ACCURACY CONTRACT (this is the credibility lever — read before changing):
|
|
11
|
+
* - The COUNT + utilization % is always defensible and is the headline.
|
|
12
|
+
* - A token/$ magnitude is ONLY computed from items whose weight we actually
|
|
13
|
+
* MEASURED — skill/subagent/command frontmatter (weightConfidence
|
|
14
|
+
* "estimated"). We never price items whose weight we could not measure.
|
|
15
|
+
* - MCP servers have NO readable schemas in local config, so their real
|
|
16
|
+
* weight is unknown. They are COUNTED as dead but NEVER assigned a $/token
|
|
17
|
+
* figure — to size them we'd have to query each server's tools/list. They
|
|
18
|
+
* surface as `unmeasuredDeadCount` so the renderer can say "not measurable".
|
|
19
|
+
* - Pricing is cache-aware (one cache write/session + a read/turn), never the
|
|
20
|
+
* inflated full-input-rate-every-turn number.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_WINDOW_DAYS = 30;
|
|
23
|
+
/**
|
|
24
|
+
* Representative model for pricing measured dead context. Claude Code runs
|
|
25
|
+
* Anthropic models; Sonnet rates are a conservative middle. Always "estimated".
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_PRICING_MODEL = "claude-sonnet-4";
|
|
28
|
+
/** Load inventory + invocations from disk (or use injected ones) and price the waste. */
|
|
29
|
+
export async function loadDeadContext(options = {}) {
|
|
30
|
+
const inventory = options.inventory ?? (await loadAgentInventory(options));
|
|
31
|
+
const invocations = options.invocations ?? (await loadToolInvocations(options));
|
|
32
|
+
return computeDeadContext(inventory.items, invocations, {
|
|
33
|
+
windowDays: options.windowDays ?? DEFAULT_WINDOW_DAYS,
|
|
34
|
+
pricingModel: options.pricingModel ?? DEFAULT_PRICING_MODEL
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Illustrative dead-context numbers for the first-run / demo card, so the
|
|
39
|
+
* feature is always visible even when a user has nothing loaded yet. Clearly
|
|
40
|
+
* flagged isSample. Shows the measured-skills case (count + a small honest $).
|
|
41
|
+
*/
|
|
42
|
+
export function sampleDeadContext() {
|
|
43
|
+
const loadedCount = 38;
|
|
44
|
+
const deadCount = 29;
|
|
45
|
+
return {
|
|
46
|
+
hasData: true,
|
|
47
|
+
isSample: true,
|
|
48
|
+
loadedCount,
|
|
49
|
+
deadCount,
|
|
50
|
+
measuredDeadCount: deadCount,
|
|
51
|
+
unmeasuredDeadCount: 0,
|
|
52
|
+
deadTokens: 80,
|
|
53
|
+
monthlyDeadTokens: 120_000,
|
|
54
|
+
wastePercent: deadCount / loadedCount,
|
|
55
|
+
monthlyUsd: 0.4,
|
|
56
|
+
monthlyUsdUpperBound: 2.4,
|
|
57
|
+
deadItems: [],
|
|
58
|
+
sessions: 120,
|
|
59
|
+
totalTurns: 1500,
|
|
60
|
+
pricingModel: DEFAULT_PRICING_MODEL,
|
|
61
|
+
windowDays: 30
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Pure core: compare inventory vs. invocations; count all dead, price only measured. */
|
|
65
|
+
export function computeDeadContext(items, invocations, config) {
|
|
66
|
+
const windowDays = Math.max(1, config.windowDays);
|
|
67
|
+
const sessions = invocations.sessions;
|
|
68
|
+
const totalTurns = invocations.totalAssistantTurns;
|
|
69
|
+
const usedSkills = new Set(invocations.invokedSkills);
|
|
70
|
+
const usedSubagents = new Set(invocations.invokedSubagents);
|
|
71
|
+
const usedCommands = new Set(invocations.invokedCommands);
|
|
72
|
+
const usedMcpTools = new Set(invocations.invokedMcpTools);
|
|
73
|
+
// An MCP server counts as "used" if any invoked mcp tool belongs to it.
|
|
74
|
+
const usedMcpServers = new Set(invocations.invokedMcpTools.map((tool) => tool.split("__")[1]).filter((id) => Boolean(id)));
|
|
75
|
+
const dead = [];
|
|
76
|
+
let loadedCount = 0;
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
loadedCount += 1;
|
|
79
|
+
if (!isDead(item, { usedSkills, usedSubagents, usedCommands, usedMcpTools, usedMcpServers })) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
dead.push({
|
|
83
|
+
kind: item.kind,
|
|
84
|
+
name: item.name,
|
|
85
|
+
alwaysLoadedTokens: item.alwaysLoadedTokens,
|
|
86
|
+
weightConfidence: item.weightConfidence
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
dead.sort((a, b) => b.alwaysLoadedTokens - a.alwaysLoadedTokens);
|
|
90
|
+
// Only items whose weight we actually measured get a $/token figure. MCP
|
|
91
|
+
// servers (weight unknown without querying tools/list) are counted, not priced.
|
|
92
|
+
const measuredDead = dead.filter((item) => item.weightConfidence === "estimated");
|
|
93
|
+
const unmeasuredDead = dead.filter((item) => item.weightConfidence !== "estimated");
|
|
94
|
+
const measuredTokens = measuredDead.reduce((total, item) => total + item.alwaysLoadedTokens, 0);
|
|
95
|
+
const hasData = items.length > 0 && sessions > 0 && dead.length > 0;
|
|
96
|
+
const rates = pricingRates(config.pricingModel);
|
|
97
|
+
// Cached: one cache write per session + a cache read on every later turn.
|
|
98
|
+
const cacheReads = Math.max(0, totalTurns - sessions);
|
|
99
|
+
const windowCachedUsd = (measuredTokens * (sessions * rates.write5mPerM + cacheReads * rates.cacheReadPerM)) / 1_000_000;
|
|
100
|
+
const windowUncachedUsd = (measuredTokens * totalTurns * rates.inputPerM) / 1_000_000;
|
|
101
|
+
const monthFactor = DEFAULT_WINDOW_DAYS / windowDays;
|
|
102
|
+
return {
|
|
103
|
+
hasData,
|
|
104
|
+
loadedCount,
|
|
105
|
+
deadCount: dead.length,
|
|
106
|
+
measuredDeadCount: measuredDead.length,
|
|
107
|
+
unmeasuredDeadCount: unmeasuredDead.length,
|
|
108
|
+
deadTokens: measuredTokens,
|
|
109
|
+
monthlyDeadTokens: Math.round(measuredTokens * totalTurns * monthFactor),
|
|
110
|
+
wastePercent: loadedCount > 0 ? dead.length / loadedCount : 0,
|
|
111
|
+
monthlyUsd: roundMoney(windowCachedUsd * monthFactor),
|
|
112
|
+
monthlyUsdUpperBound: roundMoney(windowUncachedUsd * monthFactor),
|
|
113
|
+
deadItems: dead,
|
|
114
|
+
sessions,
|
|
115
|
+
totalTurns,
|
|
116
|
+
pricingModel: config.pricingModel,
|
|
117
|
+
windowDays
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Adapt a dead-context result to a {@link CutAction} so it can flow into the
|
|
122
|
+
* ranked cut list / AI Receipt. Returns null unless there is MEASURED waste
|
|
123
|
+
* worth a dollar figure (MCP-only waste is shown as a count, not a cut $).
|
|
124
|
+
*/
|
|
125
|
+
export function deadContextCutAction(result) {
|
|
126
|
+
if (!result.hasData || result.measuredDeadCount === 0 || result.monthlyUsd < 0.5) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const pct = Math.round(result.wastePercent * 100);
|
|
130
|
+
return {
|
|
131
|
+
id: "dead-context",
|
|
132
|
+
title: `Trim ${result.measuredDeadCount} loaded tool${result.measuredDeadCount === 1 ? "" : "s"} your agent never calls`,
|
|
133
|
+
action: `Remove or lazy-load ${result.deadCount} of ${result.loadedCount} loaded item${result.loadedCount === 1 ? "" : "s"} ` +
|
|
134
|
+
`(${pct}% never invoked) to reclaim ~${result.deadTokens.toLocaleString("en-US")} tokens of dead context per turn.`,
|
|
135
|
+
estimatedMonthlySavingsUsd: result.monthlyUsd,
|
|
136
|
+
affectedSpendUsd: result.monthlyUsd,
|
|
137
|
+
recordCount: result.deadCount,
|
|
138
|
+
// Dead-context savings come from inventory, not priced usage records, so
|
|
139
|
+
// there are no record IDs to dedupe against the spend-based cut actions.
|
|
140
|
+
recordIds: [],
|
|
141
|
+
confidence: "estimated",
|
|
142
|
+
kind: "context_trim"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function isDead(item, used) {
|
|
146
|
+
switch (item.kind) {
|
|
147
|
+
case "skill":
|
|
148
|
+
return !used.usedSkills.has(item.name);
|
|
149
|
+
case "subagent":
|
|
150
|
+
return !used.usedSubagents.has(item.name);
|
|
151
|
+
case "command":
|
|
152
|
+
return !used.usedCommands.has(item.name);
|
|
153
|
+
case "mcp_tool":
|
|
154
|
+
return !used.usedMcpTools.has(item.name);
|
|
155
|
+
case "mcp_server":
|
|
156
|
+
return !used.usedMcpServers.has(item.name);
|
|
157
|
+
default:
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function pricingRates(model) {
|
|
162
|
+
const rule = findPricingRule(model) ?? findPricingRule(DEFAULT_PRICING_MODEL);
|
|
163
|
+
// Fallback to Sonnet-class numbers if even the default is somehow unmatched.
|
|
164
|
+
const inputPerM = rule?.inputPerM ?? 3;
|
|
165
|
+
return {
|
|
166
|
+
inputPerM,
|
|
167
|
+
cacheReadPerM: rule?.cacheReadPerM ?? inputPerM * 0.1,
|
|
168
|
+
write5mPerM: rule?.cacheWrite5mPerM ?? inputPerM * 1.25
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function roundMoney(value) {
|
|
172
|
+
return Math.round(value * 100) / 100;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=deadContext.js.map
|
package/dist/discovery.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type LocalDiscoveryResult = {
|
|
|
10
10
|
rootPath: string;
|
|
11
11
|
scannedFiles: number;
|
|
12
12
|
skippedDirectories: string[];
|
|
13
|
+
/** Paths that could not be read (permissions, dangling symlinks, non-UTF8) — skipped, never fatal. */
|
|
14
|
+
unreadablePaths: string[];
|
|
13
15
|
signals: UsageSignal[];
|
|
14
16
|
secretsDetected: string[];
|
|
15
17
|
redactedEvidence: string[];
|
package/dist/discovery.js
CHANGED
|
@@ -22,30 +22,66 @@ const providerRules = [
|
|
|
22
22
|
{ provider: "cursor", kind: "invoice", patterns: [/cursor/i], confidence: 0.76 },
|
|
23
23
|
{ provider: "replit", kind: "invoice", patterns: [/replit/i], confidence: 0.72 }
|
|
24
24
|
];
|
|
25
|
-
|
|
25
|
+
// Name-based redaction: any UPPER_SNAKE env-style assignment whose name ends
|
|
26
|
+
// in a secret-ish suffix. `KEY` deliberately subsumes API_KEY/ADMIN_KEY/etc —
|
|
27
|
+
// over-redacting a public key is harmless; leaking a private one is not.
|
|
28
|
+
const secretAssignmentPattern = /\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?|AUTH))\s*=\s*([^\s#"']+)/g;
|
|
29
|
+
// Value-based redaction: known secret shapes regardless of how they're named.
|
|
26
30
|
const providerSecretPatterns = [
|
|
27
31
|
/sk-proj-[A-Za-z0-9_-]{20,}/g,
|
|
28
32
|
/sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}/g,
|
|
29
33
|
/sk-[A-Za-z0-9_-]{20,}/g,
|
|
30
|
-
/helicone_[A-Za-z0-9_-]{16,}/gi
|
|
34
|
+
/helicone_[A-Za-z0-9_-]{16,}/gi,
|
|
35
|
+
// GitHub tokens: classic PATs, OAuth, server/user/refresh, fine-grained.
|
|
36
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
37
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
38
|
+
// Google API keys.
|
|
39
|
+
/\bAIza[0-9A-Za-z_-]{30,}\b/g,
|
|
40
|
+
// JWTs (three base64url segments).
|
|
41
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
42
|
+
// Slack tokens.
|
|
43
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
44
|
+
// AWS access key IDs.
|
|
45
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
46
|
+
// GitLab PATs and npm tokens.
|
|
47
|
+
/\bglpat-[A-Za-z0-9_-]{16,}\b/g,
|
|
48
|
+
/\bnpm_[A-Za-z0-9]{30,}\b/g
|
|
31
49
|
];
|
|
32
50
|
export async function scanLocalUsageSignals(rootPath) {
|
|
33
51
|
const result = {
|
|
34
52
|
rootPath,
|
|
35
53
|
scannedFiles: 0,
|
|
36
54
|
skippedDirectories: [],
|
|
55
|
+
unreadablePaths: [],
|
|
37
56
|
signals: [],
|
|
38
57
|
secretsDetected: [],
|
|
39
58
|
redactedEvidence: []
|
|
40
59
|
};
|
|
41
60
|
const secrets = new Set();
|
|
42
61
|
const skipped = new Set();
|
|
62
|
+
const unreadable = new Set();
|
|
43
63
|
await walk(rootPath, async (path) => {
|
|
44
|
-
|
|
64
|
+
// A dangling symlink, permission-denied entry, or unreadable file must
|
|
65
|
+
// never reject the whole scan — real machines are messy. Skip and report.
|
|
66
|
+
let fileInfo;
|
|
67
|
+
try {
|
|
68
|
+
fileInfo = await stat(path);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
unreadable.add(path);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
45
74
|
if (fileInfo.size > maxFileBytes || !isInterestingFile(path)) {
|
|
46
75
|
return;
|
|
47
76
|
}
|
|
48
|
-
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = await readFile(path, "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
unreadable.add(path);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
49
85
|
const redacted = redactSecrets(raw);
|
|
50
86
|
const relativePath = relative(rootPath, path) || basename(path);
|
|
51
87
|
result.scannedFiles += 1;
|
|
@@ -70,8 +106,11 @@ export async function scanLocalUsageSignals(rootPath) {
|
|
|
70
106
|
confidence: rule.confidence
|
|
71
107
|
});
|
|
72
108
|
}
|
|
73
|
-
}, skipped);
|
|
109
|
+
}, skipped, unreadable);
|
|
74
110
|
result.skippedDirectories = Array.from(skipped).sort();
|
|
111
|
+
result.unreadablePaths = Array.from(unreadable)
|
|
112
|
+
.map((path) => relative(rootPath, path) || basename(path))
|
|
113
|
+
.sort();
|
|
75
114
|
result.secretsDetected = Array.from(secrets).sort();
|
|
76
115
|
result.signals = dedupeSignals(result.signals).sort((left, right) => {
|
|
77
116
|
const provider = left.provider.localeCompare(right.provider);
|
|
@@ -96,8 +135,16 @@ function detectSecretNames(text) {
|
|
|
96
135
|
}
|
|
97
136
|
return Array.from(names);
|
|
98
137
|
}
|
|
99
|
-
async function walk(rootPath, visit, skipped) {
|
|
100
|
-
|
|
138
|
+
async function walk(rootPath, visit, skipped, unreadable) {
|
|
139
|
+
let entries;
|
|
140
|
+
try {
|
|
141
|
+
entries = await readdir(rootPath, { withFileTypes: true });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Permission-denied or vanished directory: skip it, keep scanning.
|
|
145
|
+
unreadable.add(rootPath);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
101
148
|
for (const entry of entries) {
|
|
102
149
|
const path = join(rootPath, entry.name);
|
|
103
150
|
if (entry.isDirectory()) {
|
|
@@ -105,10 +152,10 @@ async function walk(rootPath, visit, skipped) {
|
|
|
105
152
|
skipped.add(entry.name);
|
|
106
153
|
continue;
|
|
107
154
|
}
|
|
108
|
-
await walk(path, visit, skipped);
|
|
155
|
+
await walk(path, visit, skipped, unreadable);
|
|
109
156
|
continue;
|
|
110
157
|
}
|
|
111
|
-
if (entry.isFile()) {
|
|
158
|
+
if (entry.isFile() || entry.isSymbolicLink()) {
|
|
112
159
|
await visit(path);
|
|
113
160
|
}
|
|
114
161
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
export * from "./analyze.js";
|
|
2
|
+
export * from "./agentInventory.js";
|
|
2
3
|
export * from "./attribution.js";
|
|
3
4
|
export * from "./credentialDetection.js";
|
|
4
5
|
export * from "./cutList.js";
|
|
6
|
+
export * from "./deadContext.js";
|
|
5
7
|
export * from "./discovery.js";
|
|
8
|
+
export * from "./toolInvocations.js";
|
|
6
9
|
export * from "./insights.js";
|
|
7
10
|
export * from "./localAgentLogs.js";
|
|
8
11
|
export * from "./modelPricing.js";
|
|
9
12
|
export * from "./planMath.js";
|
|
10
13
|
export * from "./sampleData.js";
|
|
14
|
+
export * from "./scanGuard.js";
|
|
11
15
|
export * from "./schema.js";
|
|
12
16
|
export * from "./sourceRegistry.js";
|
|
13
17
|
export * from "./providerConnectors.js";
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
export * from "./analyze.js";
|
|
2
|
+
export * from "./agentInventory.js";
|
|
2
3
|
export * from "./attribution.js";
|
|
3
4
|
export * from "./credentialDetection.js";
|
|
4
5
|
export * from "./cutList.js";
|
|
6
|
+
export * from "./deadContext.js";
|
|
5
7
|
export * from "./discovery.js";
|
|
8
|
+
export * from "./toolInvocations.js";
|
|
6
9
|
export * from "./insights.js";
|
|
7
10
|
export * from "./localAgentLogs.js";
|
|
8
11
|
export * from "./modelPricing.js";
|
|
9
12
|
export * from "./planMath.js";
|
|
10
13
|
export * from "./sampleData.js";
|
|
14
|
+
export * from "./scanGuard.js";
|
|
11
15
|
export * from "./schema.js";
|
|
12
16
|
export * from "./sourceRegistry.js";
|
|
13
17
|
export * from "./providerConnectors.js";
|
|
@@ -12,9 +12,11 @@ type ProviderResponse = {
|
|
|
12
12
|
export type ProviderQaPagination = {
|
|
13
13
|
label: string;
|
|
14
14
|
pagesFetched: number;
|
|
15
|
-
stoppedBecause: "complete" | "missing_cursor" | "max_pages";
|
|
15
|
+
stoppedBecause: "complete" | "missing_cursor" | "max_pages" | "fetch_error";
|
|
16
16
|
maxPages: number;
|
|
17
17
|
limitPerPage?: number;
|
|
18
|
+
/** Present when stoppedBecause is "fetch_error": the sanitized reason the fetch stopped early. */
|
|
19
|
+
note?: string;
|
|
18
20
|
};
|
|
19
21
|
export type ProviderQaRateLimit = {
|
|
20
22
|
label: string;
|
|
@@ -62,6 +64,8 @@ export type CreateProviderConnectionInput = {
|
|
|
62
64
|
verifiedRecordCount: number;
|
|
63
65
|
totalUsd: number;
|
|
64
66
|
fetchedAt?: Date;
|
|
67
|
+
/** Record-derived completeness; the source's verification label mirrors it. */
|
|
68
|
+
completeness?: ProviderConnectorResult["completeness"];
|
|
65
69
|
};
|
|
66
70
|
export type TokenResolver = (reference: string) => string;
|
|
67
71
|
export type Fetcher = (url: string, init?: {
|