specweave 1.0.556 → 1.0.557
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/dashboard/assets/index-C434W7yF.js +13 -0
- package/dist/dashboard/assets/index-YkogWPHY.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/src/core/auto/completion-evaluator.js +2 -2
- package/dist/src/core/auto/completion-evaluator.js.map +1 -1
- package/dist/src/core/cost/budget-monitor.d.ts +32 -0
- package/dist/src/core/cost/budget-monitor.d.ts.map +1 -0
- package/dist/src/core/cost/budget-monitor.js +63 -0
- package/dist/src/core/cost/budget-monitor.js.map +1 -0
- package/dist/src/core/cost/model-pricing-registry.d.ts +64 -0
- package/dist/src/core/cost/model-pricing-registry.d.ts.map +1 -0
- package/dist/src/core/cost/model-pricing-registry.js +113 -0
- package/dist/src/core/cost/model-pricing-registry.js.map +1 -0
- package/dist/src/core/cost/spend-aggregator.d.ts +68 -0
- package/dist/src/core/cost/spend-aggregator.d.ts.map +1 -0
- package/dist/src/core/cost/spend-aggregator.js +123 -0
- package/dist/src/core/cost/spend-aggregator.js.map +1 -0
- package/dist/src/core/cost/spend-pipeline.d.ts +27 -0
- package/dist/src/core/cost/spend-pipeline.d.ts.map +1 -0
- package/dist/src/core/cost/spend-pipeline.js +51 -0
- package/dist/src/core/cost/spend-pipeline.js.map +1 -0
- package/dist/src/core/cost/spend-record-reader.d.ts +23 -0
- package/dist/src/core/cost/spend-record-reader.d.ts.map +1 -0
- package/dist/src/core/cost/spend-record-reader.js +81 -0
- package/dist/src/core/cost/spend-record-reader.js.map +1 -0
- package/dist/src/core/cost/spend-record-writer.d.ts +18 -0
- package/dist/src/core/cost/spend-record-writer.d.ts.map +1 -0
- package/dist/src/core/cost/spend-record-writer.js +27 -0
- package/dist/src/core/cost/spend-record-writer.js.map +1 -0
- package/dist/src/core/cost/spend-record.d.ts +81 -0
- package/dist/src/core/cost/spend-record.d.ts.map +1 -0
- package/dist/src/core/cost/spend-record.js +60 -0
- package/dist/src/core/cost/spend-record.js.map +1 -0
- package/dist/src/core/cost/spend-tracking-provider.d.ts +43 -0
- package/dist/src/core/cost/spend-tracking-provider.d.ts.map +1 -0
- package/dist/src/core/cost/spend-tracking-provider.js +109 -0
- package/dist/src/core/cost/spend-tracking-provider.js.map +1 -0
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.js +3 -4
- package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
- package/dist/src/core/llm/provider-factory.d.ts.map +1 -1
- package/dist/src/core/llm/provider-factory.js +27 -8
- package/dist/src/core/llm/provider-factory.js.map +1 -1
- package/dist/src/core/llm/providers/claude-code-provider.d.ts.map +1 -1
- package/dist/src/core/llm/providers/claude-code-provider.js +2 -1
- package/dist/src/core/llm/providers/claude-code-provider.js.map +1 -1
- package/dist/src/dashboard/server/dashboard-server.d.ts.map +1 -1
- package/dist/src/dashboard/server/dashboard-server.js +58 -1
- package/dist/src/dashboard/server/dashboard-server.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard/assets/index-BMm0qiMB.css +0 -1
- package/dist/dashboard/assets/index-CBXB9ta5.js +0 -13
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendAggregator — daily rollups and provider breakdown from spend records
|
|
3
|
+
*
|
|
4
|
+
* Reads SpendRecord JSONL files and computes:
|
|
5
|
+
* - Daily cost rollups with per-provider breakdown
|
|
6
|
+
* - Overall provider cost summary
|
|
7
|
+
* - Budget status (current spend vs configured limits)
|
|
8
|
+
*
|
|
9
|
+
* Uses mtime-based cache invalidation (same pattern as CostAggregator).
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { SpendRecordReader } from './spend-record-reader.js';
|
|
14
|
+
export class SpendAggregator {
|
|
15
|
+
constructor(dir) {
|
|
16
|
+
this.cache = null;
|
|
17
|
+
this.dir = dir;
|
|
18
|
+
this.reader = new SpendRecordReader(dir);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get daily cost rollups, optionally filtered by date range
|
|
22
|
+
*/
|
|
23
|
+
getDailyRollups(from, to) {
|
|
24
|
+
const records = this.getRecords();
|
|
25
|
+
const filtered = from && to
|
|
26
|
+
? records.filter(r => {
|
|
27
|
+
const ts = new Date(r.timestamp);
|
|
28
|
+
return ts >= from && ts <= to;
|
|
29
|
+
})
|
|
30
|
+
: records;
|
|
31
|
+
const byDay = new Map();
|
|
32
|
+
for (const record of filtered) {
|
|
33
|
+
const date = record.timestamp.slice(0, 10); // YYYY-MM-DD
|
|
34
|
+
if (!byDay.has(date)) {
|
|
35
|
+
byDay.set(date, { date, total_cost: 0, total_tokens: 0, sessions: 0, providers: {} });
|
|
36
|
+
}
|
|
37
|
+
const day = byDay.get(date);
|
|
38
|
+
day.total_cost += record.cost_usd;
|
|
39
|
+
day.total_tokens += record.input_tokens + record.output_tokens;
|
|
40
|
+
day.sessions += 1;
|
|
41
|
+
if (!day.providers[record.provider]) {
|
|
42
|
+
day.providers[record.provider] = { cost: 0, tokens: 0, sessions: 0 };
|
|
43
|
+
}
|
|
44
|
+
day.providers[record.provider].cost += record.cost_usd;
|
|
45
|
+
day.providers[record.provider].tokens += record.input_tokens + record.output_tokens;
|
|
46
|
+
day.providers[record.provider].sessions += 1;
|
|
47
|
+
}
|
|
48
|
+
return [...byDay.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get cost breakdown by provider
|
|
52
|
+
*/
|
|
53
|
+
getProviderBreakdown() {
|
|
54
|
+
const records = this.getRecords();
|
|
55
|
+
const breakdown = {};
|
|
56
|
+
for (const record of records) {
|
|
57
|
+
if (!breakdown[record.provider]) {
|
|
58
|
+
breakdown[record.provider] = { total_cost: 0, total_tokens: 0, sessions: 0, models: {} };
|
|
59
|
+
}
|
|
60
|
+
const pb = breakdown[record.provider];
|
|
61
|
+
pb.total_cost += record.cost_usd;
|
|
62
|
+
pb.total_tokens += record.input_tokens + record.output_tokens;
|
|
63
|
+
pb.sessions += 1;
|
|
64
|
+
if (!pb.models[record.model]) {
|
|
65
|
+
pb.models[record.model] = { cost: 0, tokens: 0, sessions: 0 };
|
|
66
|
+
}
|
|
67
|
+
pb.models[record.model].cost += record.cost_usd;
|
|
68
|
+
pb.models[record.model].tokens += record.input_tokens + record.output_tokens;
|
|
69
|
+
pb.models[record.model].sessions += 1;
|
|
70
|
+
}
|
|
71
|
+
return breakdown;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get budget status for current month
|
|
75
|
+
*/
|
|
76
|
+
getBudgetStatus(config) {
|
|
77
|
+
if (!config?.monthly)
|
|
78
|
+
return null;
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
|
81
|
+
const monthEnd = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0, 23, 59, 59));
|
|
82
|
+
const records = this.getRecords();
|
|
83
|
+
const monthRecords = records.filter(r => {
|
|
84
|
+
const ts = new Date(r.timestamp);
|
|
85
|
+
return ts >= monthStart && ts <= monthEnd;
|
|
86
|
+
});
|
|
87
|
+
const currentSpend = monthRecords.reduce((sum, r) => sum + r.cost_usd, 0);
|
|
88
|
+
return {
|
|
89
|
+
currentSpend,
|
|
90
|
+
monthlyLimit: config.monthly,
|
|
91
|
+
percentUsed: (currentSpend / config.monthly) * 100,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Force cache invalidation
|
|
96
|
+
*/
|
|
97
|
+
invalidateCache() {
|
|
98
|
+
this.cache = null;
|
|
99
|
+
}
|
|
100
|
+
getRecords() {
|
|
101
|
+
const hash = this.computeMtimeHash();
|
|
102
|
+
if (this.cache && this.cache.mtimeHash === hash) {
|
|
103
|
+
return this.cache.records;
|
|
104
|
+
}
|
|
105
|
+
const records = this.reader.readAll();
|
|
106
|
+
this.cache = { records, mtimeHash: hash };
|
|
107
|
+
return records;
|
|
108
|
+
}
|
|
109
|
+
computeMtimeHash() {
|
|
110
|
+
if (!fs.existsSync(this.dir))
|
|
111
|
+
return '';
|
|
112
|
+
const files = fs.readdirSync(this.dir)
|
|
113
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
114
|
+
.sort();
|
|
115
|
+
return files
|
|
116
|
+
.map(f => {
|
|
117
|
+
const stat = fs.statSync(path.join(this.dir, f));
|
|
118
|
+
return `${f}:${stat.mtimeMs}:${stat.size}`;
|
|
119
|
+
})
|
|
120
|
+
.join('|');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=spend-aggregator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-aggregator.js","sourceRoot":"","sources":["../../../../src/core/cost/spend-aggregator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendPipeline — wires SpendEmitter to persistence and alerting
|
|
3
|
+
*
|
|
4
|
+
* Call initSpendPipeline() once at startup to connect:
|
|
5
|
+
* SpendEmitter → SpendRecordWriter (persistence)
|
|
6
|
+
* SpendEmitter → BudgetMonitor (alerting)
|
|
7
|
+
*/
|
|
8
|
+
import type { BudgetAlert } from './budget-monitor.js';
|
|
9
|
+
import type { BillingBudgetConfig } from './spend-aggregator.js';
|
|
10
|
+
export interface SpendPipelineOptions {
|
|
11
|
+
/** Directory for JSONL spend files (default: .specweave/state/spend) */
|
|
12
|
+
spendDir: string;
|
|
13
|
+
/** Budget configuration from config.json billing.budgets */
|
|
14
|
+
budgetConfig?: BillingBudgetConfig;
|
|
15
|
+
/** Callback when budget alert fires */
|
|
16
|
+
onBudgetAlert?: (alert: BudgetAlert) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the spend tracking pipeline.
|
|
20
|
+
* Safe to call multiple times — only initializes once.
|
|
21
|
+
*/
|
|
22
|
+
export declare function initSpendPipeline(options: SpendPipelineOptions): void;
|
|
23
|
+
/**
|
|
24
|
+
* Reset initialization state (for testing)
|
|
25
|
+
*/
|
|
26
|
+
export declare function resetSpendPipeline(): void;
|
|
27
|
+
//# sourceMappingURL=spend-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-pipeline.d.ts","sourceRoot":"","sources":["../../../../src/core/cost/spend-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAIjE,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,uCAAuC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAgCrE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendPipeline — wires SpendEmitter to persistence and alerting
|
|
3
|
+
*
|
|
4
|
+
* Call initSpendPipeline() once at startup to connect:
|
|
5
|
+
* SpendEmitter → SpendRecordWriter (persistence)
|
|
6
|
+
* SpendEmitter → BudgetMonitor (alerting)
|
|
7
|
+
*/
|
|
8
|
+
import { SpendEmitter } from './spend-record.js';
|
|
9
|
+
import { SpendRecordWriter } from './spend-record-writer.js';
|
|
10
|
+
import { BudgetMonitor } from './budget-monitor.js';
|
|
11
|
+
let initialized = false;
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the spend tracking pipeline.
|
|
14
|
+
* Safe to call multiple times — only initializes once.
|
|
15
|
+
*/
|
|
16
|
+
export function initSpendPipeline(options) {
|
|
17
|
+
if (initialized)
|
|
18
|
+
return;
|
|
19
|
+
initialized = true;
|
|
20
|
+
const emitter = SpendEmitter.getInstance();
|
|
21
|
+
const writer = new SpendRecordWriter(options.spendDir);
|
|
22
|
+
// Persist every spend record to JSONL
|
|
23
|
+
emitter.onSpend(record => {
|
|
24
|
+
try {
|
|
25
|
+
writer.write(record);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fail silently — don't crash the LLM call for a write failure
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// Budget alerting (if configured)
|
|
32
|
+
if (options.budgetConfig?.monthly || options.onBudgetAlert) {
|
|
33
|
+
const monitor = new BudgetMonitor(options.spendDir, options.budgetConfig, options.onBudgetAlert ?? (() => { }));
|
|
34
|
+
emitter.onSpend(record => {
|
|
35
|
+
try {
|
|
36
|
+
monitor.check(record);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Fail silently
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reset initialization state (for testing)
|
|
46
|
+
*/
|
|
47
|
+
export function resetSpendPipeline() {
|
|
48
|
+
initialized = false;
|
|
49
|
+
SpendEmitter.getInstance().removeAllListeners();
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=spend-pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-pipeline.js","sourceRoot":"","sources":["../../../../src/core/cost/spend-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIpD,IAAI,WAAW,GAAG,KAAK,CAAC;AAWxB;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA6B;IAC7D,IAAI,WAAW;QAAE,OAAO;IACxB,WAAW,GAAG,IAAI,CAAC;IAEnB,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEvD,sCAAsC;IACtC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;QACvB,IAAI,CAAC;YACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,+DAA+D;QACjE,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,kCAAkC;IAClC,IAAI,OAAO,CAAC,YAAY,EAAE,OAAO,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;QAC3D,MAAM,OAAO,GAAG,IAAI,aAAa,CAC/B,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,YAAY,EACpB,OAAO,CAAC,aAAa,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CACpC,CAAC;QAEF,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;YACvB,IAAI,CAAC;gBACH,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC;YAAC,MAAM,CAAC;gBACP,gBAAgB;YAClB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,WAAW,GAAG,KAAK,CAAC;IACpB,YAAY,CAAC,WAAW,EAAE,CAAC,kBAAkB,EAAE,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendRecordReader — reads and filters spend records from JSONL files
|
|
3
|
+
*
|
|
4
|
+
* Supports date-range queries across monthly-partitioned files.
|
|
5
|
+
*/
|
|
6
|
+
import type { SpendRecord } from './spend-record.js';
|
|
7
|
+
export declare class SpendRecordReader {
|
|
8
|
+
private readonly dir;
|
|
9
|
+
constructor(dir: string);
|
|
10
|
+
/**
|
|
11
|
+
* Read spend records within a date range
|
|
12
|
+
*/
|
|
13
|
+
readRange(from: Date, to: Date): SpendRecord[];
|
|
14
|
+
/**
|
|
15
|
+
* Read all spend records from all files
|
|
16
|
+
*/
|
|
17
|
+
readAll(): SpendRecord[];
|
|
18
|
+
/**
|
|
19
|
+
* Determine which monthly files might contain records in the given range
|
|
20
|
+
*/
|
|
21
|
+
private getRelevantFiles;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=spend-record-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-record-reader.d.ts","sourceRoot":"","sources":["../../../../src/core/cost/spend-record-reader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,EAAE,MAAM;IAIvB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,GAAG,WAAW,EAAE;IA0B9C;;OAEG;IACH,OAAO,IAAI,WAAW,EAAE;IAwBxB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAczB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendRecordReader — reads and filters spend records from JSONL files
|
|
3
|
+
*
|
|
4
|
+
* Supports date-range queries across monthly-partitioned files.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
export class SpendRecordReader {
|
|
9
|
+
constructor(dir) {
|
|
10
|
+
this.dir = dir;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read spend records within a date range
|
|
14
|
+
*/
|
|
15
|
+
readRange(from, to) {
|
|
16
|
+
const files = this.getRelevantFiles(from, to);
|
|
17
|
+
const records = [];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const filePath = path.join(this.dir, file);
|
|
20
|
+
if (!fs.existsSync(filePath))
|
|
21
|
+
continue;
|
|
22
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
23
|
+
for (const line of content.split('\n')) {
|
|
24
|
+
if (!line.trim())
|
|
25
|
+
continue;
|
|
26
|
+
try {
|
|
27
|
+
const record = JSON.parse(line);
|
|
28
|
+
const ts = new Date(record.timestamp);
|
|
29
|
+
if (ts >= from && ts <= to) {
|
|
30
|
+
records.push(record);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Skip malformed lines
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return records;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read all spend records from all files
|
|
42
|
+
*/
|
|
43
|
+
readAll() {
|
|
44
|
+
if (!fs.existsSync(this.dir))
|
|
45
|
+
return [];
|
|
46
|
+
const files = fs.readdirSync(this.dir)
|
|
47
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
48
|
+
.sort();
|
|
49
|
+
const records = [];
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const content = fs.readFileSync(path.join(this.dir, file), 'utf-8');
|
|
52
|
+
for (const line of content.split('\n')) {
|
|
53
|
+
if (!line.trim())
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
records.push(JSON.parse(line));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Skip malformed lines
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return records;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Determine which monthly files might contain records in the given range
|
|
67
|
+
*/
|
|
68
|
+
getRelevantFiles(from, to) {
|
|
69
|
+
const files = [];
|
|
70
|
+
const current = new Date(from);
|
|
71
|
+
current.setUTCDate(1); // Start of month
|
|
72
|
+
while (current <= to) {
|
|
73
|
+
const year = current.getUTCFullYear();
|
|
74
|
+
const month = String(current.getUTCMonth() + 1).padStart(2, '0');
|
|
75
|
+
files.push(`${year}-${month}.jsonl`);
|
|
76
|
+
current.setUTCMonth(current.getUTCMonth() + 1);
|
|
77
|
+
}
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=spend-record-reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-record-reader.js","sourceRoot":"","sources":["../../../../src/core/cost/spend-record-reader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendRecordWriter — JSONL persistence for spend records
|
|
3
|
+
*
|
|
4
|
+
* Appends SpendRecords to monthly-partitioned JSONL files:
|
|
5
|
+
* .specweave/state/spend/YYYY-MM.jsonl
|
|
6
|
+
*
|
|
7
|
+
* Uses O_APPEND for atomic writes (safe for concurrent processes).
|
|
8
|
+
*/
|
|
9
|
+
import type { SpendRecord } from './spend-record.js';
|
|
10
|
+
export declare class SpendRecordWriter {
|
|
11
|
+
private readonly dir;
|
|
12
|
+
constructor(dir: string);
|
|
13
|
+
/**
|
|
14
|
+
* Append a SpendRecord to the appropriate monthly JSONL file
|
|
15
|
+
*/
|
|
16
|
+
write(record: SpendRecord): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=spend-record-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-record-writer.d.ts","sourceRoot":"","sources":["../../../../src/core/cost/spend-record-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,EAAE,MAAM;IAIvB;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;CAUjC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendRecordWriter — JSONL persistence for spend records
|
|
3
|
+
*
|
|
4
|
+
* Appends SpendRecords to monthly-partitioned JSONL files:
|
|
5
|
+
* .specweave/state/spend/YYYY-MM.jsonl
|
|
6
|
+
*
|
|
7
|
+
* Uses O_APPEND for atomic writes (safe for concurrent processes).
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
export class SpendRecordWriter {
|
|
12
|
+
constructor(dir) {
|
|
13
|
+
this.dir = dir;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Append a SpendRecord to the appropriate monthly JSONL file
|
|
17
|
+
*/
|
|
18
|
+
write(record) {
|
|
19
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
20
|
+
const partition = record.timestamp.slice(0, 7); // YYYY-MM
|
|
21
|
+
const filePath = path.join(this.dir, `${partition}.jsonl`);
|
|
22
|
+
const line = JSON.stringify(record) + '\n';
|
|
23
|
+
// O_APPEND ensures atomic line writes even with concurrent processes
|
|
24
|
+
fs.appendFileSync(filePath, line, { flag: 'a' });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=spend-record-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-record-writer.js","sourceRoot":"","sources":["../../../../src/core/cost/spend-record-writer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,MAAM,OAAO,iBAAiB;IAG5B,YAAY,GAAW;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,MAAmB;QACvB,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE5C,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU;QAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,QAAQ,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;QAE3C,qEAAqE;QACrE,EAAE,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;IACnD,CAAC;CACF"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendRecord type and SpendEmitter
|
|
3
|
+
*
|
|
4
|
+
* Foundation for multi-model spend tracking. SpendRecord captures per-request
|
|
5
|
+
* cost data across all LLM providers. SpendEmitter is a singleton EventEmitter
|
|
6
|
+
* that decouples tracking from persistence and alerting.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
/**
|
|
10
|
+
* Unified spend record for any LLM provider request
|
|
11
|
+
*/
|
|
12
|
+
export interface SpendRecord {
|
|
13
|
+
/** Unique record ID (UUID v4) */
|
|
14
|
+
id: string;
|
|
15
|
+
/** ISO 8601 timestamp */
|
|
16
|
+
timestamp: string;
|
|
17
|
+
/** Provider identifier */
|
|
18
|
+
provider: string;
|
|
19
|
+
/** Full model ID */
|
|
20
|
+
model: string;
|
|
21
|
+
/** Input token count */
|
|
22
|
+
input_tokens: number;
|
|
23
|
+
/** Output token count */
|
|
24
|
+
output_tokens: number;
|
|
25
|
+
/** Cached input tokens read (discount pricing) */
|
|
26
|
+
cached_tokens: number;
|
|
27
|
+
/** Cache write tokens (surcharge pricing) */
|
|
28
|
+
cache_write_tokens: number;
|
|
29
|
+
/** Reasoning/thinking tokens */
|
|
30
|
+
reasoning_tokens: number;
|
|
31
|
+
/** Calculated cost in USD */
|
|
32
|
+
cost_usd: number;
|
|
33
|
+
/** How cost was determined */
|
|
34
|
+
cost_source: 'calculated' | 'api_reported' | 'unknown' | 'local';
|
|
35
|
+
/** SpecWeave project name */
|
|
36
|
+
project?: string;
|
|
37
|
+
/** Increment ID if in increment context */
|
|
38
|
+
increment_id?: string;
|
|
39
|
+
/** Session identifier */
|
|
40
|
+
session_id?: string;
|
|
41
|
+
/** Request duration in milliseconds */
|
|
42
|
+
duration_ms?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Input for creating a SpendRecord (required fields + optional overrides)
|
|
46
|
+
*/
|
|
47
|
+
export interface SpendRecordInput {
|
|
48
|
+
provider: string;
|
|
49
|
+
model: string;
|
|
50
|
+
input_tokens: number;
|
|
51
|
+
output_tokens: number;
|
|
52
|
+
cached_tokens?: number;
|
|
53
|
+
cache_write_tokens?: number;
|
|
54
|
+
reasoning_tokens?: number;
|
|
55
|
+
cost_usd: number;
|
|
56
|
+
cost_source: SpendRecord['cost_source'];
|
|
57
|
+
project?: string;
|
|
58
|
+
increment_id?: string;
|
|
59
|
+
session_id?: string;
|
|
60
|
+
duration_ms?: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a SpendRecord with generated ID and timestamp
|
|
64
|
+
*/
|
|
65
|
+
export declare function createSpendRecord(input: SpendRecordInput): SpendRecord;
|
|
66
|
+
/**
|
|
67
|
+
* Singleton EventEmitter for spend tracking events.
|
|
68
|
+
* Decouples spend record creation from persistence and alerting.
|
|
69
|
+
*/
|
|
70
|
+
export declare class SpendEmitter extends EventEmitter {
|
|
71
|
+
private static instance;
|
|
72
|
+
private constructor();
|
|
73
|
+
static getInstance(): SpendEmitter;
|
|
74
|
+
/** Type-safe emit for spend events */
|
|
75
|
+
emitSpend(record: SpendRecord): boolean;
|
|
76
|
+
/** Type-safe listener registration */
|
|
77
|
+
onSpend(listener: (record: SpendRecord) => void): this;
|
|
78
|
+
/** Type-safe listener removal */
|
|
79
|
+
offSpend(listener: (record: SpendRecord) => void): this;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=spend-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-record.d.ts","sourceRoot":"","sources":["../../../../src/core/cost/spend-record.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,yBAAyB;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,aAAa,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gCAAgC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,WAAW,EAAE,YAAY,GAAG,cAAc,GAAG,SAAS,GAAG,OAAO,CAAC;IACjE,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yBAAyB;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAkBtE;AAED;;;GAGG;AACH,qBAAa,YAAa,SAAQ,YAAY;IAC5C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAe;IAEtC,OAAO;IAKP,MAAM,CAAC,WAAW,IAAI,YAAY;IAOlC,sCAAsC;IACtC,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO;IAIvC,sCAAsC;IACtC,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAItD,iCAAiC;IACjC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;CAGxD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendRecord type and SpendEmitter
|
|
3
|
+
*
|
|
4
|
+
* Foundation for multi-model spend tracking. SpendRecord captures per-request
|
|
5
|
+
* cost data across all LLM providers. SpendEmitter is a singleton EventEmitter
|
|
6
|
+
* that decouples tracking from persistence and alerting.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
/**
|
|
11
|
+
* Create a SpendRecord with generated ID and timestamp
|
|
12
|
+
*/
|
|
13
|
+
export function createSpendRecord(input) {
|
|
14
|
+
return {
|
|
15
|
+
id: randomUUID(),
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
provider: input.provider,
|
|
18
|
+
model: input.model,
|
|
19
|
+
input_tokens: input.input_tokens,
|
|
20
|
+
output_tokens: input.output_tokens,
|
|
21
|
+
cached_tokens: input.cached_tokens ?? 0,
|
|
22
|
+
cache_write_tokens: input.cache_write_tokens ?? 0,
|
|
23
|
+
reasoning_tokens: input.reasoning_tokens ?? 0,
|
|
24
|
+
cost_usd: input.cost_usd,
|
|
25
|
+
cost_source: input.cost_source,
|
|
26
|
+
project: input.project,
|
|
27
|
+
increment_id: input.increment_id,
|
|
28
|
+
session_id: input.session_id,
|
|
29
|
+
duration_ms: input.duration_ms,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Singleton EventEmitter for spend tracking events.
|
|
34
|
+
* Decouples spend record creation from persistence and alerting.
|
|
35
|
+
*/
|
|
36
|
+
export class SpendEmitter extends EventEmitter {
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
this.setMaxListeners(20);
|
|
40
|
+
}
|
|
41
|
+
static getInstance() {
|
|
42
|
+
if (!SpendEmitter.instance) {
|
|
43
|
+
SpendEmitter.instance = new SpendEmitter();
|
|
44
|
+
}
|
|
45
|
+
return SpendEmitter.instance;
|
|
46
|
+
}
|
|
47
|
+
/** Type-safe emit for spend events */
|
|
48
|
+
emitSpend(record) {
|
|
49
|
+
return this.emit('spend', record);
|
|
50
|
+
}
|
|
51
|
+
/** Type-safe listener registration */
|
|
52
|
+
onSpend(listener) {
|
|
53
|
+
return this.on('spend', listener);
|
|
54
|
+
}
|
|
55
|
+
/** Type-safe listener removal */
|
|
56
|
+
offSpend(listener) {
|
|
57
|
+
return this.off('spend', listener);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=spend-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-record.js","sourceRoot":"","sources":["../../../../src/core/cost/spend-record.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAyDpC;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAuB;IACvD,OAAO;QACL,EAAE,EAAE,UAAU,EAAE;QAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,CAAC;QACvC,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,IAAI,CAAC;QACjD,gBAAgB,EAAE,KAAK,CAAC,gBAAgB,IAAI,CAAC;QAC7C,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;KAC/B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,YAAa,SAAQ,YAAY;IAG5C;QACE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,WAAW;QAChB,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC;YAC3B,YAAY,CAAC,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAC;QAC7C,CAAC;QACD,OAAO,YAAY,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAED,sCAAsC;IACtC,SAAS,CAAC,MAAmB;QAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAED,sCAAsC;IACtC,OAAO,CAAC,QAAuC;QAC7C,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACpC,CAAC;IAED,iCAAiC;IACjC,QAAQ,CAAC,QAAuC;QAC9C,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACrC,CAAC;CACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendTrackingProvider — LLMProvider decorator
|
|
3
|
+
*
|
|
4
|
+
* Wraps any LLMProvider to capture token usage from every analyze() call
|
|
5
|
+
* and emit a SpendRecord via the SpendEmitter singleton. Uses the same
|
|
6
|
+
* decorator pattern as the existing BudgetGuardProvider.
|
|
7
|
+
*
|
|
8
|
+
* Zero changes required to individual provider implementations.
|
|
9
|
+
*/
|
|
10
|
+
import type { LLMProvider, LLMProviderType, AnalyzeResult, AnalyzeOptions, StructuredOptions } from '../llm/types.js';
|
|
11
|
+
export interface SpendTrackingContext {
|
|
12
|
+
incrementId?: string;
|
|
13
|
+
project?: string;
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Decorator that intercepts LLMProvider.analyze() results and emits
|
|
18
|
+
* SpendRecords for every successful request with token usage data.
|
|
19
|
+
*/
|
|
20
|
+
export declare class SpendTrackingProvider implements LLMProvider {
|
|
21
|
+
private readonly inner;
|
|
22
|
+
private readonly emitter;
|
|
23
|
+
private readonly registry;
|
|
24
|
+
private readonly context;
|
|
25
|
+
constructor(inner: LLMProvider, context?: SpendTrackingContext);
|
|
26
|
+
get name(): LLMProviderType;
|
|
27
|
+
get defaultModel(): string;
|
|
28
|
+
analyze(prompt: string, options?: AnalyzeOptions): Promise<AnalyzeResult>;
|
|
29
|
+
analyzeStructured<T>(prompt: string, options: StructuredOptions<T>): Promise<{
|
|
30
|
+
data: T;
|
|
31
|
+
usage: AnalyzeResult['usage'];
|
|
32
|
+
estimatedCost: number;
|
|
33
|
+
}>;
|
|
34
|
+
estimateCost(inputTokens: number, outputTokens: number, model?: string): number;
|
|
35
|
+
isAvailable(): Promise<boolean>;
|
|
36
|
+
getStatus(): Promise<{
|
|
37
|
+
available: boolean;
|
|
38
|
+
latencyMs?: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
}>;
|
|
41
|
+
private emitSpendRecord;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=spend-tracking-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend-tracking-provider.d.ts","sourceRoot":"","sources":["../../../../src/core/cost/spend-tracking-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,aAAa,EACb,cAAc,EACd,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAIzB,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,WAAW;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;gBAEnC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,oBAAyB;IAOlE,IAAI,IAAI,IAAI,eAAe,CAE1B;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAEK,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAMzE,iBAAiB,CAAC,CAAC,EACvB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC5B,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,CAAC;QAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IAe7E,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM;IAI/E,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/B,SAAS,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAIhF,OAAO,CAAC,eAAe;CAsDxB"}
|