token-optimizer-opencode 1.0.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.
Files changed (90) hide show
  1. package/README.md +127 -0
  2. package/dist/activity/intel.d.ts +10 -0
  3. package/dist/activity/intel.d.ts.map +1 -0
  4. package/dist/activity/intel.js +26 -0
  5. package/dist/activity/intel.js.map +1 -0
  6. package/dist/activity/tracker.d.ts +12 -0
  7. package/dist/activity/tracker.d.ts.map +1 -0
  8. package/dist/activity/tracker.js +101 -0
  9. package/dist/activity/tracker.js.map +1 -0
  10. package/dist/compaction/checkpoint.d.ts +17 -0
  11. package/dist/compaction/checkpoint.d.ts.map +1 -0
  12. package/dist/compaction/checkpoint.js +82 -0
  13. package/dist/compaction/checkpoint.js.map +1 -0
  14. package/dist/compaction/dynamic-instructions.d.ts +3 -0
  15. package/dist/compaction/dynamic-instructions.d.ts.map +1 -0
  16. package/dist/compaction/dynamic-instructions.js +54 -0
  17. package/dist/compaction/dynamic-instructions.js.map +1 -0
  18. package/dist/continuity/matcher.d.ts +14 -0
  19. package/dist/continuity/matcher.d.ts.map +1 -0
  20. package/dist/continuity/matcher.js +57 -0
  21. package/dist/continuity/matcher.js.map +1 -0
  22. package/dist/continuity/restore.d.ts +4 -0
  23. package/dist/continuity/restore.d.ts.map +1 -0
  24. package/dist/continuity/restore.js +53 -0
  25. package/dist/continuity/restore.js.map +1 -0
  26. package/dist/dashboard/generator.d.ts +8 -0
  27. package/dist/dashboard/generator.d.ts.map +1 -0
  28. package/dist/dashboard/generator.js +282 -0
  29. package/dist/dashboard/generator.js.map +1 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +447 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/nudges/loop-detection.d.ts +6 -0
  35. package/dist/nudges/loop-detection.d.ts.map +1 -0
  36. package/dist/nudges/loop-detection.js +56 -0
  37. package/dist/nudges/loop-detection.js.map +1 -0
  38. package/dist/nudges/quality-nudge.d.ts +7 -0
  39. package/dist/nudges/quality-nudge.d.ts.map +1 -0
  40. package/dist/nudges/quality-nudge.js +28 -0
  41. package/dist/nudges/quality-nudge.js.map +1 -0
  42. package/dist/nudges/tool-call-warn.d.ts +7 -0
  43. package/dist/nudges/tool-call-warn.d.ts.map +1 -0
  44. package/dist/nudges/tool-call-warn.js +20 -0
  45. package/dist/nudges/tool-call-warn.js.map +1 -0
  46. package/dist/plugin.d.ts +9 -0
  47. package/dist/plugin.d.ts.map +1 -0
  48. package/dist/plugin.js +5 -0
  49. package/dist/plugin.js.map +1 -0
  50. package/dist/quality/curves.d.ts +5 -0
  51. package/dist/quality/curves.d.ts.map +1 -0
  52. package/dist/quality/curves.js +102 -0
  53. package/dist/quality/curves.js.map +1 -0
  54. package/dist/quality/scoring.d.ts +39 -0
  55. package/dist/quality/scoring.d.ts.map +1 -0
  56. package/dist/quality/scoring.js +239 -0
  57. package/dist/quality/scoring.js.map +1 -0
  58. package/dist/quality/signals.d.ts +23 -0
  59. package/dist/quality/signals.d.ts.map +1 -0
  60. package/dist/quality/signals.js +100 -0
  61. package/dist/quality/signals.js.map +1 -0
  62. package/dist/storage/session-store.d.ts +73 -0
  63. package/dist/storage/session-store.d.ts.map +1 -0
  64. package/dist/storage/session-store.js +259 -0
  65. package/dist/storage/session-store.js.map +1 -0
  66. package/dist/storage/trends.d.ts +28 -0
  67. package/dist/storage/trends.d.ts.map +1 -0
  68. package/dist/storage/trends.js +125 -0
  69. package/dist/storage/trends.js.map +1 -0
  70. package/dist/tools/dashboard.d.ts +3 -0
  71. package/dist/tools/dashboard.d.ts.map +1 -0
  72. package/dist/tools/dashboard.js +45 -0
  73. package/dist/tools/dashboard.js.map +1 -0
  74. package/dist/tools/token-status.d.ts +9 -0
  75. package/dist/tools/token-status.d.ts.map +1 -0
  76. package/dist/tools/token-status.js +51 -0
  77. package/dist/tools/token-status.js.map +1 -0
  78. package/dist/util/context-window.d.ts +2 -0
  79. package/dist/util/context-window.d.ts.map +1 -0
  80. package/dist/util/context-window.js +83 -0
  81. package/dist/util/context-window.js.map +1 -0
  82. package/dist/util/env.d.ts +23 -0
  83. package/dist/util/env.d.ts.map +1 -0
  84. package/dist/util/env.js +63 -0
  85. package/dist/util/env.js.map +1 -0
  86. package/dist/util/grade.d.ts +4 -0
  87. package/dist/util/grade.d.ts.map +1 -0
  88. package/dist/util/grade.js +32 -0
  89. package/dist/util/grade.js.map +1 -0
  90. package/package.json +74 -0
