auditor-lambda 0.3.37 → 0.3.39

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 (37) hide show
  1. package/dist/cli.js +102 -2
  2. package/dist/extractors/designAssessment.d.ts +11 -0
  3. package/dist/extractors/designAssessment.js +254 -0
  4. package/dist/io/artifacts.d.ts +3 -0
  5. package/dist/io/artifacts.js +1 -0
  6. package/dist/orchestrator/advance.js +7 -1
  7. package/dist/orchestrator/dependencyMap.js +5 -0
  8. package/dist/orchestrator/designReviewPrompt.d.ts +2 -0
  9. package/dist/orchestrator/designReviewPrompt.js +151 -0
  10. package/dist/orchestrator/executors.js +10 -0
  11. package/dist/orchestrator/internalExecutors.d.ts +2 -0
  12. package/dist/orchestrator/internalExecutors.js +43 -0
  13. package/dist/orchestrator/nextStep.js +2 -0
  14. package/dist/orchestrator/state.js +2 -0
  15. package/dist/providers/types.d.ts +6 -0
  16. package/dist/quota/discoveredLimits.d.ts +21 -0
  17. package/dist/quota/discoveredLimits.js +74 -0
  18. package/dist/quota/headerExtraction.d.ts +8 -0
  19. package/dist/quota/headerExtraction.js +140 -0
  20. package/dist/quota/headerExtractors/claudeCodeHeaderExtractor.d.ts +6 -0
  21. package/dist/quota/headerExtractors/claudeCodeHeaderExtractor.js +28 -0
  22. package/dist/quota/headerExtractors/genericHeaderExtractor.d.ts +9 -0
  23. package/dist/quota/headerExtractors/genericHeaderExtractor.js +7 -0
  24. package/dist/quota/headerExtractors/index.d.ts +5 -0
  25. package/dist/quota/headerExtractors/index.js +12 -0
  26. package/dist/quota/index.d.ts +6 -0
  27. package/dist/quota/index.js +3 -0
  28. package/dist/quota/scheduler.d.ts +3 -0
  29. package/dist/quota/scheduler.js +18 -1
  30. package/dist/reporting/mergeFindings.d.ts +2 -1
  31. package/dist/reporting/mergeFindings.js +13 -1
  32. package/dist/reporting/synthesis.d.ts +2 -0
  33. package/dist/reporting/synthesis.js +1 -1
  34. package/dist/types/designAssessment.d.ts +7 -0
  35. package/dist/types/designAssessment.js +1 -0
  36. package/dist/types/sessionConfig.d.ts +3 -0
  37. package/package.json +1 -1
