@danieltmn/tamnora 0.1.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/SECURITY.md +31 -0
  5. package/harness/packages/cli/bin/tamnora.mjs +11 -0
  6. package/harness/packages/cli/package.json +10 -0
  7. package/harness/packages/cli/src/cli.js +90 -0
  8. package/harness/packages/cli/src/commands/autostart.js +121 -0
  9. package/harness/packages/cli/src/commands/bridge.js +274 -0
  10. package/harness/packages/cli/src/commands/doctor.js +73 -0
  11. package/harness/packages/cli/src/commands/eval.js +183 -0
  12. package/harness/packages/cli/src/commands/init.js +31 -0
  13. package/harness/packages/cli/src/commands/mcp.js +101 -0
  14. package/harness/packages/cli/src/commands/repl.js +172 -0
  15. package/harness/packages/cli/src/commands/run.js +165 -0
  16. package/harness/packages/cli/src/commands/serve.js +1 -0
  17. package/harness/packages/cli/src/commands/server.js +345 -0
  18. package/harness/packages/cli/src/commands/tui.js +110 -0
  19. package/harness/packages/cli/src/commands/update.js +76 -0
  20. package/harness/packages/cli/src/commands/user.js +135 -0
  21. package/harness/packages/cli/src/lib/core.js +2 -0
  22. package/harness/packages/cli/src/lib/daemon.js +153 -0
  23. package/harness/packages/cli/src/lib/qr.js +203 -0
  24. package/harness/packages/cli/src/lib/runtime.js +110 -0
  25. package/harness/packages/cli/src/lib/tunnel.js +113 -0
  26. package/harness/packages/cli/src/lib/util.js +24 -0
  27. package/harness/packages/cli/src/tui/app.js +302 -0
  28. package/harness/packages/cli/src/tui/keys.js +78 -0
  29. package/harness/packages/cli/src/tui/layout.js +130 -0
  30. package/harness/packages/cli/src/tui/screen.js +13 -0
  31. package/harness/packages/cli/src/ui/color.js +15 -0
  32. package/harness/packages/cli/test/release.test.js +34 -0
  33. package/harness/packages/cli/test/tui.test.js +99 -0
  34. package/harness/packages/cli/test/tunnel.test.js +20 -0
  35. package/harness/packages/core/package.json +14 -0
  36. package/harness/packages/core/src/agent/context.js +66 -0
  37. package/harness/packages/core/src/agent/modes.js +26 -0
  38. package/harness/packages/core/src/agent/runAgent.js +257 -0
  39. package/harness/packages/core/src/config/app.js +49 -0
  40. package/harness/packages/core/src/config/home.js +98 -0
  41. package/harness/packages/core/src/config/identity.js +41 -0
  42. package/harness/packages/core/src/config/index.js +69 -0
  43. package/harness/packages/core/src/config/providers.js +24 -0
  44. package/harness/packages/core/src/env.js +19 -0
  45. package/harness/packages/core/src/eval/dataset.js +47 -0
  46. package/harness/packages/core/src/eval/judge.js +39 -0
  47. package/harness/packages/core/src/eval/report.js +48 -0
  48. package/harness/packages/core/src/eval/runTask.js +71 -0
  49. package/harness/packages/core/src/eval/runner.js +150 -0
  50. package/harness/packages/core/src/eval/scorers.js +217 -0
  51. package/harness/packages/core/src/eval/task.js +59 -0
  52. package/harness/packages/core/src/index.js +72 -0
  53. package/harness/packages/core/src/mcp/common.js +82 -0
  54. package/harness/packages/core/src/mcp/http.js +149 -0
  55. package/harness/packages/core/src/mcp/index.js +167 -0
  56. package/harness/packages/core/src/mcp/stdio.js +218 -0
  57. package/harness/packages/core/src/providers/index.js +119 -0
  58. package/harness/packages/core/src/providers/ollama.js +76 -0
  59. package/harness/packages/core/src/providers/openaiCompatible.js +57 -0
  60. package/harness/packages/core/src/providers/openrouter.js +57 -0
  61. package/harness/packages/core/src/providers/presets.js +19 -0
  62. package/harness/packages/core/src/providers/resilience.js +74 -0
  63. package/harness/packages/core/src/providers/stream.js +200 -0
  64. package/harness/packages/core/src/providers/utils.js +66 -0
  65. package/harness/packages/core/src/storage/sqlite.js +453 -0
  66. package/harness/packages/core/src/tools/diff.js +90 -0
  67. package/harness/packages/core/src/tools/executors.js +277 -0
  68. package/harness/packages/core/src/tools/glob.js +67 -0
  69. package/harness/packages/core/src/tools/index.js +28 -0
  70. package/harness/packages/core/src/tools/paths.js +29 -0
  71. package/harness/packages/core/src/tools/permissions.js +69 -0
  72. package/harness/packages/core/src/tools/registry.js +44 -0
  73. package/harness/packages/core/src/tools/schema.js +177 -0
  74. package/harness/packages/core/test/agent.test.js +237 -0
  75. package/harness/packages/core/test/config.test.js +33 -0
  76. package/harness/packages/core/test/context.test.js +44 -0
  77. package/harness/packages/core/test/diff.test.js +20 -0
  78. package/harness/packages/core/test/eval.test.js +144 -0
  79. package/harness/packages/core/test/git-tools.test.js +44 -0
  80. package/harness/packages/core/test/home.test.js +62 -0
  81. package/harness/packages/core/test/mcp.test.js +158 -0
  82. package/harness/packages/core/test/providers.test.js +80 -0
  83. package/harness/packages/core/test/resilience.test.js +61 -0
  84. package/harness/packages/core/test/storage.test.js +55 -0
  85. package/harness/packages/core/test/stream.test.js +91 -0
  86. package/harness/packages/core/test/tools.test.js +96 -0
  87. package/harness/packages/server/bin/tamnora-server.mjs +11 -0
  88. package/harness/packages/server/package.json +10 -0
  89. package/harness/packages/server/public/app.js +2188 -0
  90. package/harness/packages/server/public/img/fondo-agentes.webp +0 -0
  91. package/harness/packages/server/public/index.html +368 -0
  92. package/harness/packages/server/public/landing/index.html +523 -0
  93. package/harness/packages/server/public/landing/landing.js +89 -0
  94. package/harness/packages/server/public/markdown.js +174 -0
  95. package/harness/packages/server/public/styles.css +923 -0
  96. package/harness/packages/server/src/auth.js +146 -0
  97. package/harness/packages/server/src/cli.js +61 -0
  98. package/harness/packages/server/src/index.js +1 -0
  99. package/harness/packages/server/src/lib/approvals.js +51 -0
  100. package/harness/packages/server/src/lib/core.js +2 -0
  101. package/harness/packages/server/src/lib/http.js +74 -0
  102. package/harness/packages/server/src/lib/modelCache.js +32 -0
  103. package/harness/packages/server/src/lib/prompt.js +48 -0
  104. package/harness/packages/server/src/lib/workspace.js +74 -0
  105. package/harness/packages/server/src/routes/auth.js +56 -0
  106. package/harness/packages/server/src/routes/chat.js +222 -0
  107. package/harness/packages/server/src/routes/eval.js +100 -0
  108. package/harness/packages/server/src/routes/files.js +117 -0
  109. package/harness/packages/server/src/routes/folders.js +80 -0
  110. package/harness/packages/server/src/routes/mcp.js +50 -0
  111. package/harness/packages/server/src/routes/models.js +26 -0
  112. package/harness/packages/server/src/routes/providers.js +135 -0
  113. package/harness/packages/server/src/routes/sessions.js +157 -0
  114. package/harness/packages/server/src/server.js +249 -0
  115. package/harness/packages/server/src/static.js +67 -0
  116. package/harness/packages/server/test/markdown.test.js +36 -0
  117. package/harness/packages/server/test/modelCache.test.js +72 -0
  118. package/harness/packages/server/test/server.test.js +809 -0
  119. package/harness/scripts/example-dataset.jsonl +4 -0
  120. package/harness/scripts/mcp-echo-server.mjs +108 -0
  121. package/harness/scripts/mock-provider.mjs +134 -0
  122. package/harness/tamnora.example.json +18 -0
  123. package/package.json +70 -0
