@vidiemme/copilot-proxy 0.5.0 → 0.7.0

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.
@@ -0,0 +1,214 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { copyFileSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { isAbsolute, join, resolve } from 'node:path';
5
+ import { isMap, isSeq, parseDocument } from 'yaml';
6
+ import { proxyUrl } from './agentRuntime.js';
7
+ import { pointsToProxy } from './editors.js';
8
+ /** Provider di LiteLLM che parla con GitHub Copilot. */
9
+ const COPILOT_PREFIX = 'github_copilot/';
10
+ function text(value) {
11
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
12
+ }
13
+ /** Legge le rotte verso Copilot; `undefined` se il file manca o non e' YAML. */
14
+ export function readCopilotRoutes(file) {
15
+ let content;
16
+ try {
17
+ content = readFileSync(file, 'utf-8');
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ const document = parseDocument(content);
23
+ if (document.errors.length > 0)
24
+ return undefined;
25
+ const list = document.toJS();
26
+ if (!Array.isArray(list?.model_list))
27
+ return [];
28
+ const routes = [];
29
+ for (const entry of list.model_list) {
30
+ const params = (entry?.litellm_params ?? {});
31
+ const model = text(params.model);
32
+ const alias = text(entry?.model_name);
33
+ if (!model?.startsWith(COPILOT_PREFIX) || !alias)
34
+ continue;
35
+ routes.push({ alias, model, apiBase: text(params.api_base) });
36
+ }
37
+ return routes;
38
+ }
39
+ /** `api_base` lasciabile al proxy: assente o un host Copilot, che il proxy usa gia' come upstream. */
40
+ function replaceable(apiBase) {
41
+ if (!apiBase)
42
+ return true;
43
+ try {
44
+ return /(^|\.)githubcopilot\.com$/.test(new URL(apiBase).hostname);
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ /**
51
+ * Indirizza al proxy le rotte di LiteLLM verso Copilot, impostando `api_base`.
52
+ *
53
+ * Si modifica il documento YAML e non l'oggetto letto: commenti, ordine e
54
+ * ancore del file restano com'erano.
55
+ */
56
+ export function applyLiteLLMRouting(file, port) {
57
+ const document = parseDocument(readFileSync(file, 'utf-8'));
58
+ if (document.errors.length > 0)
59
+ throw new Error(`${file} non e' YAML valido`);
60
+ const result = { updated: 0, already: 0, foreign: [], backup: undefined };
61
+ const list = document.get('model_list');
62
+ if (!isSeq(list))
63
+ return result;
64
+ for (const item of list.items) {
65
+ if (!isMap(item))
66
+ continue;
67
+ const params = item.get('litellm_params');
68
+ if (!isMap(params))
69
+ continue;
70
+ const model = text(params.get('model'));
71
+ if (!model?.startsWith(COPILOT_PREFIX))
72
+ continue;
73
+ const apiBase = text(params.get('api_base'));
74
+ if (apiBase && pointsToProxy(apiBase, port)) {
75
+ result.already += 1;
76
+ continue;
77
+ }
78
+ if (!replaceable(apiBase)) {
79
+ result.foreign.push({ alias: text(item.get('model_name')) ?? model, model, apiBase });
80
+ continue;
81
+ }
82
+ params.set('api_base', proxyUrl(port));
83
+ result.updated += 1;
84
+ }
85
+ if (result.updated > 0) {
86
+ result.backup = `${file}.bak`;
87
+ copyFileSync(file, result.backup);
88
+ writeFileSync(file, document.toString());
89
+ }
90
+ return result;
91
+ }
92
+ /** `--config <file>`, `--config=<file>` o `-c <file>` nella riga di comando di LiteLLM. */
93
+ export function configFromCommand(command) {
94
+ if (!/(^|[\s/])litellm(\.exe)?(\s|$)|litellm[\s/]proxy|-m\s+litellm/.test(command))
95
+ return undefined;
96
+ return /(?:^|\s)(?:--config(?:=|\s+)|-c\s+)(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(command)?.slice(1).find(Boolean);
97
+ }
98
+ function runningConfigs() {
99
+ if (process.platform === 'win32')
100
+ return [];
101
+ try {
102
+ const list = execFileSync('ps', ['-x', '-o', 'command='], { encoding: 'utf-8' });
103
+ // Senza il cwd del processo un path relativo e' ambiguo: si prova dalla home.
104
+ return list
105
+ .split('\n')
106
+ .map((line) => configFromCommand(line))
107
+ .filter((file) => !!file)
108
+ .map((file) => (isAbsolute(file) ? file : resolve(homedir(), file)));
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ }
114
+ /** Configurazioni nei LaunchAgent che avviano LiteLLM. */
115
+ function launchAgentConfigs() {
116
+ if (process.platform !== 'darwin')
117
+ return [];
118
+ const dir = join(homedir(), 'Library', 'LaunchAgents');
119
+ let files;
120
+ try {
121
+ files = readdirSync(dir).filter((name) => name.endsWith('.plist'));
122
+ }
123
+ catch {
124
+ return [];
125
+ }
126
+ const found = [];
127
+ for (const name of files) {
128
+ let content;
129
+ try {
130
+ content = readFileSync(join(dir, name), 'utf-8');
131
+ }
132
+ catch {
133
+ continue;
134
+ }
135
+ if (!content.includes('litellm'))
136
+ continue;
137
+ const args = [...content.matchAll(/<string>([^<]*)<\/string>/g)].map((match) => match[1]);
138
+ const index = args.findIndex((arg) => arg === '--config' || arg === '-c');
139
+ const inline = args.find((arg) => arg.startsWith('--config='));
140
+ const file = index >= 0 ? args[index + 1] : inline?.slice('--config='.length);
141
+ if (file)
142
+ found.push(file);
143
+ }
144
+ return found;
145
+ }
146
+ function conventionalConfigs() {
147
+ const home = homedir();
148
+ return [
149
+ join(home, 'litellm_config.yaml'),
150
+ join(home, 'litellm-config.yaml'),
151
+ join(home, 'litellm', 'config.yaml'),
152
+ join(home, '.litellm', 'config.yaml'),
153
+ join(home, '.config', 'litellm', 'config.yaml'),
154
+ ];
155
+ }
156
+ /**
157
+ * File di configurazione di LiteLLM su questa macchina che hanno rotte verso
158
+ * Copilot: quello del processo in esecuzione, dei LaunchAgent, o nei posti
159
+ * consueti. LiteLLM non ha un percorso predefinito: si passa con `--config`.
160
+ */
161
+ export function findLiteLLMConfigs(explicit = []) {
162
+ const candidates = [
163
+ ...explicit,
164
+ ...(process.env.CONFIG_FILE_PATH ? [process.env.CONFIG_FILE_PATH] : []),
165
+ ...runningConfigs(),
166
+ ...launchAgentConfigs(),
167
+ ...conventionalConfigs(),
168
+ ].map((file) => resolve(file));
169
+ return [...new Set(candidates)].filter((file) => existsSync(file) && (readCopilotRoutes(file)?.length ?? 0) > 0);
170
+ }
171
+ /** Stessa normalizzazione del listino del collector, per confrontare nomi scritti in modo diverso. */
172
+ export function normalizeModel(model) {
173
+ return model
174
+ .trim()
175
+ .toLowerCase()
176
+ .replace(/^[a-z0-9_-]+[/:]/, '')
177
+ .replace(/[.\s_]+/g, '-')
178
+ .replace(/-\d{4}-?\d{2}-?\d{2}$/, '');
179
+ }
180
+ /**
181
+ * Riconosce le chiamate che LiteLLM ha mandato a Copilot dal nome del modello
182
+ * chiesto dal client, e restituisce il modello Copilot con cui prezzarle.
183
+ */
184
+ export function copilotModelMatcher(routes) {
185
+ const exact = new Map();
186
+ const wildcards = [];
187
+ for (const route of routes) {
188
+ if (route.alias.includes('*')) {
189
+ wildcards.push({ prefix: route.alias.slice(0, route.alias.indexOf('*')), model: route.model });
190
+ continue;
191
+ }
192
+ exact.set(normalizeModel(route.alias), route.model);
193
+ // Alcuni client registrano il modello restituito, che e' quello Copilot.
194
+ if (!route.model.includes('*'))
195
+ exact.set(normalizeModel(route.model), route.model);
196
+ }
197
+ return (candidates) => {
198
+ for (const candidate of candidates) {
199
+ if (!candidate)
200
+ continue;
201
+ const model = exact.get(normalizeModel(candidate));
202
+ if (model)
203
+ return model;
204
+ for (const wildcard of wildcards) {
205
+ if (!candidate.startsWith(wildcard.prefix))
206
+ continue;
207
+ const rest = candidate.slice(wildcard.prefix.length);
208
+ return wildcard.model.includes('*') ? wildcard.model.replace('*', rest) : wildcard.model;
209
+ }
210
+ }
211
+ return undefined;
212
+ };
213
+ }
214
+ //# sourceMappingURL=litellm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"litellm.js","sourceRoot":"","sources":["../src/litellm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { TokenCounters } from '@vidiemme/copilot-usage-contract';
2
+ export type AgentTool = 'claude-code' | 'cline' | 'opencode';
3
+ /**
4
+ * Una chiamata al modello registrata da un harness di terze parti. Quale
5
+ * provider ci fosse dietro i log non lo dicono sempre: lo decide chi carica,
6
+ * confrontando il modello con le rotte di LiteLLM.
7
+ */
8
+ export interface AgentCall {
9
+ tool: AgentTool;
10
+ requestId: string;
11
+ occurredAt: Date;
12
+ cwd: string | undefined;
13
+ sessionId: string;
14
+ /** Nomi del modello in ordine di affidabilita': quello chiesto, poi quello restituito. */
15
+ models: string[];
16
+ /** Provider dichiarato dal client (`litellm`, `github-copilot`, ...), se lo registra. */
17
+ provider: string | null;
18
+ usage: TokenCounters;
19
+ durationMs: number | null;
20
+ }
21
+ export declare function defaultClaudeProjectsDir(): string;
22
+ /**
23
+ * Normalizza una riga `assistant` di Claude Code.
24
+ *
25
+ * I contatori sono quelli dell'API Anthropic: `input_tokens` esclude gia' la
26
+ * cache, come nel contratto.
27
+ */
28
+ export declare function parseClaudeLine(line: string): AgentCall | undefined;
29
+ /** Legge le chiamate registrate nelle sessioni di Claude Code. */
30
+ export declare function readClaudeCalls(projectsDir?: string, modifiedSince?: Date): AgentCall[];
31
+ export declare function defaultClineTaskDirs(): string[];
32
+ /** Chiamate di un task di Cline, una per `api_req_started` con dei contatori. */
33
+ export declare function readClineTask(taskDir: string): AgentCall[];
34
+ export declare function readClineCalls(taskDirs?: string[], modifiedSince?: Date): AgentCall[];
35
+ export declare function defaultOpencodeDir(): string;
36
+ /**
37
+ * Normalizza un messaggio `assistant` di opencode.
38
+ *
39
+ * opencode separa gia' la cache dall'input fresco, per ogni provider.
40
+ */
41
+ export declare function parseOpencodeMessage(raw: unknown, fallback: {
42
+ id?: string;
43
+ sessionId?: string;
44
+ cwd?: string;
45
+ }): AgentCall | undefined;
46
+ export declare function readOpencodeCalls(dataDir?: string, modifiedSince?: Date): AgentCall[];
47
+ /** Tutte le chiamate degli harness di terze parti salvate su questa macchina. */
48
+ export declare function readAgentCalls(modifiedSince?: Date): AgentCall[];
@@ -0,0 +1,378 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { knownEditors } from '../editors.js';
6
+ function asObject(value) {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
8
+ }
9
+ function count(value) {
10
+ return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
11
+ }
12
+ function text(value) {
13
+ return typeof value === 'string' && value.length > 0 ? value : null;
14
+ }
15
+ function listDir(path) {
16
+ try {
17
+ return readdirSync(path);
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ }
23
+ function touchedSince(file, since) {
24
+ if (!since)
25
+ return true;
26
+ try {
27
+ return statSync(file).mtime >= since;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ function totalTokens(usage) {
34
+ return usage.inputTokens + usage.cachedInputTokens + usage.cacheWriteTokens + usage.outputTokens;
35
+ }
36
+ // --- Claude Code -------------------------------------------------------------
37
+ export function defaultClaudeProjectsDir() {
38
+ return join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), '.claude'), 'projects');
39
+ }
40
+ /** File di sessione, compresi quelli dei subagent in `<sessione>/subagents/`. */
41
+ function jsonlFiles(dir, depth = 3) {
42
+ const files = [];
43
+ for (const name of listDir(dir)) {
44
+ const path = join(dir, name);
45
+ if (name.endsWith('.jsonl'))
46
+ files.push(path);
47
+ else if (depth > 0 && !name.includes('.'))
48
+ files.push(...jsonlFiles(path, depth - 1));
49
+ }
50
+ return files;
51
+ }
52
+ /**
53
+ * Normalizza una riga `assistant` di Claude Code.
54
+ *
55
+ * I contatori sono quelli dell'API Anthropic: `input_tokens` esclude gia' la
56
+ * cache, come nel contratto.
57
+ */
58
+ export function parseClaudeLine(line) {
59
+ let record;
60
+ try {
61
+ record = asObject(JSON.parse(line));
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ if (record?.type !== 'assistant' || record.isApiErrorMessage === true)
67
+ return undefined;
68
+ const message = asObject(record.message);
69
+ const usage = asObject(message?.usage);
70
+ const model = text(message?.model);
71
+ if (!message || !usage || model === '<synthetic>')
72
+ return undefined;
73
+ const id = text(message.id) ?? text(record.requestId) ?? text(record.uuid);
74
+ const at = new Date(typeof record.timestamp === 'string' ? record.timestamp : NaN);
75
+ if (!id || Number.isNaN(at.getTime()))
76
+ return undefined;
77
+ return {
78
+ tool: 'claude-code',
79
+ requestId: `claude-code:${id}`,
80
+ occurredAt: at,
81
+ cwd: text(record.cwd) ?? undefined,
82
+ sessionId: text(record.sessionId) ?? 'claude-code',
83
+ models: [text(record.requestedModel), model].filter((name) => !!name),
84
+ provider: null,
85
+ usage: {
86
+ inputTokens: count(usage.input_tokens),
87
+ cachedInputTokens: count(usage.cache_read_input_tokens),
88
+ cacheWriteTokens: count(usage.cache_creation_input_tokens),
89
+ outputTokens: count(usage.output_tokens),
90
+ },
91
+ durationMs: null,
92
+ };
93
+ }
94
+ /** Legge le chiamate registrate nelle sessioni di Claude Code. */
95
+ export function readClaudeCalls(projectsDir = defaultClaudeProjectsDir(), modifiedSince) {
96
+ const byId = new Map();
97
+ for (const file of jsonlFiles(projectsDir)) {
98
+ if (!touchedSince(file, modifiedSince))
99
+ continue;
100
+ let content;
101
+ try {
102
+ content = readFileSync(file, 'utf-8');
103
+ }
104
+ catch {
105
+ continue;
106
+ }
107
+ for (const line of content.split('\n')) {
108
+ if (!line.includes('"usage"') || !line.includes('"assistant"'))
109
+ continue;
110
+ const call = parseClaudeLine(line);
111
+ if (!call)
112
+ continue;
113
+ // Ogni blocco della risposta e' una riga con lo stesso id; le sessioni
114
+ // riprese ricopiano le righe. Si tiene la copia piu' completa.
115
+ const known = byId.get(call.requestId);
116
+ if (!known || call.usage.outputTokens > known.usage.outputTokens)
117
+ byId.set(call.requestId, call);
118
+ }
119
+ }
120
+ return [...byId.values()];
121
+ }
122
+ // --- Cline, Roo Code, Kilo Code ---------------------------------------------
123
+ /** Estensioni derivate da Cline: stesso formato dei task. */
124
+ const CLINE_EXTENSIONS = ['saoudrizwan.claude-dev', 'rooveterinaryinc.roo-cline', 'kilocode.kilo-code'];
125
+ /** Provider in cui `tokensIn` esclude la cache, come nell'API Anthropic. */
126
+ const ANTHROPIC_PROTOCOL = new Set(['anthropic', 'bedrock', 'vertex', 'claude-code']);
127
+ export function defaultClineTaskDirs() {
128
+ const dirs = [join(homedir(), '.cline', 'data', 'tasks')];
129
+ for (const editor of knownEditors()) {
130
+ const globalStorage = join(dirname(editor.settingsFile), 'globalStorage');
131
+ for (const extension of CLINE_EXTENSIONS)
132
+ dirs.push(join(globalStorage, extension, 'tasks'));
133
+ }
134
+ return dirs;
135
+ }
136
+ function readJson(file) {
137
+ try {
138
+ return JSON.parse(readFileSync(file, 'utf-8'));
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ }
144
+ const CWD_PATTERN = /Current Working Directory \((.+?)\)/;
145
+ /** La cartella del task compare nell'`environment_details` che Cline allega a ogni richiesta. */
146
+ function taskCwd(requests, taskDir) {
147
+ for (const request of requests) {
148
+ const match = CWD_PATTERN.exec(request);
149
+ if (match)
150
+ return match[1];
151
+ }
152
+ const history = readJson(join(taskDir, 'api_conversation_history.json'));
153
+ const match = history ? CWD_PATTERN.exec(JSON.stringify(history)) : null;
154
+ if (!match)
155
+ return undefined;
156
+ try {
157
+ // Il path viene da un JSON serializzato: gli escape (`\\` su Windows) vanno sciolti.
158
+ return JSON.parse(`"${match[1]}"`);
159
+ }
160
+ catch {
161
+ return undefined;
162
+ }
163
+ }
164
+ function modelAt(history, ts) {
165
+ let current = history[0];
166
+ for (const entry of history)
167
+ if (entry.ts <= ts)
168
+ current = entry;
169
+ return current;
170
+ }
171
+ /** Chiamate di un task di Cline, una per `api_req_started` con dei contatori. */
172
+ export function readClineTask(taskDir) {
173
+ const messages = readJson(join(taskDir, 'ui_messages.json'));
174
+ if (!Array.isArray(messages))
175
+ return [];
176
+ const metadata = asObject(readJson(join(taskDir, 'task_metadata.json')));
177
+ const history = (Array.isArray(metadata?.model_usage) ? metadata.model_usage : [])
178
+ .map((raw) => asObject(raw))
179
+ .filter((entry) => !!entry)
180
+ .map((entry) => ({ ts: count(entry.ts), model: text(entry.model_id), provider: text(entry.model_provider_id) }))
181
+ .sort((a, b) => a.ts - b.ts);
182
+ const started = [];
183
+ for (const raw of messages) {
184
+ const message = asObject(raw);
185
+ if (message?.say !== 'api_req_started' || typeof message.text !== 'string')
186
+ continue;
187
+ try {
188
+ const info = asObject(JSON.parse(message.text));
189
+ if (info)
190
+ started.push({ ts: count(message.ts), info });
191
+ }
192
+ catch {
193
+ // Testo non JSON: richiesta senza contatori.
194
+ }
195
+ }
196
+ const cwd = taskCwd(started.map(({ info }) => (typeof info.request === 'string' ? info.request : '')), taskDir);
197
+ const taskId = taskDir.split(/[\\/]/).pop();
198
+ const calls = [];
199
+ for (const { ts, info } of started) {
200
+ const used = modelAt(history, ts);
201
+ const cacheRead = count(info.cacheReads);
202
+ const cacheWrite = count(info.cacheWrites);
203
+ // Con i provider in stile OpenAI (LiteLLM compreso) `tokensIn` comprende la cache.
204
+ const protocol = text(info.apiProtocol) ?? (used?.provider && ANTHROPIC_PROTOCOL.has(used.provider) ? 'anthropic' : 'openai');
205
+ const tokensIn = count(info.tokensIn);
206
+ const usage = {
207
+ inputTokens: protocol === 'anthropic' ? tokensIn : Math.max(0, tokensIn - cacheRead - cacheWrite),
208
+ cachedInputTokens: cacheRead,
209
+ cacheWriteTokens: cacheWrite,
210
+ outputTokens: count(info.tokensOut),
211
+ };
212
+ if (ts === 0 || totalTokens(usage) === 0)
213
+ continue;
214
+ calls.push({
215
+ tool: 'cline',
216
+ requestId: `cline:${taskId}:${ts}`,
217
+ occurredAt: new Date(ts),
218
+ cwd,
219
+ sessionId: taskId,
220
+ models: used?.model ? [used.model] : [],
221
+ provider: used?.provider ?? null,
222
+ usage,
223
+ durationMs: null,
224
+ });
225
+ }
226
+ return calls;
227
+ }
228
+ export function readClineCalls(taskDirs = defaultClineTaskDirs(), modifiedSince) {
229
+ const calls = [];
230
+ for (const dir of taskDirs) {
231
+ for (const task of listDir(dir)) {
232
+ const taskDir = join(dir, task);
233
+ if (!touchedSince(join(taskDir, 'ui_messages.json'), modifiedSince))
234
+ continue;
235
+ calls.push(...readClineTask(taskDir));
236
+ }
237
+ }
238
+ return calls;
239
+ }
240
+ // --- opencode ---------------------------------------------------------------
241
+ export function defaultOpencodeDir() {
242
+ return join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), '.local', 'share'), 'opencode');
243
+ }
244
+ /**
245
+ * Normalizza un messaggio `assistant` di opencode.
246
+ *
247
+ * opencode separa gia' la cache dall'input fresco, per ogni provider.
248
+ */
249
+ export function parseOpencodeMessage(raw, fallback) {
250
+ const message = asObject(raw);
251
+ if (message?.role !== 'assistant')
252
+ return undefined;
253
+ const tokens = asObject(message.tokens);
254
+ const cache = asObject(tokens?.cache);
255
+ const time = asObject(message.time);
256
+ const id = text(message.id) ?? fallback.id;
257
+ const created = count(time?.created);
258
+ if (!tokens || !id || created === 0)
259
+ return undefined;
260
+ const usage = {
261
+ inputTokens: count(tokens.input),
262
+ cachedInputTokens: count(cache?.read),
263
+ cacheWriteTokens: count(cache?.write),
264
+ // Il ragionamento e' fatturato come output.
265
+ outputTokens: count(tokens.output) + count(tokens.reasoning),
266
+ };
267
+ if (totalTokens(usage) === 0)
268
+ return undefined;
269
+ const completed = count(time?.completed);
270
+ return {
271
+ tool: 'opencode',
272
+ requestId: `opencode:${id}`,
273
+ occurredAt: new Date(created),
274
+ cwd: text(asObject(message.path)?.cwd) ?? fallback.cwd,
275
+ sessionId: text(message.sessionID) ?? fallback.sessionId ?? 'opencode',
276
+ models: [text(message.modelID)].filter((name) => !!name),
277
+ provider: text(message.providerID),
278
+ usage,
279
+ durationMs: completed > created ? completed - created : null,
280
+ };
281
+ }
282
+ /** `node:sqlite` si carica solo se serve: e' ancora sperimentale e lo segnala all'import. */
283
+ function openSqlite(file) {
284
+ try {
285
+ const require = createRequire(import.meta.url);
286
+ const { DatabaseSync } = require('node:sqlite');
287
+ return new DatabaseSync(file, { readOnly: true });
288
+ }
289
+ catch {
290
+ return undefined;
291
+ }
292
+ }
293
+ /** Versioni recenti: tutto in `opencode.db`. */
294
+ function readOpencodeDb(file, since) {
295
+ // Prima di caricare `node:sqlite`: chi non usa opencode non deve vederne l'avviso.
296
+ if (!existsSync(file))
297
+ return [];
298
+ const db = openSqlite(file);
299
+ if (!db)
300
+ return [];
301
+ try {
302
+ const rows = db
303
+ .prepare(`SELECT m.id, m.session_id, m.data, s.directory
304
+ FROM message m JOIN session s ON s.id = m.session_id
305
+ WHERE m.time_created >= ?`)
306
+ .all(since?.getTime() ?? 0);
307
+ const calls = [];
308
+ for (const row of rows) {
309
+ let data;
310
+ try {
311
+ data = JSON.parse(row.data);
312
+ }
313
+ catch {
314
+ continue;
315
+ }
316
+ const call = parseOpencodeMessage(data, { id: row.id, sessionId: row.session_id, cwd: row.directory });
317
+ if (call)
318
+ calls.push(call);
319
+ }
320
+ return calls;
321
+ }
322
+ catch {
323
+ return [];
324
+ }
325
+ finally {
326
+ db.close();
327
+ }
328
+ }
329
+ /** Versioni precedenti: un JSON per messaggio in `storage/message/<sessione>/`. */
330
+ function readOpencodeStorage(storage, since) {
331
+ const sessionDirs = new Map();
332
+ const sessions = join(storage, 'session');
333
+ for (const project of listDir(sessions)) {
334
+ for (const name of listDir(join(sessions, project))) {
335
+ const session = asObject(readJson(join(sessions, project, name)));
336
+ if (typeof session?.id === 'string' && typeof session.directory === 'string') {
337
+ sessionDirs.set(session.id, session.directory);
338
+ }
339
+ }
340
+ }
341
+ const calls = [];
342
+ const messages = join(storage, 'message');
343
+ for (const sessionId of listDir(messages)) {
344
+ for (const name of listDir(join(messages, sessionId))) {
345
+ const file = join(messages, sessionId, name);
346
+ if (!name.endsWith('.json') || !touchedSince(file, since))
347
+ continue;
348
+ const call = parseOpencodeMessage(readJson(file), {
349
+ id: name.replace(/\.json$/, ''),
350
+ sessionId,
351
+ cwd: sessionDirs.get(sessionId),
352
+ });
353
+ if (call)
354
+ calls.push(call);
355
+ }
356
+ }
357
+ return calls;
358
+ }
359
+ export function readOpencodeCalls(dataDir = defaultOpencodeDir(), modifiedSince) {
360
+ const byId = new Map();
361
+ // Dopo la migrazione a SQLite i JSON possono restare: stessi id, una sola copia.
362
+ for (const call of [
363
+ ...readOpencodeStorage(join(dataDir, 'storage'), modifiedSince),
364
+ ...readOpencodeDb(join(dataDir, 'opencode.db'), modifiedSince),
365
+ ]) {
366
+ byId.set(call.requestId, call);
367
+ }
368
+ return [...byId.values()];
369
+ }
370
+ /** Tutte le chiamate degli harness di terze parti salvate su questa macchina. */
371
+ export function readAgentCalls(modifiedSince) {
372
+ return [
373
+ ...readClaudeCalls(undefined, modifiedSince),
374
+ ...readClineCalls(undefined, modifiedSince),
375
+ ...readOpencodeCalls(undefined, modifiedSince),
376
+ ];
377
+ }
378
+ //# sourceMappingURL=agents.js.map