@@ -0,0 +1,151 @@
1
+ function summarizeUnits(bundle) {
2
+ const units = bundle.unit_manifest?.units ?? [];
3
+ if (units.length === 0)
4
+ return "No units identified.";
5
+ const lines = units.map((unit) => {
6
+ const lenses = unit.required_lenses.join(", ") || "none";
7
+ return `- ${unit.unit_id} (${unit.files.length} files, lenses: ${lenses})`;
8
+ });
9
+ return [
10
+ `${units.length} units:`,
11
+ ...lines.slice(0, 40),
12
+ ...(units.length > 40 ? [` ... and ${units.length - 40} more`] : []),
13
+ ].join("\n");
14
+ }
15
+ function summarizeGraph(bundle) {
16
+ const graphs = bundle.graph_bundle?.graphs;
17
+ if (!graphs)
18
+ return "No dependency graph available.";
19
+ const counts = [];
20
+ for (const [kind, edges] of Object.entries(graphs)) {
21
+ if (Array.isArray(edges) && edges.length > 0) {
22
+ counts.push(`${kind}: ${edges.length} edges`);
23
+ }
24
+ }
25
+ if (counts.length === 0)
26
+ return "Dependency graph is empty.";
27
+ return `Dependency graph: ${counts.join(", ")}.`;
28
+ }
29
+ function summarizeFlows(bundle) {
30
+ const flows = bundle.critical_flows?.flows ?? [];
31
+ if (flows.length === 0)
32
+ return "No critical flows identified.";
33
+ const lines = flows.map((flow) => `- ${flow.name}: ${flow.paths.length} files, concerns: ${flow.concerns.join(", ") || "none"}`);
34
+ return [`${flows.length} critical flows:`, ...lines].join("\n");
35
+ }
36
+ function summarizeRisk(bundle) {
37
+ const items = bundle.risk_register?.items ?? [];
38
+ if (items.length === 0)
39
+ return "No risk items.";
40
+ const sorted = [...items].sort((a, b) => b.risk_score - a.risk_score);
41
+ const top = sorted.slice(0, 10);
42
+ const lines = top.map((item) => `- ${item.unit_id}: score ${item.risk_score}, signals: ${item.signals.join(", ") || "none"}`);
43
+ return [
44
+ `${items.length} risk items (top ${top.length} by score):`,
45
+ ...lines,
46
+ ].join("\n");
47
+ }
48
+ function summarizeSurfaces(bundle) {
49
+ const surfaces = bundle.surface_manifest?.surfaces ?? [];
50
+ if (surfaces.length === 0)
51
+ return "No externally reachable surfaces identified.";
52
+ const lines = surfaces.map((surface) => `- ${surface.id} (${surface.kind}): ${surface.entrypoint}${surface.methods?.length ? ` [${surface.methods.join(", ")}]` : ""}`);
53
+ return [`${surfaces.length} surfaces:`, ...lines].join("\n");
54
+ }
55
+ function summarizeFiles(bundle) {
56
+ const files = bundle.repo_manifest?.files ?? [];
57
+ if (files.length === 0)
58
+ return "No files in manifest.";
59
+ const byLanguage = new Map();
60
+ for (const file of files) {
61
+ const lang = file.language || "unknown";
62
+ byLanguage.set(lang, (byLanguage.get(lang) ?? 0) + 1);
63
+ }
64
+ const langSummary = [...byLanguage.entries()]
65
+ .sort((a, b) => b[1] - a[1])
66
+ .map(([lang, count]) => `${lang}: ${count}`)
67
+ .join(", ");
68
+ return `${files.length} files (${langSummary}).`;
69
+ }
70
+ function formatDeterministicFindings(findings) {
71
+ if (findings.length === 0)
72
+ return "No structural issues detected by deterministic analysis.";
73
+ const lines = findings.map((finding) => `- [${finding.severity}] ${finding.title}: ${finding.summary}`);
74
+ return [
75
+ `${findings.length} structural findings from deterministic analysis:`,
76
+ ...lines,
77
+ ].join("\n");
78
+ }
79
+ export function renderDesignReviewPrompt(bundle) {
80
+ const deterministicFindings = bundle.design_assessment?.findings ?? [];
81
+ return [
82
+ "# Project design review",
83
+ "",
84
+ "You are reviewing the overall design of this project. The deterministic audit pipeline has already analyzed the codebase structure. Your job is to provide qualitative, big-picture design observations that static analysis cannot produce.",
85
+ "",
86
+ "## Project context",
87
+ "",
88
+ `Repository: ${bundle.repo_manifest?.repository?.name ?? "unknown"}`,
89
+ "",
90
+ "### File inventory",
91
+ "",
92
+ summarizeFiles(bundle),
93
+ "",
94
+ "### Unit structure",
95
+ "",
96
+ summarizeUnits(bundle),
97
+ "",
98
+ "### Dependency graph",
99
+ "",
100
+ summarizeGraph(bundle),
101
+ "",
102
+ "### Externally reachable surfaces",
103
+ "",
104
+ summarizeSurfaces(bundle),
105
+ "",
106
+ "### Critical flows",
107
+ "",
108
+ summarizeFlows(bundle),
109
+ "",
110
+ "### Risk profile",
111
+ "",
112
+ summarizeRisk(bundle),
113
+ "",
114
+ "### Deterministic structural findings",
115
+ "",
116
+ formatDeterministicFindings(deterministicFindings),
117
+ "",
118
+ "## What to assess",
119
+ "",
120
+ "Read the project source to understand what it does and how it works, then produce findings about:",
121
+ "",
122
+ "- **Tool and library opportunities**: third-party tools, libraries, or frameworks that would improve the project. Concrete suggestions with rationale, not generic advice.",
123
+ "- **Architecture pattern improvements**: structural changes that would improve extensibility, testability, or maintainability. Consider whether the current abstractions match the problem domain.",
124
+ "- **Design simplification**: areas where the design is over-engineered or where simpler alternatives would work. Conversely, areas that are under-designed for their importance.",
125
+ "- **Integration and generalization**: opportunities to make the project more portable, composable, or protocol-aligned (e.g., MCP, standard APIs, plugin architectures).",
126
+ "- **Missing capabilities**: gaps in the design that would become pain points as the project evolves.",
127
+ "",
128
+ "## Output format",
129
+ "",
130
+ "Produce a JSON array of findings. Each finding must conform to:",
131
+ "",
132
+ "```json",
133
+ "{",
134
+ ' "id": "DR-001",',
135
+ ' "title": "short descriptive title",',
136
+ ' "category": "one of: tool_opportunity, architecture_pattern, design_simplification, integration, missing_capability",',
137
+ ' "severity": "one of: critical, high, medium, low, info",',
138
+ ' "confidence": "one of: high, medium, low",',
139
+ ' "lens": "architecture",',
140
+ ' "summary": "detailed explanation of the observation and the recommended change",',
141
+ ' "affected_files": [{"path": "relevant/file.ts"}],',
142
+ ' "systemic": true',
143
+ "}",
144
+ "```",
145
+ "",
146
+ "Write the JSON array to the design review results path provided below. Use finding IDs starting with DR-001.",
147
+ "",
148
+ "Focus on substantive, actionable observations. Prefer fewer high-quality findings over many surface-level ones.",
149
+ "",
150
+ ].join("\n");
151
+ }
@@ -9,6 +9,16 @@ export const EXECUTOR_REGISTRY = [
9
9
  obligation_ids: ["structure_artifacts"],
10
10
  description: "Build structure artifacts such as units, surfaces, graphs, flows, and risk.",
11
11
  },