@@ -0,0 +1,453 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdirSync } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ function likePattern(query) {
7
+ return `%${String(query).replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
8
+ }
9
+
10
+ const SCHEMA = `
11
+ CREATE TABLE IF NOT EXISTS sessions (
12
+ id TEXT PRIMARY KEY,
13
+ title TEXT,
14
+ provider TEXT,
15
+ model TEXT,
16
+ cwd TEXT,
17
+ created_at INTEGER,
18
+ updated_at INTEGER,
19
+ meta TEXT
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS messages (
23
+ id TEXT PRIMARY KEY,
24
+ session_id TEXT NOT NULL,
25
+ role TEXT NOT NULL,
26
+ content TEXT,
27
+ tool_calls TEXT,
28
+ tool_call_id TEXT,
29
+ name TEXT,
30
+ tokens_in INTEGER,
31
+ tokens_out INTEGER,
32
+ created_at INTEGER
33
+ );
34
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at);
35
+
36
+ CREATE TABLE IF NOT EXISTS runs (
37
+ id TEXT PRIMARY KEY,
38
+ dataset TEXT,
39
+ config TEXT,
40
+ status TEXT,
41
+ created_at INTEGER,
42
+ finished_at INTEGER,
43
+ summary TEXT
44
+ );
45
+ CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
46
+
47
+ CREATE TABLE IF NOT EXISTS run_results (
48
+ id TEXT PRIMARY KEY,
49
+ run_id TEXT NOT NULL,
50
+ task_id TEXT,
51
+ model TEXT,
52
+ trial INTEGER,
53
+ output TEXT,
54
+ score REAL,
55
+ passed INTEGER,
56
+ trace TEXT,
57
+ duration_ms INTEGER,
58
+ tokens_in INTEGER,
59
+ tokens_out INTEGER,
60
+ cost REAL,
61
+ artifacts_dir TEXT,
62
+ created_at INTEGER
63
+ );
64
+ CREATE INDEX IF NOT EXISTS idx_results_run ON run_results(run_id);
65
+
66
+ CREATE TABLE IF NOT EXISTS mcp_servers (
67
+ id TEXT PRIMARY KEY,
68
+ name TEXT,
69
+ transport TEXT,
70
+ command TEXT,
71
+ url TEXT,
72
+ enabled INTEGER,
73
+ config TEXT
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS providers (
77
+ id TEXT PRIMARY KEY,
78
+ label TEXT,
79
+ base_url TEXT,
80
+ api_key TEXT,
81
+ models TEXT,
82
+ requires_key INTEGER,
83
+ custom INTEGER,
84
+ enabled INTEGER,
85
+ updated_at INTEGER
86
+ );
87
+
88
+ CREATE TABLE IF NOT EXISTS settings (
89
+ key TEXT PRIMARY KEY,
90
+ value TEXT
91
+ );
92
+ `;
93
+
94
+ /** Open (creating if needed) a SQLite database with the harness schema. */
95
+ export function openDatabase(file) {
96
+ if (file && file !== ':memory:') {
97
+ mkdirSync(path.dirname(file), { recursive: true });
98
+ }
99
+ const db = new DatabaseSync(file ?? ':memory:');
100
+ db.exec('PRAGMA journal_mode = WAL;');
101
+ db.exec(SCHEMA);
102
+ return db;
103
+ }
104
+
105
+ const now = () => Date.now();
106
+ const json = (value) => (value === undefined || value === null ? null : JSON.stringify(value));
107
+ const parseJson = (value) => {
108
+ if (value === null || value === undefined) return null;
109
+ try {
110
+ return JSON.parse(value);
111
+ } catch {
112
+ return value;
113
+ }
114
+ };
115
+
116
+ /** Thin data-access layer over the SQLite database. */
117
+ export class Store {
118
+ constructor(file) {
119
+ this.db = openDatabase(file);
120
+ }
121
+
122
+ close() {
123
+ this.db.close();
124
+ }
125
+
126
+ // --- Sessions -----------------------------------------------------------
127
+
128
+ createSession({ title = 'Nueva sesión', provider, model, cwd, meta } = {}) {
129
+ const id = randomUUID();
130
+ const ts = now();
131
+ this.db
132
+ .prepare(
133
+ `INSERT INTO sessions (id, title, provider, model, cwd, created_at, updated_at, meta)
134
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
135
+ )
136
+ .run(id, title, provider ?? null, model ?? null, cwd ?? null, ts, ts, json(meta));
137
+ return id;
138
+ }
139
+
140
+ touchSession(id, { provider, model, title, meta } = {}) {
141
+ const session = this.getSession(id);
142
+ if (!session) return;
143
+ const mergedMeta = meta === undefined ? session.meta : { ...(session.meta ?? {}), ...meta };
144
+ this.db
145
+ .prepare(`UPDATE sessions SET updated_at = ?, provider = ?, model = ?, title = ?, meta = ? WHERE id = ?`)
146
+ .run(
147
+ now(),
148
+ provider ?? session.provider,
149
+ model ?? session.model,
150
+ title ?? session.title,
151
+ json(mergedMeta),
152
+ id
153
+ );
154
+ }
155
+
156
+ clearMessages(sessionId) {
157
+ this.db.prepare(`DELETE FROM messages WHERE session_id = ?`).run(sessionId);
158
+ }
159
+
160
+ /** Borra desde `messageId` (incluido) hasta el final. Devuelve cuantos borro. */
161
+ deleteMessagesFrom(sessionId, messageId) {
162
+ const row = this.db
163
+ .prepare(`SELECT rowid FROM messages WHERE id = ? AND session_id = ?`)
164
+ .get(messageId, sessionId);
165
+ if (!row) return 0;
166
+ const info = this.db.prepare(`DELETE FROM messages WHERE session_id = ? AND rowid >= ?`).run(sessionId, row.rowid);
167
+ return info.changes;
168
+ }
169
+
170
+ sessionTokens(sessionId) {
171
+ const row = this.db
172
+ .prepare(
173
+ `SELECT COALESCE(SUM(tokens_in), 0) AS input_tokens, COALESCE(SUM(tokens_out), 0) AS output_tokens
174
+ FROM messages WHERE session_id = ?`
175
+ )
176
+ .get(sessionId);
177
+ return { in: row.input_tokens, out: row.output_tokens };
178
+ }
179
+
180
+ getSession(id) {
181
+ const row = this.db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id);
182
+ if (!row) return null;
183
+ return { ...row, meta: parseJson(row.meta) };
184
+ }
185
+
186
+ listSessions(limit = 20) {
187
+ return this.db
188
+ .prepare(`SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?`)
189
+ .all(limit)
190
+ .map((row) => ({ ...row, meta: parseJson(row.meta) }));
191
+ }
192
+
193
+ latestSession() {
194
+ return this.listSessions(1)[0] ?? null;
195
+ }
196
+
197
+ searchSessions(query, limit = 20) {
198
+ const like = likePattern(query);
199
+ return this.db
200
+ .prepare(`SELECT * FROM sessions WHERE title LIKE ? ESCAPE '\\' ORDER BY updated_at DESC LIMIT ?`)
201
+ .all(like, limit)
202
+ .map((row) => ({ ...row, meta: parseJson(row.meta) }));
203
+ }
204
+
205
+ searchMessages(query, limit = 30) {
206
+ const like = likePattern(query);
207
+ return this.db
208
+ .prepare(
209
+ `SELECT m.id, m.session_id, m.role, m.content, m.created_at, s.title, s.cwd
210
+ FROM messages m JOIN sessions s ON s.id = m.session_id
211
+ WHERE m.content LIKE ? ESCAPE '\\'
212
+ ORDER BY m.created_at DESC LIMIT ?`
213
+ )
214
+ .all(like, limit)
215
+ .map((row) => ({
216
+ id: row.id,
217
+ sessionId: row.session_id,
218
+ role: row.role,
219
+ content: parseJson(row.content),
220
+ title: row.title,
221
+ cwd: row.cwd,
222
+ }));
223
+ }
224
+
225
+ deleteSession(id) {
226
+ this.db.prepare(`DELETE FROM messages WHERE session_id = ?`).run(id);
227
+ this.db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
228
+ }
229
+
230
+ // --- Messages -----------------------------------------------------------
231
+
232
+ addMessage(sessionId, message) {
233
+ const id = randomUUID();
234
+ this.db
235
+ .prepare(
236
+ `INSERT INTO messages (id, session_id, role, content, tool_calls, tool_call_id, name, tokens_in, tokens_out, created_at)
237
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
238
+ )
239
+ .run(
240
+ id,
241
+ sessionId,
242
+ message.role,
243
+ json(message.content),
244
+ json(message.tool_calls),
245
+ message.tool_call_id ?? null,
246
+ message.name ?? null,
247
+ message.tokens_in ?? null,
248
+ message.tokens_out ?? null,
249
+ now()
250
+ );
251
+ return id;
252
+ }
253
+
254
+ addMessages(sessionId, messages) {
255
+ for (const message of messages) this.addMessage(sessionId, message);
256
+ }
257
+
258
+ getMessages(sessionId) {
259
+ return this.db
260
+ .prepare(`SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC`)
261
+ .all(sessionId)
262
+ .map((row) => {
263
+ const message = {
264
+ id: row.id,
265
+ role: row.role,
266
+ content: parseJson(row.content),
267
+ };
268
+ if (row.tool_calls) message.tool_calls = parseJson(row.tool_calls);
269
+ if (row.tool_call_id) message.tool_call_id = row.tool_call_id;
270
+ if (row.name) message.name = row.name;
271
+ return message;
272
+ });
273
+ }
274
+
275
+ // --- Runs (evaluation) --------------------------------------------------
276
+
277
+ createRun({ dataset, config } = {}) {
278
+ const id = randomUUID();
279
+ this.db
280
+ .prepare(`INSERT INTO runs (id, dataset, config, status, created_at) VALUES (?, ?, ?, ?, ?)`)
281
+ .run(id, dataset ?? null, json(config), 'running', now());
282
+ return id;
283
+ }
284
+
285
+ finishRun(id, { status = 'done', summary } = {}) {
286
+ this.db
287
+ .prepare(`UPDATE runs SET status = ?, finished_at = ?, summary = ? WHERE id = ?`)
288
+ .run(status, now(), json(summary), id);
289
+ }
290
+
291
+ getRun(id) {
292
+ const row = this.db.prepare(`SELECT * FROM runs WHERE id = ?`).get(id);
293
+ if (!row) return null;
294
+ return { ...row, config: parseJson(row.config), summary: parseJson(row.summary) };
295
+ }
296
+
297
+ listRuns(limit = 20) {
298
+ return this.db
299
+ .prepare(`SELECT * FROM runs ORDER BY created_at DESC LIMIT ?`)
300
+ .all(limit)
301
+ .map((row) => ({ ...row, config: parseJson(row.config), summary: parseJson(row.summary) }));
302
+ }
303
+
304
+ addRunResult(result) {
305
+ const id = randomUUID();
306
+ this.db
307
+ .prepare(
308
+ `INSERT INTO run_results
309
+ (id, run_id, task_id, model, trial, output, score, passed, trace, duration_ms,
310
+ tokens_in, tokens_out, cost, artifacts_dir, created_at)
311
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
312
+ )
313
+ .run(
314
+ id,
315
+ result.runId,
316
+ result.taskId ?? null,
317
+ result.model ?? null,
318
+ result.trial ?? 0,
319
+ result.output ?? null,
320
+ result.score ?? null,
321
+ result.passed ? 1 : 0,
322
+ json(result.trace),
323
+ result.durationMs ?? null,
324
+ result.tokensIn ?? null,
325
+ result.tokensOut ?? null,
326
+ result.cost ?? null,
327
+ result.artifactsDir ?? null,
328
+ now()
329
+ );
330
+ return id;
331
+ }
332
+
333
+ getRunResults(runId) {
334
+ return this.db
335
+ .prepare(`SELECT * FROM run_results WHERE run_id = ? ORDER BY created_at ASC`)
336
+ .all(runId)
337
+ .map((row) => ({ ...row, trace: parseJson(row.trace) }));
338
+ }
339
+
340
+ // --- MCP servers --------------------------------------------------------
341
+
342
+ listMcpServers() {
343
+ return this.db
344
+ .prepare(`SELECT * FROM mcp_servers ORDER BY name ASC`)
345
+ .all()
346
+ .map((row) => ({ ...row, enabled: Boolean(row.enabled), config: parseJson(row.config) }));
347
+ }
348
+
349
+ saveMcpServer(server) {
350
+ this.db
351
+ .prepare(
352
+ `INSERT INTO mcp_servers (id, name, transport, command, url, enabled, config)
353
+ VALUES (?, ?, ?, ?, ?, ?, ?)
354
+ ON CONFLICT(id) DO UPDATE SET
355
+ name = excluded.name, transport = excluded.transport, command = excluded.command,
356
+ url = excluded.url, enabled = excluded.enabled, config = excluded.config`
357
+ )
358
+ .run(
359
+ server.id,
360
+ server.name ?? server.id,
361
+ server.transport ?? 'http',
362
+ server.command ?? null,
363
+ server.url ?? null,
364
+ server.enabled === false ? 0 : 1,
365
+ json(server.config)
366
+ );
367
+ }
368
+
369
+ deleteMcpServer(id) {
370
+ this.db.prepare(`DELETE FROM mcp_servers WHERE id = ?`).run(id);
371
+ }
372
+
373
+ // --- Providers ----------------------------------------------------------
374
+
375
+ listProviders() {
376
+ return this.db
377
+ .prepare(`SELECT * FROM providers ORDER BY id ASC`)
378
+ .all()
379
+ .map(toProvider);
380
+ }
381
+
382
+ getProvider(id) {
383
+ const row = this.db.prepare(`SELECT * FROM providers WHERE id = ?`).get(id);
384
+ return row ? toProvider(row) : null;
385
+ }
386
+
387
+ saveProvider(provider) {
388
+ this.db
389
+ .prepare(
390
+ `INSERT INTO providers (id, label, base_url, api_key, models, requires_key, custom, enabled, updated_at)
391
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
392
+ ON CONFLICT(id) DO UPDATE SET
393
+ label = excluded.label, base_url = excluded.base_url, api_key = excluded.api_key,
394
+ models = excluded.models, requires_key = excluded.requires_key,
395
+ custom = excluded.custom, enabled = excluded.enabled, updated_at = excluded.updated_at`
396
+ )
397
+ .run(
398
+ provider.id,
399
+ provider.label ?? provider.id,
400
+ provider.baseUrl ?? null,
401
+ provider.apiKey ?? null,
402
+ json(provider.models ?? []),
403
+ provider.requiresKey === undefined ? null : provider.requiresKey ? 1 : 0,
404
+ provider.custom ? 1 : 0,
405
+ provider.enabled === false ? 0 : 1,
406
+ now()
407
+ );
408
+ return this.getProvider(provider.id);
409
+ }
410
+
411
+ deleteProvider(id) {
412
+ this.db.prepare(`DELETE FROM providers WHERE id = ?`).run(id);
413
+ }
414
+
415
+ // --- Settings (key/value) ----------------------------------------------
416
+
417
+ getSetting(key, fallback = null) {
418
+ const row = this.db.prepare(`SELECT value FROM settings WHERE key = ?`).get(key);
419
+ return row ? parseJson(row.value) : fallback;
420
+ }
421
+
422
+ setSetting(key, value) {
423
+ this.db
424
+ .prepare(
425
+ `INSERT INTO settings (key, value) VALUES (?, ?)
426
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
427
+ )
428
+ .run(key, json(value));
429
+ return value;
430
+ }
431
+
432
+ allSettings() {
433
+ const out = {};
434
+ for (const row of this.db.prepare(`SELECT key, value FROM settings`).all()) {
435
+ out[row.key] = parseJson(row.value);
436
+ }
437
+ return out;
438
+ }
439
+ }
440
+
441
+ function toProvider(row) {
442
+ return {
443
+ id: row.id,
444
+ label: row.label ?? row.id,
445
+ baseUrl: row.base_url ?? '',
446
+ apiKey: row.api_key ?? '',
447
+ models: parseJson(row.models) ?? [],
448
+ requiresKey: row.requires_key === null ? undefined : Boolean(row.requires_key),
449
+ custom: Boolean(row.custom),
450
+ enabled: row.enabled === 0 ? false : true,
451
+ updatedAt: row.updated_at ?? null,
452
+ };
453
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Diff de lineas minimo (sin dependencias) para mostrar que cambio write/edit.
3
+ * Produce lineas con prefijo ' ' / '-' / '+', con contexto y truncado.
4
+ */
5
+
6
+ const MAX_CELLS = 1_500_000;
7
+
8
+ function splitLines(text) {
9
+ if (text === '' || text == null) return [];
10
+ return String(text).replace(/\r\n/g, '\n').split('\n');
11
+ }
12
+
13
+ function computeOps(a, b) {
14
+ const n = a.length;
15
+ const m = b.length;
16
+ if (n === 0) return b.map((line) => ({ t: 'add', line }));
17
+ if (m === 0) return a.map((line) => ({ t: 'del', line }));
18
+ if (n * m > MAX_CELLS) {
19
+ return [
20
+ ...a.map((line) => ({ t: 'del', line })),
21
+ ...b.map((line) => ({ t: 'add', line })),
22
+ ];
23
+ }
24
+
25
+ const dp = new Uint32Array((n + 1) * (m + 1));
26
+ const at = (i, j) => i * (m + 1) + j;
27
+ for (let i = n - 1; i >= 0; i--) {
28
+ for (let j = m - 1; j >= 0; j--) {
29
+ dp[at(i, j)] =
30
+ a[i] === b[j] ? dp[at(i + 1, j + 1)] + 1 : Math.max(dp[at(i + 1, j)], dp[at(i, j + 1)]);
31
+ }
32
+ }
33
+
34
+ const ops = [];
35
+ let i = 0;
36
+ let j = 0;
37
+ while (i < n && j < m) {
38
+ if (a[i] === b[j]) {
39
+ ops.push({ t: 'eq', line: a[i] });
40
+ i++;
41
+ j++;
42
+ } else if (dp[at(i + 1, j)] >= dp[at(i, j + 1)]) {
43
+ ops.push({ t: 'del', line: a[i] });
44
+ i++;
45
+ } else {
46
+ ops.push({ t: 'add', line: b[j] });
47
+ j++;
48
+ }
49
+ }
50
+ while (i < n) ops.push({ t: 'del', line: a[i++] });
51
+ while (j < m) ops.push({ t: 'add', line: b[j++] });
52
+ return ops;
53
+ }
54
+
55
+ /**
56
+ * @returns {string} diff con prefijos, o '' si no hay cambios; '(diff truncado)'
57
+ * al final si se corto por tamano.
58
+ */
59
+ export function diffLines(oldText, newText, { context = 3, maxLines = 200 } = {}) {
60
+ const ops = computeOps(splitLines(oldText), splitLines(newText));
61
+ const changes = ops.map((o, idx) => (o.t === 'eq' ? -1 : idx)).filter((idx) => idx >= 0);
62
+ if (!changes.length) return '';
63
+
64
+ const keep = new Array(ops.length).fill(false);
65
+ for (const idx of changes) {
66
+ for (let j = Math.max(0, idx - context); j <= Math.min(ops.length - 1, idx + context); j++) keep[j] = true;
67
+ }
68
+
69
+ const out = [];
70
+ let skipped = false;
71
+ let truncated = false;
72
+ for (let idx = 0; idx < ops.length; idx++) {
73
+ if (!keep[idx]) {
74
+ skipped = true;
75
+ continue;
76
+ }
77
+ if (skipped) {
78
+ out.push(' …');
79
+ skipped = false;
80
+ }
81
+ const prefix = ops[idx].t === 'add' ? '+' : ops[idx].t === 'del' ? '-' : ' ';
82
+ out.push(`${prefix} ${ops[idx].line}`);
83
+ if (out.length >= maxLines) {
84
+ truncated = true;
85
+ break;
86
+ }
87
+ }
88
+ if (truncated) out.push(' … (diff truncado)');
89
+ return out.join('\n');
90
+ }