@@ -0,0 +1,259 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ /** Reduce a session id to the charset used for its DB filename and for echoing
5
+ * the id into checkpoint content. Single source of truth for all three callers. */
6
+ export function sanitizeSessionId(id) {
7
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
8
+ }
9
+ const SCHEMA = `
10
+ CREATE TABLE IF NOT EXISTS activity_log (
11
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
12
+ tool_name TEXT NOT NULL,
13
+ tool_bucket TEXT NOT NULL,
14
+ has_error INTEGER NOT NULL DEFAULT 0,
15
+ result_size INTEGER DEFAULT 0,
16
+ timestamp REAL NOT NULL
17
+ );
18
+
19
+ CREATE TABLE IF NOT EXISTS session_meta (
20
+ key TEXT PRIMARY KEY,
21
+ value TEXT NOT NULL
22
+ );
23
+
24
+ CREATE TABLE IF NOT EXISTS quality_cache (
25
+ id INTEGER PRIMARY KEY CHECK (id = 1),
26
+ resource_health REAL,
27
+ session_efficiency REAL,
28
+ fill_pct REAL,
29
+ compactions INTEGER DEFAULT 0,
30
+ tool_calls INTEGER DEFAULT 0,
31
+ last_nudge_time REAL DEFAULT 0,
32
+ nudge_count INTEGER DEFAULT 0,
33
+ data TEXT,
34
+ updated_at REAL NOT NULL
35
+ );
36
+
37
+ CREATE TABLE IF NOT EXISTS checkpoints (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ session_id TEXT NOT NULL,
40
+ trigger TEXT NOT NULL,
41
+ mode TEXT,
42
+ quality_score REAL,
43
+ fill_pct REAL,
44
+ active_files TEXT,
45
+ decisions TEXT,
46
+ content TEXT NOT NULL,
47
+ created_at REAL NOT NULL
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS reads (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ idx INTEGER NOT NULL,
53
+ path TEXT NOT NULL,
54
+ timestamp REAL NOT NULL
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS writes (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ idx INTEGER NOT NULL,
60
+ path TEXT NOT NULL,
61
+ timestamp REAL NOT NULL
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS tool_results (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ idx INTEGER NOT NULL,
67
+ tool_name TEXT NOT NULL,
68
+ result_size INTEGER NOT NULL,
69
+ is_failure INTEGER NOT NULL DEFAULT 0,
70
+ timestamp REAL NOT NULL
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS messages (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ idx INTEGER NOT NULL,
76
+ role TEXT NOT NULL,
77
+ text_length INTEGER NOT NULL,
78
+ is_substantive INTEGER NOT NULL DEFAULT 0,
79
+ timestamp REAL NOT NULL
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS agent_dispatches (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ idx INTEGER NOT NULL,
85
+ prompt_size INTEGER NOT NULL,
86
+ result_size INTEGER NOT NULL DEFAULT 0,
87
+ timestamp REAL NOT NULL
88
+ );
89
+ `;
90
+ export class SessionStore {
91
+ db = null;
92
+ dbPath;
93
+ constructor(dataDir, sessionId) {
94
+ const sessDir = join(dataDir, "token-optimizer", "sessions");
95
+ if (!existsSync(sessDir)) {
96
+ mkdirSync(sessDir, { recursive: true });
97
+ }
98
+ this.dbPath = join(sessDir, `${sanitizeSessionId(sessionId)}.db`);
99
+ }
100
+ connect() {
101
+ if (!this.db) {
102
+ this.db = new Database(this.dbPath, { create: true });
103
+ this.db.exec("PRAGMA journal_mode=WAL");
104
+ this.db.exec("PRAGMA busy_timeout=3000");
105
+ this.db.exec(SCHEMA);
106
+ }
107
+ return this.db;
108
+ }
109
+ close() {
110
+ if (this.db) {
111
+ this.db.close();
112
+ this.db = null;
113
+ }
114
+ }
115
+ getMeta(key) {
116
+ const db = this.connect();
117
+ const row = db.query("SELECT value FROM session_meta WHERE key = ?").get(key);
118
+ return row?.value;
119
+ }
120
+ setMeta(key, value) {
121
+ const db = this.connect();
122
+ db.run("INSERT OR REPLACE INTO session_meta (key, value) VALUES (?, ?)", [key, value]);
123
+ }
124
+ getQualityCache() {
125
+ const db = this.connect();
126
+ const row = db.query("SELECT * FROM quality_cache WHERE id = 1").get();
127
+ return row;
128
+ }
129
+ writeQualityCache(cache) {
130
+ const db = this.connect();
131
+ // True upsert (ON CONFLICT DO UPDATE), not INSERT OR REPLACE: REPLACE is a
132
+ // DELETE+INSERT that briefly drops the row and resets any column not listed.
133
+ db.run(`INSERT INTO quality_cache (id, resource_health, session_efficiency, fill_pct, compactions, tool_calls, last_nudge_time, nudge_count, data, updated_at)
134
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
135
+ ON CONFLICT(id) DO UPDATE SET
136
+ resource_health=excluded.resource_health,
137
+ session_efficiency=excluded.session_efficiency,
138
+ fill_pct=excluded.fill_pct,
139
+ compactions=excluded.compactions,
140
+ tool_calls=excluded.tool_calls,
141
+ last_nudge_time=excluded.last_nudge_time,
142
+ nudge_count=excluded.nudge_count,
143
+ data=excluded.data,
144
+ updated_at=excluded.updated_at`, [
145
+ cache.resource_health,
146
+ cache.session_efficiency,
147
+ cache.fill_pct,
148
+ cache.compactions,
149
+ cache.tool_calls,
150
+ cache.last_nudge_time,
151
+ cache.nudge_count,
152
+ cache.data,
153
+ Date.now() / 1000,
154
+ ]);
155
+ }
156
+ recordRead(idx, path) {
157
+ const db = this.connect();
158
+ db.run("INSERT INTO reads (idx, path, timestamp) VALUES (?, ?, ?)", [idx, path, Date.now() / 1000]);
159
+ }
160
+ recordWrite(idx, path) {
161
+ const db = this.connect();
162
+ db.run("INSERT INTO writes (idx, path, timestamp) VALUES (?, ?, ?)", [idx, path, Date.now() / 1000]);
163
+ }
164
+ recordToolResult(idx, toolName, resultSize, isFailure) {
165
+ const db = this.connect();
166
+ db.run("INSERT INTO tool_results (idx, tool_name, result_size, is_failure, timestamp) VALUES (?, ?, ?, ?, ?)", [idx, toolName, resultSize, isFailure ? 1 : 0, Date.now() / 1000]);
167
+ }
168
+ recordMessage(idx, role, textLength, isSubstantive) {
169
+ const db = this.connect();
170
+ db.run("INSERT INTO messages (idx, role, text_length, is_substantive, timestamp) VALUES (?, ?, ?, ?, ?)", [idx, role, textLength, isSubstantive ? 1 : 0, Date.now() / 1000]);
171
+ }
172
+ recordAgentDispatch(idx, promptSize, resultSize) {
173
+ const db = this.connect();
174
+ db.run("INSERT INTO agent_dispatches (idx, prompt_size, result_size, timestamp) VALUES (?, ?, ?, ?)", [idx, promptSize, resultSize, Date.now() / 1000]);
175
+ }
176
+ getRecentReads(limit) {
177
+ const db = this.connect();
178
+ return db
179
+ .query("SELECT idx, path FROM reads ORDER BY id DESC LIMIT ?")
180
+ .all(limit);
181
+ }
182
+ getRecentWrites(limit) {
183
+ const db = this.connect();
184
+ return db
185
+ .query("SELECT idx, path FROM writes ORDER BY id DESC LIMIT ?")
186
+ .all(limit);
187
+ }
188
+ getRecentToolResults(limit) {
189
+ const db = this.connect();
190
+ return db
191
+ .query("SELECT idx, tool_name, result_size, is_failure FROM tool_results ORDER BY id DESC LIMIT ?")
192
+ .all(limit);
193
+ }
194
+ getRecentMessages(limit) {
195
+ const db = this.connect();
196
+ return db
197
+ .query("SELECT idx, role, text_length, is_substantive FROM messages ORDER BY id DESC LIMIT ?")
198
+ .all(limit);
199
+ }
200
+ getRecentAgentDispatches(limit) {
201
+ const db = this.connect();
202
+ return db
203
+ .query("SELECT idx, prompt_size, result_size FROM agent_dispatches ORDER BY id DESC LIMIT ?")
204
+ .all(limit);
205
+ }
206
+ safeParseInt(value) {
207
+ const parsed = parseInt(value ?? "0", 10);
208
+ return isNaN(parsed) ? 0 : parsed;
209
+ }
210
+ atomicIncrement(key) {
211
+ const db = this.connect();
212
+ // Single round-trip: RETURNING gives us the post-increment value without a
213
+ // follow-up SELECT (SQLite >= 3.35, shipped in bun:sqlite).
214
+ const row = db
215
+ .query("INSERT INTO session_meta (key, value) VALUES (?, '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) RETURNING CAST(value AS INTEGER) AS v")
216
+ .get(key);
217
+ return row?.v ?? 1;
218
+ }
219
+ /**
220
+ * Cap each per-session signal table to its most recent `maxRows` rows.
221
+ * Without this, a long session that never compacts grows these tables
222
+ * unbounded. Table names are a fixed allowlist, never user input.
223
+ */
224
+ capSignalTables(maxRows) {
225
+ const db = this.connect();
226
+ db.transaction(() => {
227
+ for (const table of ["reads", "writes", "tool_results", "messages", "agent_dispatches"]) {
228
+ db.run(`DELETE FROM ${table} WHERE id NOT IN (SELECT id FROM ${table} ORDER BY id DESC LIMIT ?)`, [maxRows]);
229
+ }
230
+ })();
231
+ }
232
+ getCompactionCount() {
233
+ return this.safeParseInt(this.getMeta("compaction_count"));
234
+ }
235
+ incrementCompaction() {
236
+ return this.atomicIncrement("compaction_count");
237
+ }
238
+ getToolCallCount() {
239
+ return this.safeParseInt(this.getMeta("tool_call_count"));
240
+ }
241
+ incrementToolCallCount() {
242
+ return this.atomicIncrement("tool_call_count");
243
+ }
244
+ getOperationIndex() {
245
+ return this.safeParseInt(this.getMeta("operation_index"));
246
+ }
247
+ incrementOperationIndex() {
248
+ return this.atomicIncrement("operation_index");
249
+ }
250
+ resetSignalAccumulators() {
251
+ const db = this.connect();
252
+ db.run("DELETE FROM reads");
253
+ db.run("DELETE FROM writes");
254
+ db.run("DELETE FROM tool_results");
255
+ db.run("DELETE FROM messages");
256
+ db.run("DELETE FROM agent_dispatches");
257
+ }
258
+ }
259
+ //# sourceMappingURL=session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.js","sourceRoot":"","sources":["../../src/storage/session-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ export interface SessionTrendData {
2
+ sessionId: string;
3
+ project: string | null;
4
+ model: string | null;
5
+ tokensInput: number;
6
+ tokensOutput: number;
7
+ tokensCacheRead: number;
8
+ tokensCacheWrite: number;
9
+ costUsd: number;
10
+ resourceHealth: number | null;
11
+ sessionEfficiency: number | null;
12
+ toolCalls: number;
13
+ compactions: number;
14
+ mode: string | null;
15
+ durationSeconds: number;
16
+ }
17
+ export declare class TrendsStore {
18
+ private db;
19
+ private dbPath;
20
+ constructor(dataDir: string);
21
+ private connect;
22
+ close(): void;
23
+ recordSession(data: SessionTrendData): void;
24
+ getRecentSessions(days?: number): Array<Record<string, unknown>>;
25
+ getDailyStats(days?: number): Array<Record<string, unknown>>;
26
+ generateDashboardData(days?: number): string;
27
+ }
28
+ //# sourceMappingURL=trends.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trends.d.ts","sourceRoot":"","sources":["../../src/storage/trends.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,MAAM;IAQ3B,OAAO,CAAC,OAAO;IAUf,KAAK,IAAI,IAAI;IAOb,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAwC3C,iBAAiB,CAAC,IAAI,GAAE,MAAW,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAWpE,aAAa,CAAC,IAAI,GAAE,MAAW,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAuBhE,qBAAqB,CAAC,IAAI,GAAE,MAAW,GAAG,MAAM;CAejD"}
@@ -0,0 +1,125 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ const TRENDS_SCHEMA = `
5
+ CREATE TABLE IF NOT EXISTS session_log (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ session_id TEXT NOT NULL UNIQUE,
8
+ date TEXT NOT NULL,
9
+ project TEXT,
10
+ model TEXT,
11
+ tokens_input INTEGER DEFAULT 0,
12
+ tokens_output INTEGER DEFAULT 0,
13
+ tokens_cache_read INTEGER DEFAULT 0,
14
+ tokens_cache_write INTEGER DEFAULT 0,
15
+ cost_usd REAL DEFAULT 0,
16
+ resource_health REAL,
17
+ session_efficiency REAL,
18
+ tool_calls INTEGER DEFAULT 0,
19
+ compactions INTEGER DEFAULT 0,
20
+ mode TEXT,
21
+ duration_seconds INTEGER DEFAULT 0,
22
+ created_at REAL NOT NULL
23
+ );
24
+ `;
25
+ export class TrendsStore {
26
+ db = null;
27
+ dbPath;
28
+ constructor(dataDir) {
29
+ const trendsDir = join(dataDir, "token-optimizer");
30
+ if (!existsSync(trendsDir)) {
31
+ mkdirSync(trendsDir, { recursive: true });
32
+ }
33
+ this.dbPath = join(trendsDir, "trends.db");
34
+ }
35
+ connect() {
36
+ if (!this.db) {
37
+ this.db = new Database(this.dbPath, { create: true });
38
+ this.db.exec("PRAGMA journal_mode=WAL");
39
+ this.db.exec("PRAGMA busy_timeout=3000");
40
+ this.db.exec(TRENDS_SCHEMA);
41
+ }
42
+ return this.db;
43
+ }
44
+ close() {
45
+ if (this.db) {
46
+ this.db.close();
47
+ this.db = null;
48
+ }
49
+ }
50
+ recordSession(data) {
51
+ const db = this.connect();
52
+ const date = new Date().toISOString().split("T")[0];
53
+ // Upsert that PRESERVES the original date + created_at. INSERT OR REPLACE is a
54
+ // DELETE+INSERT, so a re-recorded session (double session.deleted) would jump
55
+ // to today's date bucket and lose its original timestamp.
56
+ db.run(`INSERT INTO session_log
57
+ (session_id, date, project, model, tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
58
+ cost_usd, resource_health, session_efficiency, tool_calls, compactions, mode, duration_seconds, created_at)
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
60
+ ON CONFLICT(session_id) DO UPDATE SET
61
+ project=excluded.project, model=excluded.model,
62
+ tokens_input=excluded.tokens_input, tokens_output=excluded.tokens_output,
63
+ tokens_cache_read=excluded.tokens_cache_read, tokens_cache_write=excluded.tokens_cache_write,
64
+ cost_usd=excluded.cost_usd, resource_health=excluded.resource_health,
65
+ session_efficiency=excluded.session_efficiency, tool_calls=excluded.tool_calls,
66
+ compactions=excluded.compactions, mode=excluded.mode,
67
+ duration_seconds=excluded.duration_seconds`, [
68
+ data.sessionId,
69
+ date,
70
+ data.project,
71
+ data.model,
72
+ data.tokensInput,
73
+ data.tokensOutput,
74
+ data.tokensCacheRead,
75
+ data.tokensCacheWrite,
76
+ data.costUsd,
77
+ data.resourceHealth,
78
+ data.sessionEfficiency,
79
+ data.toolCalls,
80
+ data.compactions,
81
+ data.mode,
82
+ data.durationSeconds,
83
+ Date.now() / 1000,
84
+ ]);
85
+ }
86
+ getRecentSessions(days = 30) {
87
+ const db = this.connect();
88
+ const cutoff = new Date();
89
+ cutoff.setDate(cutoff.getDate() - days);
90
+ const cutoffStr = cutoff.toISOString().split("T")[0];
91
+ return db
92
+ .query("SELECT * FROM session_log WHERE date >= ? ORDER BY created_at DESC")
93
+ .all(cutoffStr);
94
+ }
95
+ getDailyStats(days = 30) {
96
+ const db = this.connect();
97
+ const cutoff = new Date();
98
+ cutoff.setDate(cutoff.getDate() - days);
99
+ const cutoffStr = cutoff.toISOString().split("T")[0];
100
+ return db
101
+ .query(`SELECT date,
102
+ COUNT(*) as sessions,
103
+ SUM(tokens_input) as total_input,
104
+ SUM(tokens_output) as total_output,
105
+ AVG(COALESCE(resource_health, 0)) as avg_resource_health,
106
+ AVG(COALESCE(session_efficiency, 0)) as avg_session_efficiency,
107
+ SUM(cost_usd) as total_cost
108
+ FROM session_log
109
+ WHERE date >= ?
110
+ GROUP BY date
111
+ ORDER BY date DESC`)
112
+ .all(cutoffStr);
113
+ }
114
+ generateDashboardData(days = 30) {
115
+ const sessions = this.getRecentSessions(days);
116
+ const dailyStats = this.getDailyStats(days);
117
+ return JSON.stringify({
118
+ generated: new Date().toISOString(),
119
+ platform: "opencode",
120
+ sessions,
121
+ dailyStats,
122
+ }, null, 2);
123
+ }
124
+ }
125
+ //# sourceMappingURL=trends.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trends.js","sourceRoot":"","sources":["../../src/storage/trends.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ export declare function createDashboardTool(getDataDir: () => string): ToolDefinition;
3
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/tools/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGhE,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,MAAM,GAAG,cAAc,CAqC5E"}
@@ -0,0 +1,45 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { writeDashboard } from "../dashboard/generator.js";
3
+ export function createDashboardTool(getDataDir) {
4
+ return tool({
5
+ description: "Generate and open the Token Optimizer dashboard. Shows quality trends, session history, " +
6
+ "and daily stats in an interactive HTML page.",
7
+ args: {
8
+ days: tool.schema.number().optional().describe("Number of days to include (default 30)"),
9
+ },
10
+ async execute(args) {
11
+ const dataDir = getDataDir();
12
+ const days = Math.max(1, Math.min(args.days ?? 30, 365));
13
+ try {
14
+ const outputPath = writeDashboard({ dataDir, days });
15
+ const { execFileSync } = await import("node:child_process");
16
+ const platform = process.platform;
17
+ if (platform === "darwin") {
18
+ execFileSync("open", [outputPath]);
19
+ }
20
+ else if (platform === "linux") {
21
+ try {
22
+ execFileSync("xdg-open", [outputPath]);
23
+ }
24
+ catch {
25
+ execFileSync("sensible-browser", [outputPath]);
26
+ }
27
+ }
28
+ else if (platform === "win32") {
29
+ execFileSync("cmd", ["/c", "start", "", outputPath]);
30
+ }
31
+ return {
32
+ title: "Dashboard Generated",
33
+ output: `Dashboard written to ${outputPath} and opened in browser.\n\nShowing ${days} days of session data.`,
34
+ };
35
+ }
36
+ catch (err) {
37
+ return {
38
+ title: "Dashboard Error",
39
+ output: `Failed to generate dashboard: ${err instanceof Error ? err.message : String(err)}`,
40
+ };
41
+ }
42
+ },
43
+ });
44
+ }
45
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../src/tools/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAuB,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D,MAAM,UAAU,mBAAmB,CAAC,UAAwB;IAC1D,OAAO,IAAI,CAAC;QACV,WAAW,EACT,0FAA0F;YAC1F,8CAA8C;QAChD,IAAI,EAAE;YACJ,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;SACzF;QACD,KAAK,CAAC,OAAO,CAAC,IAAI;YAChB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;YAEzD,IAAI,CAAC;gBACH,MAAM,UAAU,GAAG,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;gBAErD,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;gBAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;gBAClC,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;oBAC1B,YAAY,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;gBACrC,CAAC;qBAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;oBAChC,IAAI,CAAC;wBAAC,YAAY,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;oBAAC,CAAC;oBAAC,MAAM,CAAC;wBAAC,YAAY,CAAC,kBAAkB,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;oBAAC,CAAC;gBAC3G,CAAC;qBAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;oBAChC,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC;gBACvD,CAAC;gBAED,OAAO;oBACL,KAAK,EAAE,qBAAqB;oBAC5B,MAAM,EAAE,wBAAwB,UAAU,sCAAsC,IAAI,wBAAwB;iBAC7G,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO;oBACL,KAAK,EAAE,iBAAiB;oBACxB,MAAM,EAAE,iCAAiC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;iBAC5F,CAAC;YACJ,CAAC;QACH,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { SessionStore } from "../storage/session-store.js";
3
+ import type { QualityResult } from "../quality/scoring.js";
4
+ export declare function createTokenStatusTool(getState: () => {
5
+ store: SessionStore | null;
6
+ lastQuality: QualityResult | null;
7
+ sessionId: string;
8
+ }): ToolDefinition;
9
+ //# sourceMappingURL=token-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-status.d.ts","sourceRoot":"","sources":["../../src/tools/token-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM;IACpD,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,aAAa,GAAG,IAAI,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,cAAc,CAsDjB"}
@@ -0,0 +1,51 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { scoreToBand } from "../util/grade.js";
3
+ export function createTokenStatusTool(getState) {
4
+ return tool({
5
+ description: "Report current context health: quality scores, fill percentage, activity mode, top warnings. " +
6
+ "Use when you want to check context health before deciding whether to compact or start a new session.",
7
+ args: {
8
+ detail: tool.schema.boolean().optional().describe("Include per-signal breakdown"),
9
+ },
10
+ async execute(args) {
11
+ const state = getState();
12
+ if (!state.store || !state.lastQuality) {
13
+ return {
14
+ title: "Token Status",
15
+ output: "No quality data available yet. Quality scoring starts after the first tool call.",
16
+ };
17
+ }
18
+ const q = state.lastQuality;
19
+ const mode = state.store.getMeta("current_mode") ?? "general";
20
+ const lines = [
21
+ `## Context Health Report`,
22
+ "",
23
+ `**Resource Health**: ${Math.round(q.resourceHealth)}/100 (${q.resourceHealthGrade})`,
24
+ `**Session Efficiency**: ${Math.round(q.sessionEfficiency)}/100 (${q.sessionEfficiencyGrade})`,
25
+ `**Context Fill**: ${Math.round(q.fillPct * 100)}% | **Band**: ${scoreToBand(q.resourceHealth)}`,
26
+ `**Activity Mode**: ${mode}`,
27
+ `**Tool Calls**: ${q.toolCalls} | **Compactions**: ${state.store.getCompactionCount()}`,
28
+ ];
29
+ if (q.fillWarning) {
30
+ lines.push("", `**${q.fillWarning.level}**: ${q.fillWarning.message}`);
31
+ }
32
+ if (q.toolCallWarning) {
33
+ lines.push(`**${q.toolCallWarning.level}**: ${q.toolCallWarning.message}`);
34
+ }
35
+ if (args.detail) {
36
+ lines.push("", "### Signal Breakdown", "");
37
+ lines.push("| Signal | Score | Detail |");
38
+ lines.push("|--------|-------|--------|");
39
+ for (const [name, bd] of Object.entries(q.breakdown)) {
40
+ const displayName = name.replace(/_/g, " ");
41
+ lines.push(`| ${displayName} | ${bd.score}/100 | ${bd.detail} |`);
42
+ }
43
+ }
44
+ return {
45
+ title: "Token Status",
46
+ output: lines.join("\n"),
47
+ };
48
+ },
49
+ });
50
+ }
51
+ //# sourceMappingURL=token-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-status.js","sourceRoot":"","sources":["../../src/tools/token-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAuB,MAAM,qBAAqB,CAAC;AAGhE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,UAAU,qBAAqB,CAAC,QAIrC;IACC,OAAO,IAAI,CAAC;QACV,WAAW,EACT,+FAA+F;YAC/F,sGAAsG;QACxG,IAAI,EAAE;YACJ,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8BAA8B,CAAC;SAClF;QACD,KAAK,CAAC,OAAO,CAAC,IAAI;YAChB,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;YAEzB,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;gBACvC,OAAO;oBACL,KAAK,EAAE,cAAc;oBACrB,MAAM,EAAE,kFAAkF;iBAC3F,CAAC;YACJ,CAAC;YAED,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC;YAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,SAAS,CAAC;YAE9D,MAAM,KAAK,GAAa;gBACtB,0BAA0B;gBAC1B,EAAE;gBACF,wBAAwB,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,mBAAmB,GAAG;gBACrF,2BAA2B,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC,sBAAsB,GAAG;gBAC9F,qBAAqB,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,iBAAiB,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,EAAE;gBAChG,sBAAsB,IAAI,EAAE;gBAC5B,mBAAmB,CAAC,CAAC,SAAS,uBAAuB,KAAK,CAAC,KAAK,CAAC,kBAAkB,EAAE,EAAE;aACxF,CAAC;YAEF,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;gBAClB,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,WAAW,CAAC,KAAK,OAAO,CAAC,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACzE,CAAC;YACD,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;gBACtB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,eAAe,CAAC,KAAK,OAAO,CAAC,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC,CAAC;YAC7E,CAAC;YAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,sBAAsB,EAAE,EAAE,CAAC,CAAC;gBAC3C,KAAK,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;gBAC1C,KAAK,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;gBAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;oBACrD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;oBAC5C,KAAK,CAAC,IAAI,CAAC,KAAK,WAAW,MAAM,EAAE,CAAC,KAAK,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,CAAC;gBACpE,CAAC;YACH,CAAC;YAED,OAAO;gBACL,KAAK,EAAE,cAAc;gBACrB,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;aACzB,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function contextWindowForModel(model: string): number;
2
+ //# sourceMappingURL=context-window.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-window.d.ts","sourceRoot":"","sources":["../../src/util/context-window.ts"],"names":[],"mappings":"AA8EA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAa3D"}
@@ -0,0 +1,83 @@
1
+ const MODEL_CONTEXT_WINDOWS = {
2
+ // Anthropic (Opus/Sonnet 1M GA since March 13, 2026)
3
+ opus: 1_000_000,
4
+ sonnet: 1_000_000,
5
+ haiku: 200_000,
6
+ "claude-opus-4-7": 1_000_000,
7
+ "claude-opus-4-6": 1_000_000,
8
+ "claude-sonnet-4-6": 1_000_000,
9
+ "claude-haiku-4-5": 200_000,
10
+ // OpenAI GPT-5 family
11
+ "gpt-5.5-pro": 1_000_000,
12
+ "gpt-5.5": 1_000_000,
13
+ "gpt-5.4": 1_000_000,
14
+ "gpt-5.4-mini": 400_000,
15
+ "gpt-5.4-nano": 400_000,
16
+ "gpt-5.3-codex": 400_000,
17
+ "gpt-5.2-codex": 400_000,
18
+ "gpt-5.2": 400_000,
19
+ "gpt-5.1-codex-mini": 400_000,
20
+ "gpt-5.1-codex": 400_000,
21
+ "gpt-5.1": 400_000,
22
+ "gpt-5-codex": 400_000,
23
+ "gpt-5": 400_000,
24
+ "gpt-5-mini": 400_000,
25
+ "gpt-5-nano": 400_000,
26
+ // OpenAI GPT-4 family
27
+ "gpt-4.1": 1_000_000,
28
+ "gpt-4.1-mini": 1_000_000,
29
+ "gpt-4.1-nano": 1_000_000,
30
+ "gpt-4o": 128_000,
31
+ "gpt-4o-mini": 128_000,
32
+ // OpenAI reasoning
33
+ o3: 200_000,
34
+ "o3-mini": 200_000,
35
+ "o3-pro": 200_000,
36
+ "o4-mini": 200_000,
37
+ // Google Gemini
38
+ "gemini-3.5-flash": 1_000_000,
39
+ "gemini-3.1-pro-preview": 2_000_000,
40
+ "gemini-3.1-flash-lite": 1_000_000,
41
+ "gemini-3-pro": 1_000_000,
42
+ "gemini-3-flash": 1_000_000,
43
+ "gemini-3.1-pro": 1_000_000,
44
+ "gemini-2.5-pro": 2_000_000,
45
+ "gemini-2.5-flash": 1_000_000,
46
+ "gemini-2.5-flash-lite": 1_000_000,
47
+ "gemini-2.0-flash": 1_000_000,
48
+ "gemini-2.0-flash-lite": 1_000_000,
49
+ // DeepSeek
50
+ "deepseek-v3": 128_000,
51
+ "deepseek-r1": 128_000,
52
+ // Qwen
53
+ qwen3: 128_000,
54
+ "qwen3-mini": 128_000,
55
+ "qwen-coder": 128_000,
56
+ // Mistral
57
+ "mistral-large": 262_000,
58
+ "mistral-small": 128_000,
59
+ // xAI
60
+ "grok-4": 131_000,
61
+ // Other
62
+ "kimi-k2.5": 128_000,
63
+ "minimax-2": 128_000,
64
+ "glm-4.7": 128_000,
65
+ "glm-4.7-flash": 128_000,
66
+ "mimo-flash": 128_000,
67
+ local: 128_000,
68
+ };
69
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
70
+ export function contextWindowForModel(model) {
71
+ if (!model)
72
+ return DEFAULT_CONTEXT_WINDOW;
73
+ const lower = model.toLowerCase();
74
+ const direct = MODEL_CONTEXT_WINDOWS[lower];
75
+ if (direct !== undefined)
76
+ return direct;
77
+ for (const [key, value] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
78
+ if (lower.includes(key))
79
+ return value;
80
+ }
81
+ return DEFAULT_CONTEXT_WINDOW;
82
+ }
83
+ //# sourceMappingURL=context-window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-window.js","sourceRoot":"","sources":["../../src/util/context-window.ts"],"names":[],"mappings":"AAAA,MAAM,qBAAqB,GAA2B;IACpD,qDAAqD;IACrD,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,KAAK,EAAE,OAAO;IACd,iBAAiB,EAAE,SAAS;IAC5B,iBAAiB,EAAE,SAAS;IAC5B,mBAAmB,EAAE,SAAS;IAC9B,kBAAkB,EAAE,OAAO;IAE3B,sBAAsB;IACtB,aAAa,EAAE,SAAS;IACxB,SAAS,EAAE,SAAS;IACpB,SAAS,EAAE,SAAS;IACpB,cAAc,EAAE,OAAO;IACvB,cAAc,EAAE,OAAO;IACvB,eAAe,EAAE,OAAO;IACxB,eAAe,EAAE,OAAO;IACxB,SAAS,EAAE,OAAO;IAClB,oBAAoB,EAAE,OAAO;IAC7B,eAAe,EAAE,OAAO;IACxB,SAAS,EAAE,OAAO;IAClB,aAAa,EAAE,OAAO;IACtB,OAAO,EAAE,OAAO;IAChB,YAAY,EAAE,OAAO;IACrB,YAAY,EAAE,OAAO;IACrB,sBAAsB;IACtB,SAAS,EAAE,SAAS;IACpB,cAAc,EAAE,SAAS;IACzB,cAAc,EAAE,SAAS;IACzB,QAAQ,EAAE,OAAO;IACjB,aAAa,EAAE,OAAO;IACtB,mBAAmB;IACnB,EAAE,EAAE,OAAO;IACX,SAAS,EAAE,OAAO;IAClB,QAAQ,EAAE,OAAO;IACjB,SAAS,EAAE,OAAO;IAElB,gBAAgB;IAChB,kBAAkB,EAAE,SAAS;IAC7B,wBAAwB,EAAE,SAAS;IACnC,uBAAuB,EAAE,SAAS;IAClC,cAAc,EAAE,SAAS;IACzB,gBAAgB,EAAE,SAAS;IAC3B,gBAAgB,EAAE,SAAS;IAC3B,gBAAgB,EAAE,SAAS;IAC3B,kBAAkB,EAAE,SAAS;IAC7B,uBAAuB,EAAE,SAAS;IAClC,kBAAkB,EAAE,SAAS;IAC7B,uBAAuB,EAAE,SAAS;IAElC,WAAW;IACX,aAAa,EAAE,OAAO;IACtB,aAAa,EAAE,OAAO;IAEtB,OAAO;IACP,KAAK,EAAE,OAAO;IACd,YAAY,EAAE,OAAO;IACrB,YAAY,EAAE,OAAO;IAErB,UAAU;IACV,eAAe,EAAE,OAAO;IACxB,eAAe,EAAE,OAAO;IAExB,MAAM;IACN,QAAQ,EAAE,OAAO;IAEjB,QAAQ;IACR,WAAW,EAAE,OAAO;IACpB,WAAW,EAAE,OAAO;IACpB,SAAS,EAAE,OAAO;IAClB,eAAe,EAAE,OAAO;IACxB,YAAY,EAAE,OAAO;IACrB,KAAK,EAAE,OAAO;CACf,CAAC;AAEF,MAAM,sBAAsB,GAAG,OAAO,CAAC;AAEvC,MAAM,UAAU,qBAAqB,CAAC,KAAa;IACjD,IAAI,CAAC,KAAK;QAAE,OAAO,sBAAsB,CAAC;IAE1C,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAElC,MAAM,MAAM,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAC5C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAExC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,EAAE,CAAC;QACjE,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;IACxC,CAAC;IAED,OAAO,sBAAsB,CAAC;AAChC,CAAC"}