12
+ {
13
+ id: "design_assessment_executor",
14
+ obligation_ids: ["design_assessment_current"],
15
+ description: "Run deterministic structural analysis to assess overall project design.",
16
+ },
17
+ {
18
+ id: "design_review",
19
+ obligation_ids: ["design_review_completed"],
20
+ description: "Pause the pipeline and delegate a holistic project design review to the active LLM agent.",
21
+ },
12
22
  {
13
23
  id: "planning_executor",
14
24
  obligation_ids: ["planning_artifacts"],
@@ -13,6 +13,8 @@ export declare function resolveRuntimeValidationSpawnCommand(command: string[],
13
13
  };
14
14
  export declare function runIntakeExecutor(bundle: ArtifactBundle, root: string): Promise<ExecutorRunResult>;
15
15
  export declare function runStructureExecutor(bundle: ArtifactBundle, root?: string): Promise<ExecutorRunResult>;
16
+ export declare function runDesignAssessmentExecutor(bundle: ArtifactBundle): ExecutorRunResult;
17
+ export declare function runDesignReviewAutoComplete(bundle: ArtifactBundle): ExecutorRunResult;
16
18
  export declare function runPlanningExecutor(bundle: ArtifactBundle, root: string, lineIndex?: Record<string, number>): Promise<ExecutorRunResult>;
17
19
  export declare function runResultIngestionExecutor(bundle: ArtifactBundle, results: AuditResult[]): ExecutorRunResult;
18
20
  export declare function runRuntimeValidationExecutor(bundle: ArtifactBundle, root: string): Promise<ExecutorRunResult>;
@@ -15,6 +15,7 @@ import { buildUnitManifest } from "./unitBuilder.js";
15
15
  import { buildRepoManifestFromFs } from "../extractors/fsIntake.js";
16
16
  import { loadIgnoreFile } from "../extractors/ignore.js";
17
17
  import { ingestAuditResults, updateAuditTaskStatuses, } from "./resultIngestion.js";
18
+ import { buildDesignAssessment } from "../extractors/designAssessment.js";
18
19
  import { buildSelectiveDeepeningTasks } from "./selectiveDeepening.js";
19
20
  import { updateRuntimeValidationReport } from "./runtimeValidationUpdate.js";
20
21
  import { autoCompleteTrivialCoverage } from "./trivialAudit.js";
@@ -178,6 +179,47 @@ export async function runStructureExecutor(bundle, root) {
178
179
  : ""),
179
180
  };
180
181
  }
182
+ export function runDesignAssessmentExecutor(bundle) {
183
+ if (!bundle.unit_manifest ||
184
+ !bundle.graph_bundle ||
185
+ !bundle.critical_flows ||
186
+ !bundle.risk_register) {
187
+ throw new Error("Cannot run design assessment executor without structure artifacts");
188
+ }
189
+ const designAssessment = buildDesignAssessment({
190
+ unitManifest: bundle.unit_manifest,
191
+ graphBundle: bundle.graph_bundle,
192
+ criticalFlows: bundle.critical_flows,
193
+ riskRegister: bundle.risk_register,
194
+ });
195
+ return {
196
+ updated: {
197
+ ...bundle,
198
+ design_assessment: designAssessment,
199
+ },
200
+ artifacts_written: ["design_assessment.json"],
201
+ progress_summary: `Design assessment complete: ${designAssessment.findings.length} structural finding(s).`,
202
+ };
203
+ }
204
+ export function runDesignReviewAutoComplete(bundle) {
205
+ const existing = bundle.design_assessment;
206
+ if (!existing) {
207
+ throw new Error("Cannot auto-complete design review without design_assessment artifact");
208
+ }
209
+ const updated = {
210
+ ...existing,
211
+ reviewed: true,
212
+ review_findings: existing.review_findings ?? [],
213
+ };
214
+ return {
215
+ updated: {
216
+ ...bundle,
217
+ design_assessment: updated,
218
+ },
219
+ artifacts_written: ["design_assessment.json"],
220
+ progress_summary: "Design review auto-completed (host-agent review available via next-step).",
221
+ };
222
+ }
181
223
  export async function runPlanningExecutor(bundle, root, lineIndex = {}) {
182
224
  if (!bundle.repo_manifest) {
183
225
  throw new Error("Cannot run planning executor without repo_manifest");
@@ -409,6 +451,7 @@ export function runSynthesisExecutor(bundle, results) {
409
451
  coverageMatrix: bundle.coverage_matrix,
410
452
  runtimeValidationReport: bundle.runtime_validation_report,
411
453
  externalAnalyzerResults: bundle.external_analyzer_results,
454
+ designAssessment: bundle.design_assessment,
412
455
  });
413
456
  return {
414
457
  updated: {
@@ -6,6 +6,8 @@ const PRIORITY = [
6
6
  "auto_fixes_applied",
7
7
  "syntax_resolved",
8
8
  "structure_artifacts",
9
+ "design_assessment_current",
10
+ "design_review_completed",
9
11
  "planning_artifacts",
10
12
  "audit_tasks_completed",
11
13
  "audit_results_ingested",
@@ -29,6 +29,8 @@ export function deriveAuditState(bundle) {
29
29
  "critical_flows.json",
30
30
  "risk_register.json",
31
31
  ], structureReady)));
32
+ obligations.push(obligation("design_assessment_current", staleOrSatisfied(staleArtifacts, ["design_assessment.json"], has(bundle.design_assessment))));
33
+ obligations.push(obligation("design_review_completed", bundle.design_assessment?.reviewed ? "satisfied" : "missing"));
32
34
  const planningReady = has(bundle.coverage_matrix) &&
33
35
  has(bundle.flow_coverage) &&
34
36
  has(bundle.runtime_validation_tasks) &&
@@ -21,7 +21,13 @@ export interface LaunchFreshSessionResult {
21
21
  stderrPath?: string;
22
22
  error?: string;
23
23
  }
24
+ export interface ProviderRateLimits {
25
+ requests_per_minute?: number | null;
26
+ input_tokens_per_minute?: number | null;
27
+ output_tokens_per_minute?: number | null;
28
+ }
24
29
  export interface FreshSessionProvider {
25
30
  name: string;
26
31
  launch(input: LaunchFreshSessionInput): Promise<LaunchFreshSessionResult>;
32
+ queryLimits?(model: string | null): Promise<ProviderRateLimits | null>;
27
33
  }
@@ -0,0 +1,21 @@
1
+ export interface DiscoveredRateLimits {
2
+ requests_per_minute?: number | null;
3
+ input_tokens_per_minute?: number | null;
4
+ output_tokens_per_minute?: number | null;
5
+ source: string;
6
+ }
7
+ export interface DiscoveredLimitsCacheEntry {
8
+ requests_per_minute?: number;
9
+ input_tokens_per_minute?: number;
10
+ discovered_at: string;
11
+ source: string;
12
+ }
13
+ export interface DiscoveredLimitsCache {
14
+ version: 1;
15
+ entries: Record<string, DiscoveredLimitsCacheEntry>;
16
+ }
17
+ export declare function readDiscoveredLimitsCache(): Promise<DiscoveredLimitsCache>;
18
+ export declare function writeDiscoveredLimitsCache(cache: DiscoveredLimitsCache): Promise<void>;
19
+ export declare function updateDiscoveredLimits(providerModelKey: string, limits: DiscoveredRateLimits): Promise<void>;
20
+ export declare function lookupDiscoveredLimits(providerModelKey: string): Promise<DiscoveredRateLimits | null>;
21
+ export declare function mergeDiscoveredLimits(...sources: (DiscoveredRateLimits | null | undefined)[]): DiscoveredRateLimits | null;
@@ -0,0 +1,74 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { getQuotaStatePath } from "./state.js";
4
+ function getCachePath() {
5
+ return getQuotaStatePath().replace(/quota-state\.json$/, "discovered-limits.json");
6
+ }
7
+ export async function readDiscoveredLimitsCache() {
8
+ try {
9
+ const raw = await readFile(getCachePath(), "utf8");
10
+ const parsed = JSON.parse(raw);
11
+ if (parsed !== null &&
12
+ typeof parsed === "object" &&
13
+ !Array.isArray(parsed) &&
14
+ parsed["version"] === 1) {
15
+ return parsed;
16
+ }
17
+ }
18
+ catch (error) {
19
+ if (error.code !== "ENOENT") {
20
+ process.stderr.write(`[quota] ignoring unreadable discovered-limits cache: ${error instanceof Error ? error.message : String(error)}\n`);
21
+ }
22
+ }
23
+ return { version: 1, entries: {} };
24
+ }
25
+ export async function writeDiscoveredLimitsCache(cache) {
26
+ const cachePath = getCachePath();
27
+ await mkdir(dirname(cachePath), { recursive: true });
28
+ await writeFile(cachePath, JSON.stringify(cache, null, 2) + "\n", "utf8");
29
+ }
30
+ export async function updateDiscoveredLimits(providerModelKey, limits) {
31
+ const cache = await readDiscoveredLimitsCache();
32
+ const existing = cache.entries[providerModelKey];
33
+ const entry = {
34
+ ...existing,
35
+ discovered_at: new Date().toISOString(),
36
+ source: limits.source,
37
+ };
38
+ if (limits.requests_per_minute != null) {
39
+ entry.requests_per_minute = limits.requests_per_minute;
40
+ }
41
+ if (limits.input_tokens_per_minute != null) {
42
+ entry.input_tokens_per_minute = limits.input_tokens_per_minute;
43
+ }
44
+ cache.entries[providerModelKey] = entry;
45
+ await writeDiscoveredLimitsCache(cache);
46
+ }
47
+ export async function lookupDiscoveredLimits(providerModelKey) {
48
+ const cache = await readDiscoveredLimitsCache();
49
+ const entry = cache.entries[providerModelKey];
50
+ if (!entry)
51
+ return null;
52
+ if (entry.requests_per_minute == null && entry.input_tokens_per_minute == null)
53
+ return null;
54
+ return {
55
+ requests_per_minute: entry.requests_per_minute ?? null,
56
+ input_tokens_per_minute: entry.input_tokens_per_minute ?? null,
57
+ source: entry.source,
58
+ };
59
+ }
60
+ export function mergeDiscoveredLimits(...sources) {
61
+ let merged = null;
62
+ for (const source of sources) {
63
+ if (!source)
64
+ continue;
65
+ if (!merged) {
66
+ merged = { ...source };
67
+ continue;
68
+ }
69
+ merged.requests_per_minute ??= source.requests_per_minute;
70
+ merged.input_tokens_per_minute ??= source.input_tokens_per_minute;
71
+ merged.output_tokens_per_minute ??= source.output_tokens_per_minute;
72
+ }
73
+ return merged;
74
+ }
@@ -0,0 +1,8 @@
1
+ export interface ExtractedRateLimits {
2
+ requests_per_minute: number | null;
3
+ input_tokens_per_minute: number | null;
4
+ remaining_requests: number | null;
5
+ remaining_tokens: number | null;
6
+ reset_at: string | null;
7
+ }
8
+ export declare function extractRateLimitHeaders(text: string): ExtractedRateLimits | null;
@@ -0,0 +1,140 @@
1
+ const HEADER_PATTERNS = [
2
+ // Standard x-ratelimit-* (OpenAI, Anthropic, and others)
3
+ { pattern: /x-ratelimit-limit-requests:\s*(\d+)/i, field: "requests_per_minute" },
4
+ { pattern: /x-ratelimit-limit-tokens:\s*(\d+)/i, field: "input_tokens_per_minute" },
5
+ { pattern: /x-ratelimit-remaining-requests:\s*(\d+)/i, field: "remaining_requests" },
6
+ { pattern: /x-ratelimit-remaining-tokens:\s*(\d+)/i, field: "remaining_tokens" },
7
+ { pattern: /x-ratelimit-reset-requests:\s*(.+)/i, field: "reset_at", transform: parseResetValue },
8
+ { pattern: /x-ratelimit-reset-tokens:\s*(.+)/i, field: "reset_at", transform: parseResetValue },
9
+ // Anthropic-specific header naming
10
+ { pattern: /anthropic-ratelimit-requests-limit:\s*(\d+)/i, field: "requests_per_minute" },
11
+ { pattern: /anthropic-ratelimit-tokens-limit:\s*(\d+)/i, field: "input_tokens_per_minute" },
12
+ { pattern: /anthropic-ratelimit-requests-remaining:\s*(\d+)/i, field: "remaining_requests" },
13
+ { pattern: /anthropic-ratelimit-tokens-remaining:\s*(\d+)/i, field: "remaining_tokens" },
14
+ { pattern: /anthropic-ratelimit-requests-reset:\s*(.+)/i, field: "reset_at", transform: parseResetValue },
15
+ { pattern: /anthropic-ratelimit-tokens-reset:\s*(.+)/i, field: "reset_at", transform: parseResetValue },
16
+ ];
17
+ function parseResetValue(value) {
18
+ const trimmed = value.trim();
19
+ if (!trimmed)
20
+ return null;
21
+ // ISO timestamp
22
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed))
23
+ return trimmed;
24
+ // Relative seconds (e.g. "42s", "42")
25
+ const seconds = parseFloat(trimmed);
26
+ if (Number.isFinite(seconds) && seconds > 0) {
27
+ return new Date(Date.now() + seconds * 1000).toISOString();
28
+ }
29
+ return trimmed;
30
+ }
31
+ function parseNumericValue(value) {
32
+ const n = parseInt(value, 10);
33
+ return Number.isFinite(n) && n > 0 ? n : null;
34
+ }
35
+ export function extractRateLimitHeaders(text) {
36
+ const result = {
37
+ requests_per_minute: null,
38
+ input_tokens_per_minute: null,
39
+ remaining_requests: null,
40
+ remaining_tokens: null,
41
+ reset_at: null,
42
+ };
43
+ let found = false;
44
+ for (const { pattern, field, transform } of HEADER_PATTERNS) {
45
+ const match = pattern.exec(text);
46
+ if (!match || !match[1])
47
+ continue;
48
+ if (result[field] != null)
49
+ continue; // first match wins
50
+ if (transform) {
51
+ const transformed = transform(match[1]);
52
+ if (transformed != null) {
53
+ result[field] = transformed;
54
+ found = true;
55
+ }
56
+ }
57
+ else {
58
+ const numeric = parseNumericValue(match[1]);
59
+ if (numeric != null) {
60
+ result[field] = numeric;
61
+ found = true;
62
+ }
63
+ }
64
+ }
65
+ // Also try JSON objects that embed header-like fields
66
+ if (!found) {
67
+ const jsonResult = extractFromJson(text);
68
+ if (jsonResult)
69
+ return jsonResult;
70
+ }
71
+ return found ? result : null;
72
+ }
73
+ function extractFromJson(text) {
74
+ const jsonPattern = /\{[^{}]*"(?:x-ratelimit|anthropic-ratelimit|ratelimit)[^{}]*\}/gi;
75
+ for (const match of text.matchAll(jsonPattern)) {
76
+ try {
77
+ const obj = JSON.parse(match[0]);
78
+ return extractFromHeaderObject(obj);
79
+ }
80
+ catch {
81
+ // not valid JSON
82
+ }
83
+ }
84
+ // Try line-by-line JSON (Claude Code stderr format)
85
+ for (const line of text.split("\n")) {
86
+ const trimmed = line.trim();
87
+ if (!trimmed.startsWith("{"))
88
+ continue;
89
+ try {
90
+ const obj = JSON.parse(trimmed);
91
+ const headers = obj["headers"] ??
92
+ obj["response_headers"];
93
+ if (headers) {
94
+ const extracted = extractFromHeaderObject(headers);
95
+ if (extracted)
96
+ return extracted;
97
+ }
98
+ }
99
+ catch {
100
+ // not valid JSON
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ function extractFromHeaderObject(headers) {
106
+ const get = (keys) => {
107
+ for (const key of keys) {
108
+ const val = headers[key] ?? headers[key.toLowerCase()];
109
+ if (val != null) {
110
+ const n = typeof val === "number" ? val : parseInt(String(val), 10);
111
+ if (Number.isFinite(n) && n > 0)
112
+ return n;
113
+ }
114
+ }
115
+ return null;
116
+ };
117
+ const rpm = get([
118
+ "x-ratelimit-limit-requests",
119
+ "anthropic-ratelimit-requests-limit",
120
+ ]);
121
+ const tpm = get([
122
+ "x-ratelimit-limit-tokens",
123
+ "anthropic-ratelimit-tokens-limit",
124
+ ]);
125
+ if (rpm == null && tpm == null)
126
+ return null;
127
+ return {
128
+ requests_per_minute: rpm,
129
+ input_tokens_per_minute: tpm,
130
+ remaining_requests: get([
131
+ "x-ratelimit-remaining-requests",
132
+ "anthropic-ratelimit-requests-remaining",
133
+ ]),
134
+ remaining_tokens: get([
135
+ "x-ratelimit-remaining-tokens",
136
+ "anthropic-ratelimit-tokens-remaining",
137
+ ]),
138
+ reset_at: null,
139
+ };
140
+ }
@@ -0,0 +1,6 @@
1
+ import type { ExtractedRateLimits } from "../headerExtraction.js";
2
+ import type { HeaderExtractor } from "./genericHeaderExtractor.js";
3
+ export declare class ClaudeCodeHeaderExtractor implements HeaderExtractor {
4
+ readonly name = "claude-code";
5
+ extract(stderr: string): ExtractedRateLimits | null;
6
+ }
@@ -0,0 +1,28 @@
1
+ import { extractRateLimitHeaders } from "../headerExtraction.js";
2
+ export class ClaudeCodeHeaderExtractor {
3
+ name = "claude-code";
4
+ extract(stderr) {
5
+ // Claude Code emits structured JSON lines to stderr. Collect all lines
6
+ // that might contain header data and feed them to the agnostic parser.
7
+ const candidates = [];
8
+ for (const line of stderr.split("\n")) {
9
+ const trimmed = line.trim();
10
+ if (!trimmed.startsWith("{"))
11
+ continue;
12
+ try {
13
+ const obj = JSON.parse(trimmed);
14
+ if (obj["headers"] || obj["response_headers"]) {
15
+ candidates.push(trimmed);
16
+ }
17
+ }
18
+ catch {
19
+ // not JSON
20
+ }
21
+ }
22
+ if (candidates.length > 0) {
23
+ return extractRateLimitHeaders(candidates.join("\n"));
24
+ }
25
+ // Fall back to scanning the full text for raw header lines
26
+ return extractRateLimitHeaders(stderr);
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ import type { ExtractedRateLimits } from "../headerExtraction.js";
2
+ export interface HeaderExtractor {
3
+ readonly name: string;
4
+ extract(stderr: string): ExtractedRateLimits | null;
5
+ }
6
+ export declare class GenericHeaderExtractor implements HeaderExtractor {
7
+ readonly name = "generic";
8
+ extract(stderr: string): ExtractedRateLimits | null;
9
+ }
@@ -0,0 +1,7 @@
1
+ import { extractRateLimitHeaders } from "../headerExtraction.js";
2
+ export class GenericHeaderExtractor {
3
+ name = "generic";
4
+ extract(stderr) {
5
+ return extractRateLimitHeaders(stderr);
6
+ }
7
+ }
@@ -0,0 +1,5 @@
1
+ export type { HeaderExtractor } from "./genericHeaderExtractor.js";
2
+ export { GenericHeaderExtractor } from "./genericHeaderExtractor.js";
3
+ export { ClaudeCodeHeaderExtractor } from "./claudeCodeHeaderExtractor.js";
4
+ import type { HeaderExtractor } from "./genericHeaderExtractor.js";
5
+ export declare function getHeaderExtractorForProvider(providerName: string): HeaderExtractor;
@@ -0,0 +1,12 @@
1
+ export { GenericHeaderExtractor } from "./genericHeaderExtractor.js";
2
+ export { ClaudeCodeHeaderExtractor } from "./claudeCodeHeaderExtractor.js";
3
+ import { GenericHeaderExtractor } from "./genericHeaderExtractor.js";
4
+ import { ClaudeCodeHeaderExtractor } from "./claudeCodeHeaderExtractor.js";
5
+ const PROVIDER_EXTRACTORS = {
6
+ "claude-code": () => new ClaudeCodeHeaderExtractor(),
7
+ };
8
+ const genericExtractor = new GenericHeaderExtractor();
9
+ export function getHeaderExtractorForProvider(providerName) {
10
+ const factory = PROVIDER_EXTRACTORS[providerName];
11
+ return factory ? factory() : genericExtractor;
12
+ }
@@ -16,4 +16,10 @@ export type { ErrorParser } from "./errorParsers/index.js";
16
16
  export { GenericErrorParser, ClaudeCodeErrorParser, getErrorParserForProvider } from "./errorParsers/index.js";
17
17
  export { LearnedQuotaSource } from "./learnedQuotaSource.js";
18
18
  export { CompositeQuotaSource } from "./compositeQuotaSource.js";
19
+ export { lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, readDiscoveredLimitsCache, writeDiscoveredLimitsCache, } from "./discoveredLimits.js";
20
+ export type { DiscoveredRateLimits, DiscoveredLimitsCache, DiscoveredLimitsCacheEntry } from "./discoveredLimits.js";
21
+ export { extractRateLimitHeaders } from "./headerExtraction.js";
22
+ export type { ExtractedRateLimits } from "./headerExtraction.js";
23
+ export type { HeaderExtractor } from "./headerExtractors/index.js";
24
+ export { GenericHeaderExtractor, ClaudeCodeHeaderExtractor, getHeaderExtractorForProvider } from "./headerExtractors/index.js";
19
25
  export type { ResolvedLimits, LimitSource, LimitConfidence, HostConcurrencyLimit, HostConcurrencyLimitSource, QuotaState, QuotaStateEntry, ConcurrencyBucket, WaveSchedule, DispatchQuota, ObservedWaveOutcome, } from "./types.js";
@@ -9,3 +9,6 @@ export { probeProvider } from "./probe.js";
9
9
  export { GenericErrorParser, ClaudeCodeErrorParser, getErrorParserForProvider } from "./errorParsers/index.js";
10
10
  export { LearnedQuotaSource } from "./learnedQuotaSource.js";
11
11
  export { CompositeQuotaSource } from "./compositeQuotaSource.js";
12
+ export { lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, readDiscoveredLimitsCache, writeDiscoveredLimitsCache, } from "./discoveredLimits.js";
13
+ export { extractRateLimitHeaders } from "./headerExtraction.js";
14
+ export { GenericHeaderExtractor, ClaudeCodeHeaderExtractor, getHeaderExtractorForProvider } from "./headerExtractors/index.js";
@@ -1,6 +1,7 @@
1
1
  import type { ResolvedProviderName, SessionConfig } from "../types/sessionConfig.js";
2
2
  import type { HostConcurrencyLimit, QuotaStateEntry, WaveSchedule } from "./types.js";
3
3
  import type { QuotaUsageSnapshot } from "./quotaSource.js";
4
+ import type { DiscoveredRateLimits } from "./discoveredLimits.js";
4
5
  export interface ScheduleWaveOptions {
5
6
  providerName: ResolvedProviderName;
6
7
  sessionConfig: SessionConfig;
@@ -13,6 +14,8 @@ export interface ScheduleWaveOptions {
13
14
  quotaStateEntry?: QuotaStateEntry | null;
14
15
  hostConcurrencyLimit?: HostConcurrencyLimit | null;
15
16
  quotaSourceSnapshot?: QuotaUsageSnapshot | null;
17
+ /** RPM/TPM discovered from provider queries or response header extraction. */
18
+ discoveredLimits?: DiscoveredRateLimits | null;
16
19
  }
17
20
  export declare function scheduleWave(options: ScheduleWaveOptions): WaveSchedule;
18
21
  /** Build the state key used for indexing quota-state.json entries. */