os-dpt 0.0.1

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,3477 @@
1
+ // server/load-env.ts
2
+ for (const envPath of [".env", "../.env"]) {
3
+ try {
4
+ process.loadEnvFile(envPath);
5
+ } catch {
6
+ }
7
+ }
8
+
9
+ // server/index.ts
10
+ import { pathToFileURL } from "node:url";
11
+ import { serve } from "@hono/node-server";
12
+ import { Hono as Hono10 } from "hono";
13
+ import { HTTPException as HTTPException4 } from "hono/http-exception";
14
+
15
+ // server/api/ai-providers.ts
16
+ import { Hono } from "hono";
17
+
18
+ // server/credentials/vault.ts
19
+ import crypto2 from "node:crypto";
20
+ import { promises as fs2 } from "node:fs";
21
+ import path2 from "node:path";
22
+ import keytar from "keytar";
23
+
24
+ // server/credentials/workspaceId.ts
25
+ import crypto from "node:crypto";
26
+ import { promises as fs } from "node:fs";
27
+ import path from "node:path";
28
+ var FILE = path.join(".os-dpt", "workspace-id");
29
+ async function getOrCreateWorkspaceId(workspace) {
30
+ const filePath2 = path.join(workspace, FILE);
31
+ try {
32
+ const raw = await fs.readFile(filePath2, "utf8");
33
+ const id2 = raw.trim();
34
+ if (id2) return id2;
35
+ } catch (err) {
36
+ if (err.code !== "ENOENT") throw err;
37
+ }
38
+ const id = crypto.randomUUID();
39
+ await fs.mkdir(path.dirname(filePath2), { recursive: true });
40
+ await fs.writeFile(filePath2, id + "\n", { encoding: "utf8", mode: 384 });
41
+ return id;
42
+ }
43
+
44
+ // server/credentials/vault.ts
45
+ var SERVICE = "os-dpt";
46
+ var FILE2 = path2.join(".os-dpt", "credentials.enc");
47
+ var CredentialVault = class {
48
+ constructor(workspace) {
49
+ this.workspace = workspace;
50
+ }
51
+ // Serializes read-modify-write cycles so concurrent setPassword /
52
+ // deletePassword calls don't clobber each other.
53
+ queue = Promise.resolve();
54
+ enqueue(fn) {
55
+ const next = this.queue.then(fn, fn);
56
+ this.queue = next.catch(() => {
57
+ });
58
+ return next;
59
+ }
60
+ get filePath() {
61
+ return path2.join(this.workspace, FILE2);
62
+ }
63
+ async account() {
64
+ const id = await getOrCreateWorkspaceId(this.workspace);
65
+ return `workspace:${id}`;
66
+ }
67
+ setPassword(connectionId, password) {
68
+ return this.enqueue(async () => {
69
+ const key = await this.masterKey();
70
+ const iv = crypto2.randomBytes(12);
71
+ const cipher = crypto2.createCipheriv("aes-256-gcm", key, iv);
72
+ const ciphertext = Buffer.concat([
73
+ cipher.update(password, "utf8"),
74
+ cipher.final()
75
+ ]);
76
+ const tag = cipher.getAuthTag();
77
+ const blob = await this.read();
78
+ blob.entries[connectionId] = {
79
+ iv: iv.toString("base64"),
80
+ ciphertext: ciphertext.toString("base64"),
81
+ tag: tag.toString("base64")
82
+ };
83
+ await this.persist(blob);
84
+ });
85
+ }
86
+ async getPassword(connectionId) {
87
+ const blob = await this.read();
88
+ const entry = blob.entries[connectionId];
89
+ if (!entry) return null;
90
+ const key = await this.masterKey();
91
+ const iv = Buffer.from(entry.iv, "base64");
92
+ const ciphertext = Buffer.from(entry.ciphertext, "base64");
93
+ const tag = Buffer.from(entry.tag, "base64");
94
+ const decipher = crypto2.createDecipheriv("aes-256-gcm", key, iv);
95
+ decipher.setAuthTag(tag);
96
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString(
97
+ "utf8"
98
+ );
99
+ }
100
+ deletePassword(connectionId) {
101
+ return this.enqueue(async () => {
102
+ const blob = await this.read();
103
+ if (!(connectionId in blob.entries)) return;
104
+ delete blob.entries[connectionId];
105
+ await this.persist(blob);
106
+ });
107
+ }
108
+ async masterKey() {
109
+ const account = await this.account();
110
+ const existing = await keytar.getPassword(SERVICE, account);
111
+ if (existing) return Buffer.from(existing, "base64");
112
+ const fresh = crypto2.randomBytes(32);
113
+ await keytar.setPassword(SERVICE, account, fresh.toString("base64"));
114
+ return fresh;
115
+ }
116
+ async read() {
117
+ try {
118
+ const raw = await fs2.readFile(this.filePath, "utf8");
119
+ const data = JSON.parse(raw);
120
+ return { entries: data.entries ?? {} };
121
+ } catch (err) {
122
+ if (err.code === "ENOENT") return { entries: {} };
123
+ throw err;
124
+ }
125
+ }
126
+ async persist(blob) {
127
+ await fs2.mkdir(path2.dirname(this.filePath), { recursive: true });
128
+ await fs2.writeFile(this.filePath, JSON.stringify(blob, null, 2), {
129
+ encoding: "utf8",
130
+ mode: 384
131
+ });
132
+ }
133
+ };
134
+
135
+ // server/storage/ai-providers.ts
136
+ import { promises as fs3 } from "node:fs";
137
+ import path3 from "node:path";
138
+ var FILE3 = "ai-providers.json";
139
+ var AIProviderStore = class {
140
+ constructor(workspace) {
141
+ this.workspace = workspace;
142
+ }
143
+ get filePath() {
144
+ return path3.join(this.workspace, FILE3);
145
+ }
146
+ async get(id) {
147
+ const all = await this.list();
148
+ return all[id] ?? null;
149
+ }
150
+ async set(id, meta) {
151
+ const all = await this.list();
152
+ all[id] = meta;
153
+ await this.write(all);
154
+ }
155
+ async remove(id) {
156
+ const all = await this.list();
157
+ if (all[id] === null) return;
158
+ all[id] = null;
159
+ await this.write(all);
160
+ }
161
+ async list() {
162
+ const empty = {
163
+ anthropic: null,
164
+ openai: null,
165
+ braintrust: null
166
+ };
167
+ try {
168
+ const raw = await fs3.readFile(this.filePath, "utf8");
169
+ const data = JSON.parse(raw);
170
+ return {
171
+ anthropic: data.providers?.anthropic ?? null,
172
+ openai: data.providers?.openai ?? null,
173
+ braintrust: data.providers?.braintrust ?? null
174
+ };
175
+ } catch (err) {
176
+ if (err.code === "ENOENT") return empty;
177
+ throw err;
178
+ }
179
+ }
180
+ async write(providers) {
181
+ const cleaned = {};
182
+ for (const [k, v] of Object.entries(providers)) {
183
+ if (v !== null) cleaned[k] = v;
184
+ }
185
+ await fs3.mkdir(this.workspace, { recursive: true });
186
+ await fs3.writeFile(
187
+ this.filePath,
188
+ JSON.stringify({ providers: cleaned }, null, 2) + "\n",
189
+ "utf8"
190
+ );
191
+ }
192
+ };
193
+
194
+ // server/workspace.ts
195
+ import { promises as fs4 } from "node:fs";
196
+ import { execFile } from "node:child_process";
197
+ import { promisify } from "node:util";
198
+ import path4 from "node:path";
199
+ import { HTTPException } from "hono/http-exception";
200
+
201
+ // shared/context.ts
202
+ var CONTEXT_DOCS = [
203
+ {
204
+ name: "schemas",
205
+ title: "Schemas",
206
+ description: "Table and column facts the agent has learned about the data source."
207
+ },
208
+ {
209
+ name: "conventions",
210
+ title: "Conventions",
211
+ description: "How the team writes SQL and what business terms mean."
212
+ },
213
+ {
214
+ name: "feedback",
215
+ title: "Feedback",
216
+ description: "Corrections and lessons learned from running queries."
217
+ }
218
+ ];
219
+ var CONTEXT_DOC_NAMES = CONTEXT_DOCS.map((d) => d.name);
220
+
221
+ // server/workspace.ts
222
+ var exec = promisify(execFile);
223
+ var WORKSHEETS_DIR = "worksheets";
224
+ var CONTEXT_DIR = "context";
225
+ var CONTEXT_BY_SOURCE = "by-source";
226
+ var OSDPT_DIR = ".os-dpt";
227
+ var DRAFTS_DIR = path4.join(OSDPT_DIR, "drafts");
228
+ var CHATS_DIR = path4.join(OSDPT_DIR, "chats");
229
+ var SCHEMAS_DIR = path4.join(OSDPT_DIR, "schemas");
230
+ var SESSION_FILE = path4.join(OSDPT_DIR, "session.json");
231
+ var SCHEMA_FILE = path4.join(OSDPT_DIR, "schema.json");
232
+ var HISTORY_FILE = path4.join(OSDPT_DIR, "history.db");
233
+ var GITIGNORE = ".gitignore";
234
+ var CONTEXT_FILES = CONTEXT_DOC_NAMES;
235
+ var resolved = null;
236
+ function parseWorkspaceArg(argv) {
237
+ const idx = argv.indexOf("--workspace");
238
+ if (idx !== -1 && argv[idx + 1]) return argv[idx + 1];
239
+ return process.cwd();
240
+ }
241
+ function resolveWorkspace(argv) {
242
+ return path4.resolve(parseWorkspaceArg(argv));
243
+ }
244
+ async function initWorkspace(argv = process.argv.slice(2)) {
245
+ const root = resolveWorkspace(argv);
246
+ await fs4.mkdir(path4.join(root, WORKSHEETS_DIR), { recursive: true });
247
+ await fs4.mkdir(path4.join(root, DRAFTS_DIR), { recursive: true });
248
+ await fs4.mkdir(path4.join(root, CHATS_DIR), { recursive: true });
249
+ await fs4.mkdir(path4.join(root, CONTEXT_DIR), { recursive: true });
250
+ await fs4.mkdir(path4.join(root, SCHEMAS_DIR), { recursive: true });
251
+ const sessionPath = path4.join(root, SESSION_FILE);
252
+ try {
253
+ await fs4.access(sessionPath);
254
+ } catch {
255
+ await fs4.writeFile(
256
+ sessionPath,
257
+ JSON.stringify({ openTabs: [], activeSlug: null, resultsPaneSize: null }, null, 2)
258
+ );
259
+ }
260
+ await ensureGitRepo(root);
261
+ await ensureGitignored(root);
262
+ resolved = root;
263
+ return root;
264
+ }
265
+ async function ensureGitRepo(root) {
266
+ try {
267
+ await exec("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root });
268
+ return;
269
+ } catch {
270
+ }
271
+ try {
272
+ await exec("git", ["init"], { cwd: root });
273
+ console.log(`os-dpt: initialized a git repository in ${root} for worksheet history`);
274
+ } catch {
275
+ console.warn(
276
+ "os-dpt: git not found \u2014 worksheet history is disabled. Install git and run `git init` here to enable it."
277
+ );
278
+ }
279
+ }
280
+ async function ensureGitignored(root) {
281
+ const gitignorePath = path4.join(root, GITIGNORE);
282
+ let current = "";
283
+ try {
284
+ current = await fs4.readFile(gitignorePath, "utf8");
285
+ } catch {
286
+ }
287
+ const needs = ["/.os-dpt/"];
288
+ const missing = needs.filter((line) => !current.split("\n").some((l) => l.trim() === line));
289
+ if (missing.length === 0) return;
290
+ const append = (current.endsWith("\n") || current === "" ? "" : "\n") + missing.join("\n") + "\n";
291
+ await fs4.writeFile(gitignorePath, current + append);
292
+ }
293
+ function workspaceRoot() {
294
+ if (!resolved) throw new Error("Workspace not initialized");
295
+ return resolved;
296
+ }
297
+ function workspacePath(...segments) {
298
+ return path4.join(workspaceRoot(), ...segments);
299
+ }
300
+ var paths = {
301
+ worksheets: () => workspacePath(WORKSHEETS_DIR),
302
+ worksheet: (slug) => workspacePath(WORKSHEETS_DIR, `${slug}.sql`),
303
+ drafts: () => workspacePath(DRAFTS_DIR),
304
+ draft: (slug) => workspacePath(DRAFTS_DIR, `${slug}.sql`),
305
+ schemas: () => workspacePath(SCHEMAS_DIR),
306
+ connectionSchema: (id) => workspacePath(SCHEMAS_DIR, `${id}.json`),
307
+ session: () => workspacePath(SESSION_FILE),
308
+ schema: () => workspacePath(SCHEMA_FILE),
309
+ history: () => workspacePath(HISTORY_FILE),
310
+ chats: () => workspacePath(CHATS_DIR),
311
+ chat: (id) => workspacePath(CHATS_DIR, `${id}.json`),
312
+ context: () => workspacePath(CONTEXT_DIR),
313
+ // `connectionId` scopes docs to a data source; null/undefined → the
314
+ // workspace-root "unassigned" set.
315
+ contextDir: (connectionId) => connectionId ? workspacePath(CONTEXT_DIR, CONTEXT_BY_SOURCE, connectionId) : workspacePath(CONTEXT_DIR),
316
+ contextFile: (name, connectionId) => connectionId ? workspacePath(CONTEXT_DIR, CONTEXT_BY_SOURCE, connectionId, `${name}.md`) : workspacePath(CONTEXT_DIR, `${name}.md`)
317
+ };
318
+ var SLUG_RE = /^[a-z0-9][a-z0-9-_]*$/i;
319
+ function assertSafeSlug(slug) {
320
+ if (!SLUG_RE.test(slug) || slug.includes("..") || slug.length > 100) {
321
+ throw new HTTPException(400, { message: `Invalid slug: ${slug}` });
322
+ }
323
+ }
324
+ var CHAT_ID_RE = /^[a-z0-9][a-z0-9-]*$/i;
325
+ function assertSafeChatId(id) {
326
+ if (!CHAT_ID_RE.test(id) || id.includes("..") || id.length > 64) {
327
+ throw new HTTPException(400, { message: `Invalid chat id: ${id}` });
328
+ }
329
+ }
330
+ var CONNECTION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
331
+ function isSafeConnectionId(id) {
332
+ return CONNECTION_ID_RE.test(id);
333
+ }
334
+ function assertSafeConnectionId(id) {
335
+ if (!isSafeConnectionId(id)) {
336
+ throw new HTTPException(400, { message: `Invalid connection id` });
337
+ }
338
+ }
339
+
340
+ // server/agent/tracing.ts
341
+ var bt = null;
342
+ var enabled = false;
343
+ function tracingEnabled() {
344
+ return enabled;
345
+ }
346
+ async function resolveApiKey() {
347
+ if (process.env.BRAINTRUST_API_KEY) return process.env.BRAINTRUST_API_KEY;
348
+ try {
349
+ const vault = new CredentialVault(workspaceRoot());
350
+ return await vault.getPassword("ai:braintrust");
351
+ } catch {
352
+ return null;
353
+ }
354
+ }
355
+ async function initTracing() {
356
+ const apiKey = await resolveApiKey();
357
+ if (!apiKey) {
358
+ enabled = false;
359
+ return;
360
+ }
361
+ const projectName = process.env.BRAINTRUST_PROJECT ?? "os-dpt";
362
+ try {
363
+ if (!bt) bt = await import("braintrust");
364
+ bt.initLogger({
365
+ projectName,
366
+ apiKey,
367
+ // Flush in the background so logging never adds latency to a turn.
368
+ asyncFlush: true
369
+ });
370
+ enabled = true;
371
+ console.log(`[os-dpt] Braintrust tracing enabled (project: ${projectName})`);
372
+ } catch (err) {
373
+ const code = err.code;
374
+ const message = code === "ERR_MODULE_NOT_FOUND" ? "the braintrust package is not installed. Run `pnpm add braintrust` (or `npm i braintrust`) in your workspace to enable tracing." : err.message;
375
+ console.warn(
376
+ "[os-dpt] Braintrust key found but tracing failed to initialize:",
377
+ message
378
+ );
379
+ bt = null;
380
+ enabled = false;
381
+ }
382
+ }
383
+ async function refreshTracing() {
384
+ await initTracing();
385
+ }
386
+ var NOOP_SPAN = { log() {
387
+ } };
388
+ async function traced(args, fn) {
389
+ if (!enabled || !bt) return fn(NOOP_SPAN);
390
+ return bt.traced((span) => fn(span), {
391
+ name: args.name,
392
+ type: args.type,
393
+ event: args.event,
394
+ parent: args.parent
395
+ });
396
+ }
397
+ async function startConversationSpan(args) {
398
+ if (!enabled || !bt) return void 0;
399
+ try {
400
+ const span = bt.startSpan({
401
+ name: args.name,
402
+ type: "task",
403
+ event: args.event
404
+ });
405
+ try {
406
+ return await span.export();
407
+ } finally {
408
+ span.end();
409
+ }
410
+ } catch (err) {
411
+ console.warn("[os-dpt] failed to start conversation span:", err.message);
412
+ return void 0;
413
+ }
414
+ }
415
+ async function flushTracing() {
416
+ if (!enabled || !bt) return;
417
+ try {
418
+ await bt.flush();
419
+ } catch {
420
+ }
421
+ }
422
+
423
+ // server/api/ai-providers.ts
424
+ var AI_PROVIDER_IDS = [
425
+ "anthropic",
426
+ "openai",
427
+ "braintrust"
428
+ ];
429
+ var AI_PROVIDER_LABELS = {
430
+ anthropic: "Anthropic",
431
+ openai: "OpenAI",
432
+ braintrust: "Braintrust"
433
+ };
434
+ var AI_PROVIDER_ENV_VARS = {
435
+ anthropic: "ANTHROPIC_API_KEY",
436
+ openai: "OPENAI_API_KEY",
437
+ braintrust: "BRAINTRUST_API_KEY"
438
+ };
439
+ var AI_PROVIDER_KINDS = {
440
+ anthropic: "model",
441
+ openai: "model",
442
+ braintrust: "observability"
443
+ };
444
+ var vaultKey = (id) => `ai:${id}`;
445
+ function isAIProviderId(value) {
446
+ return AI_PROVIDER_IDS.includes(value);
447
+ }
448
+ function last4(apiKey) {
449
+ return apiKey.slice(-4);
450
+ }
451
+ function toApi(id, stored) {
452
+ return {
453
+ id,
454
+ label: AI_PROVIDER_LABELS[id],
455
+ kind: AI_PROVIDER_KINDS[id],
456
+ envVar: AI_PROVIDER_ENV_VARS[id],
457
+ configured: stored !== null,
458
+ last4: stored?.last4,
459
+ updatedAt: stored?.updatedAt
460
+ };
461
+ }
462
+ var VERIFY_TIMEOUT_MS = 1e4;
463
+ async function verifyKey(id, apiKey) {
464
+ const signal = AbortSignal.timeout(VERIFY_TIMEOUT_MS);
465
+ try {
466
+ if (id === "anthropic") {
467
+ const res2 = await fetch("https://api.anthropic.com/v1/models", {
468
+ headers: {
469
+ "x-api-key": apiKey,
470
+ "anthropic-version": "2023-06-01"
471
+ },
472
+ signal
473
+ });
474
+ if (!res2.ok) {
475
+ const body = await res2.json().catch(() => null);
476
+ throw new Error(body?.error?.message ?? `Anthropic returned ${res2.status}`);
477
+ }
478
+ return;
479
+ }
480
+ if (id === "braintrust") {
481
+ const appUrl = process.env.BRAINTRUST_APP_URL ?? "https://www.braintrust.dev";
482
+ const res2 = await fetch(`${appUrl}/api/apikey/login`, {
483
+ method: "POST",
484
+ headers: {
485
+ "Content-Type": "application/json",
486
+ Authorization: `Bearer ${apiKey}`
487
+ },
488
+ signal
489
+ });
490
+ if (!res2.ok) {
491
+ throw new Error(
492
+ res2.status === 401 || res2.status === 403 ? "Invalid Braintrust API key" : `Braintrust returned ${res2.status}`
493
+ );
494
+ }
495
+ return;
496
+ }
497
+ const res = await fetch("https://api.openai.com/v1/models", {
498
+ headers: { Authorization: `Bearer ${apiKey}` },
499
+ signal
500
+ });
501
+ if (!res.ok) {
502
+ const body = await res.json().catch(() => null);
503
+ throw new Error(body?.error?.message ?? `OpenAI returned ${res.status}`);
504
+ }
505
+ } catch (err) {
506
+ if (err.name === "TimeoutError") {
507
+ throw new Error(`Request timed out after ${VERIFY_TIMEOUT_MS / 1e3}s`);
508
+ }
509
+ throw err;
510
+ }
511
+ }
512
+ function aiProvidersRouter(workspace) {
513
+ const router = new Hono();
514
+ const store = new AIProviderStore(workspace);
515
+ const vault = new CredentialVault(workspace);
516
+ router.get("/", async (c) => {
517
+ const all = await store.list();
518
+ const providers = AI_PROVIDER_IDS.map((id) => toApi(id, all[id]));
519
+ return c.json({ providers });
520
+ });
521
+ router.put("/:provider", async (c) => {
522
+ const provider = c.req.param("provider");
523
+ if (!isAIProviderId(provider)) {
524
+ return c.json({ error: `Unknown provider: ${provider}` }, 400);
525
+ }
526
+ const body = await c.req.json().catch(() => ({}));
527
+ const apiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
528
+ if (apiKey === "") {
529
+ return c.json({ error: "Missing required field: apiKey" }, 400);
530
+ }
531
+ const previousKey = await vault.getPassword(vaultKey(provider));
532
+ await vault.setPassword(vaultKey(provider), apiKey);
533
+ const meta = {
534
+ last4: last4(apiKey),
535
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
536
+ };
537
+ try {
538
+ await store.set(provider, meta);
539
+ } catch (err) {
540
+ const rollback = previousKey !== null ? vault.setPassword(vaultKey(provider), previousKey) : vault.deletePassword(vaultKey(provider));
541
+ await rollback.catch((rollbackErr) => {
542
+ console.warn(
543
+ `Failed to roll back vault entry for ${provider} after store write failed:`,
544
+ rollbackErr
545
+ );
546
+ });
547
+ throw err;
548
+ }
549
+ if (provider === "braintrust") await refreshTracing();
550
+ return c.json({ provider: toApi(provider, meta) });
551
+ });
552
+ router.delete("/:provider", async (c) => {
553
+ const provider = c.req.param("provider");
554
+ if (!isAIProviderId(provider)) {
555
+ return c.json({ error: `Unknown provider: ${provider}` }, 400);
556
+ }
557
+ await store.remove(provider);
558
+ await vault.deletePassword(vaultKey(provider));
559
+ if (provider === "braintrust") await refreshTracing();
560
+ return c.json({ provider: toApi(provider, null) });
561
+ });
562
+ router.post("/:provider/test", async (c) => {
563
+ const provider = c.req.param("provider");
564
+ if (!isAIProviderId(provider)) {
565
+ return c.json({ ok: false, error: `Unknown provider: ${provider}` }, 400);
566
+ }
567
+ const body = await c.req.json().catch(() => ({}));
568
+ let apiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
569
+ if (apiKey === "") {
570
+ const stored = await vault.getPassword(vaultKey(provider));
571
+ if (stored === null) {
572
+ return c.json({ ok: false, error: "Missing required field: apiKey" }, 400);
573
+ }
574
+ apiKey = stored;
575
+ }
576
+ try {
577
+ await verifyKey(provider, apiKey);
578
+ return c.json({ ok: true });
579
+ } catch (err) {
580
+ return c.json({ ok: false, error: err.message }, 400);
581
+ }
582
+ });
583
+ return router;
584
+ }
585
+
586
+ // server/api/connections.ts
587
+ import crypto3 from "node:crypto";
588
+ import { promises as fs7 } from "node:fs";
589
+ import { Hono as Hono2 } from "hono";
590
+ import { HTTPException as HTTPException2 } from "hono/http-exception";
591
+
592
+ // server/db/introspect.ts
593
+ var SYSTEM_SCHEMAS = /* @__PURE__ */ new Set(["information_schema", "pg_catalog", "pg_toast"]);
594
+ var QUERY = `
595
+ SELECT table_schema, table_name, column_name, ordinal_position
596
+ FROM information_schema.columns
597
+ WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
598
+ AND table_schema NOT LIKE 'pg_temp_%'
599
+ AND table_schema NOT LIKE 'pg_toast_temp_%'
600
+ ORDER BY table_schema, table_name, ordinal_position
601
+ `;
602
+ async function introspect(pool) {
603
+ const { rows } = await pool.query(QUERY);
604
+ const grouped = /* @__PURE__ */ new Map();
605
+ for (const row of rows) {
606
+ if (SYSTEM_SCHEMAS.has(row.table_schema)) continue;
607
+ let schema = grouped.get(row.table_schema);
608
+ if (!schema) {
609
+ schema = /* @__PURE__ */ new Map();
610
+ grouped.set(row.table_schema, schema);
611
+ }
612
+ let cols = schema.get(row.table_name);
613
+ if (!cols) {
614
+ cols = [];
615
+ schema.set(row.table_name, cols);
616
+ }
617
+ cols.push(row.column_name);
618
+ }
619
+ const ns = {};
620
+ for (const [schemaName, tables] of grouped) {
621
+ const tableNs = {};
622
+ for (const [tableName, cols] of tables) {
623
+ tableNs[tableName] = cols;
624
+ }
625
+ if (schemaName === "public") {
626
+ for (const [tableName, cols] of tables) {
627
+ ns[tableName] = cols;
628
+ }
629
+ }
630
+ ns[schemaName] = tableNs;
631
+ }
632
+ return ns;
633
+ }
634
+
635
+ // server/db/postgres.ts
636
+ import pg from "pg";
637
+ function clientConfig(conn, password) {
638
+ return {
639
+ host: conn.host,
640
+ port: conn.port,
641
+ database: conn.database,
642
+ user: conn.user,
643
+ password,
644
+ ssl: conn.ssl ? { rejectUnauthorized: false } : false,
645
+ // For read-only connections, set the GUC in the startup packet so every
646
+ // session this pool opens defaults to read-only. Postgres then rejects plain
647
+ // writes (INSERT/UPDATE/DELETE/DDL) with SQLSTATE 25006 — a guard against
648
+ // *accidental* writes that needs no client-side SQL parsing.
649
+ //
650
+ // Not a hard boundary: default_transaction_read_only is a USERSET GUC, so a
651
+ // session can re-enable writes with `SET default_transaction_read_only=off`
652
+ // or `BEGIN READ WRITE`. For a true read-only guarantee, connect as a DB
653
+ // role that lacks write privileges (or point at a read replica).
654
+ ...conn.accessMode === "read-only" ? { options: "-c default_transaction_read_only=on" } : {}
655
+ };
656
+ }
657
+ async function testConnection(conn, password) {
658
+ const client = new pg.Client({
659
+ ...clientConfig(conn, password),
660
+ connectionTimeoutMillis: 5e3
661
+ });
662
+ try {
663
+ await client.connect();
664
+ await client.query("SELECT 1");
665
+ } catch (err) {
666
+ throw normalizePgError(err);
667
+ } finally {
668
+ await client.end().catch(() => {
669
+ });
670
+ }
671
+ }
672
+ function normalizePgError(err) {
673
+ if (err instanceof AggregateError) {
674
+ const messages = err.errors.map((e) => e instanceof Error ? e.message : String(e)).filter(Boolean);
675
+ return new Error(messages.join("; ") || "Connection failed");
676
+ }
677
+ if (err instanceof Error) {
678
+ const code = err.code;
679
+ if (!err.message && code) return new Error(code);
680
+ return err;
681
+ }
682
+ return new Error(String(err));
683
+ }
684
+ function createPool(conn, password) {
685
+ const pool = new pg.Pool({
686
+ ...clientConfig(conn, password),
687
+ keepAlive: true,
688
+ keepAliveInitialDelayMillis: 1e4,
689
+ idleTimeoutMillis: 3e4,
690
+ connectionTimeoutMillis: 5e3,
691
+ max: 5
692
+ });
693
+ pool.on("error", (err) => {
694
+ console.warn(`[pg pool ${conn.name}] idle client error:`, err.message);
695
+ });
696
+ return pool;
697
+ }
698
+ var PRE_EXECUTION_CODES = /* @__PURE__ */ new Set(["57P01", "57P02", "57P03"]);
699
+ var SOCKET_DROP_CODES = /* @__PURE__ */ new Set(["ECONNRESET"]);
700
+ var SOCKET_DROP_MESSAGE_PATTERNS = [
701
+ "Connection terminated",
702
+ "Client has encountered a connection error",
703
+ "connection terminated unexpectedly"
704
+ ];
705
+ function hasCode(err, codes) {
706
+ if (!err || typeof err !== "object") return false;
707
+ const code = err.code;
708
+ return typeof code === "string" && codes.has(code);
709
+ }
710
+ function hasMessage(err, patterns) {
711
+ if (!err || typeof err !== "object") return false;
712
+ const msg = err.message ?? "";
713
+ return patterns.some((p) => msg.includes(p));
714
+ }
715
+ function isPreExecutionStaleError(err) {
716
+ return hasCode(err, PRE_EXECUTION_CODES);
717
+ }
718
+ function isSocketDropError(err) {
719
+ return hasCode(err, SOCKET_DROP_CODES) || hasMessage(err, SOCKET_DROP_MESSAGE_PATTERNS);
720
+ }
721
+ async function runWithRetry(pool, fn, mode = "pre-execution") {
722
+ try {
723
+ return await fn(pool);
724
+ } catch (err) {
725
+ const retry = isPreExecutionStaleError(err) || mode === "idempotent" && isSocketDropError(err);
726
+ if (!retry) throw err;
727
+ return await fn(pool);
728
+ }
729
+ }
730
+
731
+ // server/db/registry.ts
732
+ var pools = /* @__PURE__ */ new Map();
733
+ function setPool(id, pool, accessMode) {
734
+ const prev = pools.get(id);
735
+ pools.set(id, { pool, accessMode });
736
+ if (prev && prev.pool !== pool) {
737
+ void prev.pool.end().catch(() => {
738
+ });
739
+ }
740
+ }
741
+ function getPool(id) {
742
+ return pools.get(id)?.pool;
743
+ }
744
+ function getAccessMode(id) {
745
+ return pools.get(id)?.accessMode;
746
+ }
747
+ async function removePool(id) {
748
+ const entry = pools.get(id);
749
+ if (!entry) return;
750
+ pools.delete(id);
751
+ await entry.pool.end().catch(() => {
752
+ });
753
+ }
754
+ function activeIds() {
755
+ return new Set(pools.keys());
756
+ }
757
+ async function closeAll() {
758
+ const entries = Array.from(pools.values());
759
+ pools.clear();
760
+ await Promise.all(entries.map((entry) => entry.pool.end().catch(() => {
761
+ })));
762
+ }
763
+
764
+ // server/lib/fs-atomic.ts
765
+ import { promises as fs5 } from "node:fs";
766
+ import path5 from "node:path";
767
+ import { randomBytes } from "node:crypto";
768
+ async function writeAtomic(filePath2, content) {
769
+ const dir = path5.dirname(filePath2);
770
+ const tmp = path5.join(dir, `.${path5.basename(filePath2)}.${randomBytes(4).toString("hex")}.tmp`);
771
+ await fs5.mkdir(dir, { recursive: true });
772
+ await fs5.writeFile(tmp, content);
773
+ await fs5.rename(tmp, filePath2);
774
+ }
775
+
776
+ // server/storage/connections.ts
777
+ import { promises as fs6 } from "node:fs";
778
+ import path6 from "node:path";
779
+ function normalize(conn) {
780
+ return {
781
+ ...conn,
782
+ accessMode: conn.accessMode === "read-only" ? "read-only" : "read-write"
783
+ };
784
+ }
785
+ var FILE4 = "connections.json";
786
+ var ConnectionStore = class {
787
+ constructor(workspace) {
788
+ this.workspace = workspace;
789
+ }
790
+ // Serializes read-modify-write cycles so concurrent add/remove calls
791
+ // don't clobber each other.
792
+ queue = Promise.resolve();
793
+ enqueue(fn) {
794
+ const next = this.queue.then(fn, fn);
795
+ this.queue = next.catch(() => {
796
+ });
797
+ return next;
798
+ }
799
+ get filePath() {
800
+ return path6.join(this.workspace, FILE4);
801
+ }
802
+ async list() {
803
+ try {
804
+ const raw = await fs6.readFile(this.filePath, "utf8");
805
+ const data = JSON.parse(raw);
806
+ return Array.isArray(data.connections) ? data.connections.map(normalize) : [];
807
+ } catch (err) {
808
+ if (err.code === "ENOENT") return [];
809
+ throw err;
810
+ }
811
+ }
812
+ async get(id) {
813
+ const all = await this.list();
814
+ return all.find((c) => c.id === id) ?? null;
815
+ }
816
+ add(conn) {
817
+ return this.enqueue(async () => {
818
+ const existing = await this.list();
819
+ await this.write([...existing.filter((c) => c.id !== conn.id), conn]);
820
+ });
821
+ }
822
+ remove(id) {
823
+ return this.enqueue(async () => {
824
+ const existing = await this.list();
825
+ await this.write(existing.filter((c) => c.id !== id));
826
+ });
827
+ }
828
+ // Apply a partial patch to one connection's metadata. Returns the updated
829
+ // record, or null if no connection has that id. id/createdAt are immutable.
830
+ update(id, patch) {
831
+ return this.enqueue(async () => {
832
+ const existing = await this.list();
833
+ const current = existing.find((c) => c.id === id);
834
+ if (!current) return null;
835
+ const updated = normalize({ ...current, ...patch, id: current.id });
836
+ await this.write(existing.map((c) => c.id === id ? updated : c));
837
+ return updated;
838
+ });
839
+ }
840
+ async write(connections) {
841
+ await fs6.mkdir(this.workspace, { recursive: true });
842
+ await fs6.writeFile(
843
+ this.filePath,
844
+ JSON.stringify({ connections }, null, 2) + "\n",
845
+ "utf8"
846
+ );
847
+ }
848
+ };
849
+
850
+ // server/api/connections.ts
851
+ var MAX_ROWS = 1e3;
852
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
853
+ function assertSafeConnectionId2(id) {
854
+ if (!UUID_RE.test(id)) {
855
+ throw new HTTPException2(400, { message: `Invalid connection id` });
856
+ }
857
+ }
858
+ async function writeConnectionSchema(id, schema) {
859
+ await writeAtomic(paths.connectionSchema(id), JSON.stringify(schema, null, 2));
860
+ }
861
+ function parseInput(body) {
862
+ if (typeof body !== "object" || body === null) {
863
+ throw new Error("Invalid request body");
864
+ }
865
+ const b = body;
866
+ const requireStr = (v, field) => {
867
+ if (typeof v !== "string" || v.trim() === "") {
868
+ throw new Error(`Missing required field: ${field}`);
869
+ }
870
+ return v.trim();
871
+ };
872
+ const parsePort = (v) => {
873
+ const n = typeof v === "number" ? v : Number(v);
874
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
875
+ throw new Error(`Invalid port: must be an integer in [1, 65535]`);
876
+ }
877
+ return n;
878
+ };
879
+ return {
880
+ name: requireStr(b.name, "name"),
881
+ driver: "postgres",
882
+ host: requireStr(b.host, "host"),
883
+ port: parsePort(b.port ?? 5432),
884
+ database: requireStr(b.database, "database"),
885
+ user: requireStr(b.user, "user"),
886
+ password: typeof b.password === "string" ? b.password : "",
887
+ ssl: Boolean(b.ssl),
888
+ // Secure by default: a new connection is read-only unless the caller
889
+ // explicitly asks for "read-write". The client dialog sends an explicit
890
+ // mode; this default only governs direct API callers that omit it. (Legacy
891
+ // *stored* connections without the field stay read-write — see
892
+ // storage/connections.ts — so upgrading never silently revokes writes.)
893
+ accessMode: b.accessMode === "read-write" ? "read-write" : "read-only"
894
+ };
895
+ }
896
+ function parsePatch(body) {
897
+ if (typeof body !== "object" || body === null) {
898
+ throw new Error("Invalid request body");
899
+ }
900
+ const b = body;
901
+ const patch = {};
902
+ const requireStr = (v, field) => {
903
+ if (typeof v !== "string" || v.trim() === "") {
904
+ throw new Error(`Missing required field: ${field}`);
905
+ }
906
+ return v.trim();
907
+ };
908
+ if ("name" in b) patch.name = requireStr(b.name, "name");
909
+ if ("host" in b) patch.host = requireStr(b.host, "host");
910
+ if ("port" in b) {
911
+ const n = typeof b.port === "number" ? b.port : Number(b.port);
912
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
913
+ throw new Error("Invalid port: must be an integer in [1, 65535]");
914
+ }
915
+ patch.port = n;
916
+ }
917
+ if ("database" in b) patch.database = requireStr(b.database, "database");
918
+ if ("user" in b) patch.user = requireStr(b.user, "user");
919
+ if ("ssl" in b) patch.ssl = Boolean(b.ssl);
920
+ if ("accessMode" in b) {
921
+ if (b.accessMode !== "read-only" && b.accessMode !== "read-write") {
922
+ throw new Error("accessMode must be 'read-only' or 'read-write'");
923
+ }
924
+ patch.accessMode = b.accessMode;
925
+ }
926
+ const password = typeof b.password === "string" && b.password !== "" ? b.password : void 0;
927
+ return { patch, password };
928
+ }
929
+ function toApi2(conn, active) {
930
+ return { ...conn, active: active.has(conn.id) };
931
+ }
932
+ function buildProbe(input) {
933
+ return {
934
+ id: "probe",
935
+ name: input.name,
936
+ driver: "postgres",
937
+ host: input.host,
938
+ port: input.port,
939
+ database: input.database,
940
+ user: input.user,
941
+ ssl: input.ssl,
942
+ accessMode: input.accessMode,
943
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
944
+ };
945
+ }
946
+ async function connectStored(id, store, vault, mode = "replace") {
947
+ const conn = await store.get(id);
948
+ if (!conn) return { ok: false, error: "not_found", status: 404 };
949
+ const password = await vault.getPassword(id);
950
+ if (password === null) {
951
+ return { ok: false, error: "missing_credentials", status: 400 };
952
+ }
953
+ const pool = createPool(conn, password);
954
+ try {
955
+ const client = await pool.connect();
956
+ try {
957
+ await client.query("SELECT 1");
958
+ } finally {
959
+ client.release();
960
+ }
961
+ if (mode === "claim" && getPool(id)) {
962
+ await pool.end().catch(() => {
963
+ });
964
+ return { ok: true };
965
+ }
966
+ setPool(id, pool, conn.accessMode);
967
+ void introspect(pool).then((ns) => writeConnectionSchema(id, ns)).catch((err) => console.warn(`[introspect ${id}]`, err));
968
+ return { ok: true };
969
+ } catch (err) {
970
+ await pool.end().catch(() => {
971
+ });
972
+ return { ok: false, error: normalizePgError(err).message, status: 400 };
973
+ }
974
+ }
975
+ async function autoConnectAll(workspace) {
976
+ const store = new ConnectionStore(workspace);
977
+ const vault = new CredentialVault(workspace);
978
+ const connections = await store.list();
979
+ await Promise.all(
980
+ connections.map(async (conn) => {
981
+ if (getPool(conn.id)) return;
982
+ const result = await connectStored(conn.id, store, vault, "claim");
983
+ if (result.ok) {
984
+ console.log(`[auto-connect] ${conn.name} (${conn.id})`);
985
+ } else if (result.error !== "missing_credentials") {
986
+ console.warn(`[auto-connect] ${conn.name}: ${result.error}`);
987
+ }
988
+ })
989
+ );
990
+ }
991
+ function connectionsRouter(workspace) {
992
+ const router = new Hono2();
993
+ const store = new ConnectionStore(workspace);
994
+ const vault = new CredentialVault(workspace);
995
+ router.get("/", async (c) => {
996
+ const connections = await store.list();
997
+ const active = activeIds();
998
+ return c.json({ connections: connections.map((conn) => toApi2(conn, active)) });
999
+ });
1000
+ router.post("/", async (c) => {
1001
+ let input;
1002
+ try {
1003
+ input = parseInput(await c.req.json());
1004
+ } catch (err) {
1005
+ return c.json({ error: err.message }, 400);
1006
+ }
1007
+ const stored = {
1008
+ id: crypto3.randomUUID(),
1009
+ name: input.name,
1010
+ driver: "postgres",
1011
+ host: input.host,
1012
+ port: input.port,
1013
+ database: input.database,
1014
+ user: input.user,
1015
+ ssl: input.ssl,
1016
+ accessMode: input.accessMode,
1017
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1018
+ };
1019
+ await store.add(stored);
1020
+ if (input.password) await vault.setPassword(stored.id, input.password);
1021
+ return c.json({ connection: toApi2(stored, activeIds()) }, 201);
1022
+ });
1023
+ router.patch("/:id", async (c) => {
1024
+ const id = c.req.param("id");
1025
+ assertSafeConnectionId2(id);
1026
+ let parsed;
1027
+ try {
1028
+ parsed = parsePatch(await c.req.json());
1029
+ } catch (err) {
1030
+ return c.json({ error: err.message }, 400);
1031
+ }
1032
+ const updated = await store.update(id, parsed.patch);
1033
+ if (!updated) return c.json({ error: "not_found" }, 404);
1034
+ if (parsed.password !== void 0) {
1035
+ await vault.setPassword(id, parsed.password);
1036
+ }
1037
+ if (getPool(id)) {
1038
+ const result = await connectStored(id, store, vault);
1039
+ if (!result.ok) {
1040
+ await removePool(id);
1041
+ return c.json({ ok: false, error: result.error }, 400);
1042
+ }
1043
+ }
1044
+ return c.json({ connection: toApi2(updated, activeIds()) });
1045
+ });
1046
+ router.delete("/:id", async (c) => {
1047
+ const id = c.req.param("id");
1048
+ assertSafeConnectionId2(id);
1049
+ await removePool(id);
1050
+ await vault.deletePassword(id);
1051
+ await store.remove(id);
1052
+ await fs7.rm(paths.connectionSchema(id), { force: true }).catch(() => {
1053
+ });
1054
+ return c.json({ ok: true });
1055
+ });
1056
+ router.post("/test", async (c) => {
1057
+ let input;
1058
+ try {
1059
+ input = parseInput(await c.req.json());
1060
+ } catch (err) {
1061
+ return c.json({ ok: false, error: err.message }, 400);
1062
+ }
1063
+ try {
1064
+ await testConnection(buildProbe(input), input.password);
1065
+ return c.json({ ok: true });
1066
+ } catch (err) {
1067
+ return c.json({ ok: false, error: err.message }, 400);
1068
+ }
1069
+ });
1070
+ router.post("/:id/test", async (c) => {
1071
+ const id = c.req.param("id");
1072
+ assertSafeConnectionId2(id);
1073
+ let input;
1074
+ try {
1075
+ input = parseInput(await c.req.json());
1076
+ } catch (err) {
1077
+ return c.json({ ok: false, error: err.message }, 400);
1078
+ }
1079
+ let password = input.password;
1080
+ if (!password) {
1081
+ const stored = await vault.getPassword(id);
1082
+ if (stored === null) {
1083
+ return c.json(
1084
+ { ok: false, error: "No stored password \u2014 enter one to test." },
1085
+ 400
1086
+ );
1087
+ }
1088
+ password = stored;
1089
+ }
1090
+ try {
1091
+ await testConnection(buildProbe(input), password);
1092
+ return c.json({ ok: true });
1093
+ } catch (err) {
1094
+ return c.json({ ok: false, error: err.message }, 400);
1095
+ }
1096
+ });
1097
+ router.post("/:id/connect", async (c) => {
1098
+ const id = c.req.param("id");
1099
+ assertSafeConnectionId2(id);
1100
+ const result = await connectStored(id, store, vault);
1101
+ if (result.ok) return c.json({ ok: true });
1102
+ if (result.error === "not_found") return c.json({ error: "not_found" }, 404);
1103
+ return c.json({ ok: false, error: result.error }, 400);
1104
+ });
1105
+ router.post("/:id/query", async (c) => {
1106
+ const id = c.req.param("id");
1107
+ assertSafeConnectionId2(id);
1108
+ const pool = getPool(id);
1109
+ if (!pool) return c.json({ ok: false, error: "not_connected" }, 409);
1110
+ let sql;
1111
+ try {
1112
+ const body = await c.req.json();
1113
+ if (typeof body.sql !== "string" || body.sql.trim() === "") {
1114
+ return c.json({ ok: false, error: "Missing sql" }, 400);
1115
+ }
1116
+ sql = body.sql;
1117
+ } catch {
1118
+ return c.json({ ok: false, error: "Invalid JSON body" }, 400);
1119
+ }
1120
+ const started = Date.now();
1121
+ try {
1122
+ const result = await runWithRetry(
1123
+ pool,
1124
+ (p) => p.query({ text: sql, rowMode: "array" })
1125
+ );
1126
+ const durationMs = Date.now() - started;
1127
+ const allRows = Array.isArray(result.rows) ? result.rows : [];
1128
+ const truncated = allRows.length > MAX_ROWS;
1129
+ const rows = truncated ? allRows.slice(0, MAX_ROWS) : allRows;
1130
+ const payload = {
1131
+ ok: true,
1132
+ columns: (result.fields ?? []).map((f) => ({
1133
+ name: f.name,
1134
+ dataTypeID: f.dataTypeID
1135
+ })),
1136
+ rows,
1137
+ rowCount: typeof result.rowCount === "number" ? result.rowCount : rows.length,
1138
+ durationMs,
1139
+ truncated
1140
+ };
1141
+ return c.json(payload);
1142
+ } catch (err) {
1143
+ const normalized = normalizePgError(err);
1144
+ const code = err?.code;
1145
+ return c.json({ ok: false, error: normalized.message, code }, 200);
1146
+ }
1147
+ });
1148
+ router.get("/:id/schema", async (c) => {
1149
+ const id = c.req.param("id");
1150
+ assertSafeConnectionId2(id);
1151
+ try {
1152
+ const raw = await fs7.readFile(paths.connectionSchema(id), "utf8");
1153
+ return c.json(JSON.parse(raw));
1154
+ } catch {
1155
+ return c.json({});
1156
+ }
1157
+ });
1158
+ router.post("/:id/schema/refresh", async (c) => {
1159
+ const id = c.req.param("id");
1160
+ assertSafeConnectionId2(id);
1161
+ const pool = getPool(id);
1162
+ if (!pool) return c.json({ ok: false, error: "not_connected" }, 409);
1163
+ try {
1164
+ const schema = await runWithRetry(pool, (p) => introspect(p), "idempotent");
1165
+ await writeConnectionSchema(id, schema);
1166
+ return c.json(schema);
1167
+ } catch (err) {
1168
+ return c.json({ ok: false, error: normalizePgError(err).message }, 500);
1169
+ }
1170
+ });
1171
+ router.post("/:id/disconnect", async (c) => {
1172
+ const id = c.req.param("id");
1173
+ assertSafeConnectionId2(id);
1174
+ await removePool(id);
1175
+ return c.json({ ok: true });
1176
+ });
1177
+ return router;
1178
+ }
1179
+
1180
+ // server/api/worksheets.ts
1181
+ import { Hono as Hono3 } from "hono";
1182
+ import { promises as fs9 } from "node:fs";
1183
+ import path8 from "node:path";
1184
+
1185
+ // server/lib/slug.ts
1186
+ function slugify(name) {
1187
+ const base = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1188
+ return base || "worksheet";
1189
+ }
1190
+ function dedupeSlug(slug, existing) {
1191
+ if (!existing.has(slug)) return slug;
1192
+ let n = 2;
1193
+ while (existing.has(`${slug}-${n}`)) n++;
1194
+ return `${slug}-${n}`;
1195
+ }
1196
+
1197
+ // server/history/record.ts
1198
+ import { createHash } from "node:crypto";
1199
+ import { gzipSync } from "node:zlib";
1200
+
1201
+ // server/history/db.ts
1202
+ import Database from "better-sqlite3";
1203
+ var SCHEMA = `
1204
+ PRAGMA journal_mode = WAL;
1205
+ PRAGMA foreign_keys = ON;
1206
+ PRAGMA synchronous = NORMAL;
1207
+
1208
+ CREATE TABLE IF NOT EXISTS blobs (
1209
+ sha TEXT PRIMARY KEY,
1210
+ bytes BLOB NOT NULL,
1211
+ size INTEGER NOT NULL
1212
+ );
1213
+
1214
+ CREATE TABLE IF NOT EXISTS entries (
1215
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1216
+ worksheet TEXT NOT NULL,
1217
+ ts INTEGER NOT NULL,
1218
+ source TEXT NOT NULL,
1219
+ blob_sha TEXT NOT NULL REFERENCES blobs(sha),
1220
+ size INTEGER NOT NULL,
1221
+ label TEXT,
1222
+ meta TEXT,
1223
+ preview TEXT NOT NULL DEFAULT ''
1224
+ );
1225
+
1226
+ CREATE INDEX IF NOT EXISTS idx_entries_worksheet_ts ON entries(worksheet, ts DESC);
1227
+ `;
1228
+ var db = null;
1229
+ function openHistoryDb() {
1230
+ if (db) return db;
1231
+ db = new Database(paths.history());
1232
+ db.exec(SCHEMA);
1233
+ return db;
1234
+ }
1235
+ function closeHistoryDb() {
1236
+ if (!db) return;
1237
+ db.close();
1238
+ db = null;
1239
+ }
1240
+
1241
+ // server/history/dedup.ts
1242
+ var TTL_MS = 5e3;
1243
+ var recent = /* @__PURE__ */ new Map();
1244
+ function noteRecentWrite(slug, sha) {
1245
+ let set = recent.get(slug);
1246
+ if (!set) {
1247
+ set = /* @__PURE__ */ new Set();
1248
+ recent.set(slug, set);
1249
+ }
1250
+ set.add(sha);
1251
+ const t = setTimeout(() => {
1252
+ const s = recent.get(slug);
1253
+ if (!s) return;
1254
+ s.delete(sha);
1255
+ if (s.size === 0) recent.delete(slug);
1256
+ }, TTL_MS);
1257
+ t.unref?.();
1258
+ }
1259
+ function isRecentWrite(slug, sha) {
1260
+ return recent.get(slug)?.has(sha) ?? false;
1261
+ }
1262
+
1263
+ // server/history/record.ts
1264
+ var MERGE_WINDOW_MS = 1e4;
1265
+ var MAX_ENTRIES_PER_WORKSHEET = 50;
1266
+ var MAX_ENTRY_SIZE = 256 * 1024;
1267
+ var PREVIEW_MAX_LEN = 120;
1268
+ var SOURCE_RANK = {
1269
+ autosave: 0,
1270
+ external: 1,
1271
+ snapshot: 1,
1272
+ save: 2,
1273
+ revert: 3
1274
+ };
1275
+ function sha256(s) {
1276
+ return createHash("sha256").update(s, "utf8").digest("hex");
1277
+ }
1278
+ function buildPreview(content) {
1279
+ const line = content.split("\n").find((l) => l.trim().length > 0) ?? "";
1280
+ return line.length > PREVIEW_MAX_LEN ? line.slice(0, PREVIEW_MAX_LEN) + "\u2026" : line;
1281
+ }
1282
+ function recordHistory(opts) {
1283
+ assertSafeSlug(opts.worksheet);
1284
+ const size = Buffer.byteLength(opts.content, "utf8");
1285
+ if (size > MAX_ENTRY_SIZE) {
1286
+ console.warn(`[history] skipping ${opts.worksheet}: ${size} bytes exceeds cap`);
1287
+ return { recorded: false, skipped: "oversize" };
1288
+ }
1289
+ const db2 = openHistoryDb();
1290
+ const ts = opts.ts ?? Date.now();
1291
+ const sha = sha256(opts.content);
1292
+ const prev = db2.prepare(
1293
+ `SELECT id, ts, source, blob_sha FROM entries WHERE worksheet = ? ORDER BY ts DESC LIMIT 1`
1294
+ ).get(opts.worksheet);
1295
+ if (prev && prev.blob_sha === sha) {
1296
+ if (SOURCE_RANK[opts.source] > SOURCE_RANK[prev.source]) {
1297
+ db2.prepare(
1298
+ `UPDATE entries SET ts = ?, source = ?, label = COALESCE(?, label), meta = COALESCE(?, meta)
1299
+ WHERE id = ?`
1300
+ ).run(
1301
+ ts,
1302
+ opts.source,
1303
+ opts.label ?? null,
1304
+ opts.meta ? JSON.stringify(opts.meta) : null,
1305
+ prev.id
1306
+ );
1307
+ }
1308
+ return { recorded: true };
1309
+ }
1310
+ const canCoalesce = !!prev && opts.source === "autosave" && prev.source === "autosave" && ts - prev.ts < MERGE_WINDOW_MS;
1311
+ const compressed = gzipSync(Buffer.from(opts.content, "utf8"));
1312
+ const preview = buildPreview(opts.content);
1313
+ const insertBlob = db2.prepare(
1314
+ `INSERT OR IGNORE INTO blobs (sha, bytes, size) VALUES (?, ?, ?)`
1315
+ );
1316
+ const insertEntry = db2.prepare(
1317
+ `INSERT INTO entries (worksheet, ts, source, blob_sha, size, label, meta, preview)
1318
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1319
+ );
1320
+ const deleteById = db2.prepare(`DELETE FROM entries WHERE id = ?`);
1321
+ const tx = db2.transaction(() => {
1322
+ insertBlob.run(sha, compressed, size);
1323
+ const coalesced = !!(canCoalesce && prev) && deleteById.run(prev.id).changes > 0;
1324
+ insertEntry.run(
1325
+ opts.worksheet,
1326
+ ts,
1327
+ opts.source,
1328
+ sha,
1329
+ size,
1330
+ opts.label ?? null,
1331
+ opts.meta ? JSON.stringify(opts.meta) : null,
1332
+ preview
1333
+ );
1334
+ const evicted = enforcePerWorksheetCap(opts.worksheet);
1335
+ if (coalesced || evicted) gcOrphanBlobs();
1336
+ });
1337
+ tx();
1338
+ noteRecentWrite(opts.worksheet, sha);
1339
+ return { recorded: true };
1340
+ }
1341
+ function enforcePerWorksheetCap(worksheet) {
1342
+ const db2 = openHistoryDb();
1343
+ const { n } = db2.prepare(`SELECT COUNT(*) as n FROM entries WHERE worksheet = ?`).get(worksheet);
1344
+ if (n <= MAX_ENTRIES_PER_WORKSHEET) return false;
1345
+ const result = db2.prepare(
1346
+ `DELETE FROM entries WHERE id IN (
1347
+ SELECT id FROM entries WHERE worksheet = ? ORDER BY ts ASC LIMIT ?
1348
+ )`
1349
+ ).run(worksheet, n - MAX_ENTRIES_PER_WORKSHEET);
1350
+ return result.changes > 0;
1351
+ }
1352
+ function gcOrphanBlobs() {
1353
+ const db2 = openHistoryDb();
1354
+ db2.prepare(
1355
+ `DELETE FROM blobs WHERE sha NOT IN (SELECT DISTINCT blob_sha FROM entries)`
1356
+ ).run();
1357
+ }
1358
+
1359
+ // server/history/query.ts
1360
+ import { gunzipSync } from "node:zlib";
1361
+ function rowToEntry(row) {
1362
+ return {
1363
+ id: row.id,
1364
+ worksheet: row.worksheet,
1365
+ ts: row.ts,
1366
+ source: row.source,
1367
+ size: row.size,
1368
+ label: row.label,
1369
+ meta: row.meta ? JSON.parse(row.meta) : null,
1370
+ preview: row.preview,
1371
+ contentSha: row.blob_sha
1372
+ };
1373
+ }
1374
+ function listEntries(worksheet) {
1375
+ assertSafeSlug(worksheet);
1376
+ const db2 = openHistoryDb();
1377
+ const rows = db2.prepare(
1378
+ `SELECT id, worksheet, ts, source, blob_sha, size, label, meta, preview
1379
+ FROM entries WHERE worksheet = ? ORDER BY ts DESC`
1380
+ ).all(worksheet);
1381
+ return rows.map(rowToEntry);
1382
+ }
1383
+ function hasEntries(worksheet) {
1384
+ assertSafeSlug(worksheet);
1385
+ const db2 = openHistoryDb();
1386
+ const row = db2.prepare(`SELECT 1 as one FROM entries WHERE worksheet = ? LIMIT 1`).get(worksheet);
1387
+ return !!row;
1388
+ }
1389
+ function getEntry(worksheet, id) {
1390
+ assertSafeSlug(worksheet);
1391
+ const db2 = openHistoryDb();
1392
+ const row = db2.prepare(
1393
+ `SELECT e.id, e.worksheet, e.ts, e.source, e.blob_sha, e.size, e.label, e.meta, e.preview, b.bytes
1394
+ FROM entries e JOIN blobs b ON b.sha = e.blob_sha
1395
+ WHERE e.worksheet = ? AND e.id = ?`
1396
+ ).get(worksheet, id);
1397
+ if (!row) return null;
1398
+ const content = gunzipSync(row.bytes).toString("utf8");
1399
+ return { ...rowToEntry(row), content };
1400
+ }
1401
+ async function revertToEntry(worksheet, id) {
1402
+ const entry = getEntry(worksheet, id);
1403
+ if (!entry) return null;
1404
+ recordHistory({
1405
+ worksheet,
1406
+ source: "revert",
1407
+ content: entry.content,
1408
+ meta: { revertedFromEntryId: id }
1409
+ });
1410
+ await writeAtomic(paths.worksheet(worksheet), entry.content);
1411
+ return entry;
1412
+ }
1413
+ function deleteWorksheetHistory(worksheet) {
1414
+ assertSafeSlug(worksheet);
1415
+ const db2 = openHistoryDb();
1416
+ const tx = db2.transaction(() => {
1417
+ db2.prepare(`DELETE FROM entries WHERE worksheet = ?`).run(worksheet);
1418
+ db2.prepare(
1419
+ `DELETE FROM blobs WHERE sha NOT IN (SELECT DISTINCT blob_sha FROM entries)`
1420
+ ).run();
1421
+ });
1422
+ tx();
1423
+ }
1424
+
1425
+ // server/storage/worksheet-names.ts
1426
+ import { promises as fs8 } from "node:fs";
1427
+ import path7 from "node:path";
1428
+ var FILE5 = path7.join(".os-dpt", "worksheet-names.json");
1429
+ function filePath() {
1430
+ return path7.join(workspaceRoot(), FILE5);
1431
+ }
1432
+ async function readNames() {
1433
+ try {
1434
+ const raw = await fs8.readFile(filePath(), "utf8");
1435
+ const data = JSON.parse(raw);
1436
+ return data.names ?? {};
1437
+ } catch (err) {
1438
+ if (err.code === "ENOENT") return {};
1439
+ throw err;
1440
+ }
1441
+ }
1442
+ async function getName(slug) {
1443
+ const all = await readNames();
1444
+ return all[slug] ?? null;
1445
+ }
1446
+ async function writeNames(all) {
1447
+ await writeAtomic(filePath(), JSON.stringify({ names: all }, null, 2) + "\n");
1448
+ }
1449
+ var queue = Promise.resolve();
1450
+ function enqueue(fn) {
1451
+ const next = queue.then(fn, fn);
1452
+ queue = next.catch(() => {
1453
+ });
1454
+ return next;
1455
+ }
1456
+ function setName(slug, name) {
1457
+ return enqueue(async () => {
1458
+ const all = await readNames();
1459
+ if (all[slug] === name) return;
1460
+ all[slug] = name;
1461
+ await writeNames(all);
1462
+ });
1463
+ }
1464
+ function deleteName(slug) {
1465
+ return enqueue(async () => {
1466
+ const all = await readNames();
1467
+ if (!(slug in all)) return;
1468
+ delete all[slug];
1469
+ await writeNames(all);
1470
+ });
1471
+ }
1472
+
1473
+ // server/agent/naming.ts
1474
+ import Anthropic2 from "@anthropic-ai/sdk";
1475
+
1476
+ // server/agent/provider.ts
1477
+ import Anthropic from "@anthropic-ai/sdk";
1478
+ var DEFAULT_MODEL = process.env.ANTHROPIC_MODEL ?? "claude-sonnet-4-6";
1479
+ var DEFAULT_MAX_TOKENS = 4096;
1480
+ var MissingApiKeyError = class extends Error {
1481
+ constructor() {
1482
+ super("Anthropic API key not configured. Add one in Settings \u2192 AI providers.");
1483
+ this.name = "MissingApiKeyError";
1484
+ }
1485
+ };
1486
+ var cachedKey = null;
1487
+ async function getAnthropicKey() {
1488
+ if (cachedKey) return cachedKey;
1489
+ const vault = new CredentialVault(workspaceRoot());
1490
+ const key = await vault.getPassword("ai:anthropic");
1491
+ if (!key) throw new MissingApiKeyError();
1492
+ cachedKey = key;
1493
+ return key;
1494
+ }
1495
+ var CACHE_CONTROL = { type: "ephemeral" };
1496
+ function cachedSystem(system) {
1497
+ return [{ type: "text", text: system, cache_control: CACHE_CONTROL }];
1498
+ }
1499
+ function withConversationCacheBreakpoint(messages) {
1500
+ if (messages.length === 0) return messages;
1501
+ const out = messages.slice();
1502
+ const last = out[out.length - 1];
1503
+ const blocks = typeof last.content === "string" ? [{ type: "text", text: last.content }] : last.content.slice();
1504
+ if (blocks.length === 0) return messages;
1505
+ blocks[blocks.length - 1] = {
1506
+ ...blocks[blocks.length - 1],
1507
+ cache_control: CACHE_CONTROL
1508
+ };
1509
+ out[out.length - 1] = { ...last, content: blocks };
1510
+ return out;
1511
+ }
1512
+ async function streamAssistantMessage(p) {
1513
+ const client = new Anthropic({ apiKey: p.apiKey });
1514
+ const model = p.model ?? DEFAULT_MODEL;
1515
+ const maxTokens = p.maxTokens ?? DEFAULT_MAX_TOKENS;
1516
+ return traced(
1517
+ {
1518
+ name: "anthropic.messages",
1519
+ type: "llm",
1520
+ event: {
1521
+ input: [
1522
+ { role: "system", content: p.system },
1523
+ ...p.messages
1524
+ ],
1525
+ metadata: {
1526
+ model,
1527
+ max_tokens: maxTokens,
1528
+ provider: "anthropic",
1529
+ tools: p.tools.map((t) => t.name)
1530
+ }
1531
+ }
1532
+ },
1533
+ async (span) => {
1534
+ const stream = client.messages.stream({
1535
+ model,
1536
+ max_tokens: maxTokens,
1537
+ system: cachedSystem(p.system),
1538
+ messages: withConversationCacheBreakpoint(p.messages),
1539
+ tools: p.tools,
1540
+ // One tool_use per assistant turn so the loop can cleanly handle the
1541
+ // ask_user_question pause without leaving sibling tool_use blocks
1542
+ // without matching tool_results.
1543
+ tool_choice: { type: "auto", disable_parallel_tool_use: true }
1544
+ });
1545
+ if (p.onTextDelta) {
1546
+ stream.on("text", (delta) => p.onTextDelta?.(delta));
1547
+ }
1548
+ const message = await stream.finalMessage();
1549
+ const usage = message.usage;
1550
+ const cachedRead = usage.cache_read_input_tokens ?? 0;
1551
+ const cacheCreation = usage.cache_creation_input_tokens ?? 0;
1552
+ const promptTokens = (usage.input_tokens ?? 0) + cachedRead + cacheCreation;
1553
+ const completionTokens = usage.output_tokens ?? 0;
1554
+ span.log({
1555
+ output: message.content,
1556
+ metadata: { stop_reason: message.stop_reason },
1557
+ metrics: {
1558
+ prompt_tokens: promptTokens,
1559
+ completion_tokens: completionTokens,
1560
+ total_tokens: promptTokens + completionTokens,
1561
+ prompt_cached_tokens: cachedRead,
1562
+ prompt_cache_creation_tokens: cacheCreation
1563
+ }
1564
+ });
1565
+ return { message, model, usage };
1566
+ }
1567
+ );
1568
+ }
1569
+
1570
+ // server/agent/naming.ts
1571
+ var NAMING_MODEL = "claude-haiku-4-5-20251001";
1572
+ var MAX_INPUT_CHARS = 2e3;
1573
+ var MAX_NAME_CHARS = 60;
1574
+ var SQL_SYSTEM_PROMPT = "You name SQL queries. Given a SQL snippet, respond with a concise 2-5 word title in Title Case. No quotes, no punctuation, no preamble. Just the title.";
1575
+ var CHAT_SYSTEM_PROMPT = "You name chat conversations. Given the user's first message, respond with a concise 2-5 word title in Title Case summarizing the topic. No quotes, no punctuation, no preamble. Just the title.";
1576
+ function sanitize(raw) {
1577
+ let s = raw.trim();
1578
+ s = s.replace(/^["'`“”‘’]+|["'`“”‘’]+$/g, "");
1579
+ s = s.replace(/\s+/g, " ").trim();
1580
+ s = s.replace(/[.!?;:,]+$/, "").trim();
1581
+ if (s.length > MAX_NAME_CHARS) s = s.slice(0, MAX_NAME_CHARS).trim();
1582
+ return s;
1583
+ }
1584
+ async function generateTitle(input, systemPrompt) {
1585
+ const apiKey = await getAnthropicKey();
1586
+ const snippet = input.length > MAX_INPUT_CHARS ? input.slice(0, MAX_INPUT_CHARS) : input;
1587
+ const client = new Anthropic2({ apiKey });
1588
+ const msg = await client.messages.create({
1589
+ model: NAMING_MODEL,
1590
+ max_tokens: 32,
1591
+ system: systemPrompt,
1592
+ messages: [{ role: "user", content: snippet }]
1593
+ });
1594
+ const text = msg.content.map((block) => block.type === "text" ? block.text : "").join("");
1595
+ const name = sanitize(text);
1596
+ if (!name) throw new Error("Empty name from model");
1597
+ return name;
1598
+ }
1599
+ function generateWorksheetName(sql) {
1600
+ return generateTitle(sql, SQL_SYSTEM_PROMPT);
1601
+ }
1602
+ function generateChatTitle(prompt) {
1603
+ return generateTitle(prompt, CHAT_SYSTEM_PROMPT);
1604
+ }
1605
+
1606
+ // server/api/worksheets.ts
1607
+ var app = new Hono3();
1608
+ var MAX_NAME_LEN = 80;
1609
+ async function listMetas() {
1610
+ const dir = paths.worksheets();
1611
+ const entries = await fs9.readdir(dir).catch(() => []);
1612
+ const sqls = entries.filter((f) => f.endsWith(".sql"));
1613
+ const names = await readNames();
1614
+ const metas = await Promise.all(
1615
+ sqls.map(async (f) => {
1616
+ const slug = f.slice(0, -4);
1617
+ const stat = await fs9.stat(path8.join(dir, f));
1618
+ return {
1619
+ slug,
1620
+ name: names[slug] ?? slug,
1621
+ updatedAt: stat.mtime.toISOString()
1622
+ };
1623
+ })
1624
+ );
1625
+ metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1626
+ return metas;
1627
+ }
1628
+ async function metaFor(slug) {
1629
+ const stat = await fs9.stat(paths.worksheet(slug));
1630
+ const name = await getName(slug) ?? slug;
1631
+ return { slug, name, updatedAt: stat.mtime.toISOString() };
1632
+ }
1633
+ async function readDraft(slug) {
1634
+ try {
1635
+ return await fs9.readFile(paths.draft(slug), "utf8");
1636
+ } catch {
1637
+ return null;
1638
+ }
1639
+ }
1640
+ app.get("/", async (c) => {
1641
+ return c.json({ worksheets: await listMetas() });
1642
+ });
1643
+ var SEARCH_MAX_HITS = 50;
1644
+ var SEARCH_SNIPPET_LEN = 120;
1645
+ app.get("/search", async (c) => {
1646
+ const q = (c.req.query("q") ?? "").trim();
1647
+ if (q === "") return c.json({ hits: [] });
1648
+ const needle = q.toLowerCase();
1649
+ const metas = await listMetas();
1650
+ const hits = [];
1651
+ for (const meta of metas) {
1652
+ if (hits.length >= SEARCH_MAX_HITS) break;
1653
+ const slugHit = meta.slug.toLowerCase().includes(needle);
1654
+ let content = "";
1655
+ try {
1656
+ content = await fs9.readFile(paths.worksheet(meta.slug), "utf8");
1657
+ } catch {
1658
+ }
1659
+ const lower = content.toLowerCase();
1660
+ const idx = lower.indexOf(needle);
1661
+ if (!slugHit && idx === -1) continue;
1662
+ let snippet = "";
1663
+ let lineNumber;
1664
+ if (idx !== -1) {
1665
+ const lineStart = lower.lastIndexOf("\n", idx) + 1;
1666
+ const lineEndRaw = lower.indexOf("\n", idx);
1667
+ const lineEnd = lineEndRaw === -1 ? content.length : lineEndRaw;
1668
+ const line = content.slice(lineStart, lineEnd);
1669
+ snippet = line.length > SEARCH_SNIPPET_LEN ? line.slice(0, SEARCH_SNIPPET_LEN) + "\u2026" : line;
1670
+ lineNumber = lower.slice(0, lineStart).split("\n").length;
1671
+ }
1672
+ hits.push({ slug: meta.slug, name: meta.name, snippet, lineNumber });
1673
+ }
1674
+ return c.json({ hits });
1675
+ });
1676
+ app.post("/", async (c) => {
1677
+ const body = await c.req.json().catch(() => ({}));
1678
+ const desired = slugify(body.name ?? "untitled");
1679
+ const existing = new Set((await listMetas()).map((m) => m.slug));
1680
+ const slug = dedupeSlug(desired, existing);
1681
+ recordHistory({ worksheet: slug, source: "save", content: "" });
1682
+ await writeAtomic(paths.worksheet(slug), "");
1683
+ return c.json(await metaFor(slug));
1684
+ });
1685
+ app.get("/:slug", async (c) => {
1686
+ const slug = c.req.param("slug");
1687
+ assertSafeSlug(slug);
1688
+ const file = paths.worksheet(slug);
1689
+ let content;
1690
+ try {
1691
+ content = await fs9.readFile(file, "utf8");
1692
+ } catch {
1693
+ return c.json({ error: "not_found" }, 404);
1694
+ }
1695
+ const stat = await fs9.stat(file);
1696
+ if (!hasEntries(slug)) {
1697
+ recordHistory({ worksheet: slug, source: "snapshot", content, ts: stat.mtimeMs });
1698
+ }
1699
+ const draftContent = await readDraft(slug);
1700
+ const meta = await metaFor(slug);
1701
+ const payload = { meta, content, draftContent };
1702
+ return c.json(payload);
1703
+ });
1704
+ app.put("/:slug", async (c) => {
1705
+ const slug = c.req.param("slug");
1706
+ assertSafeSlug(slug);
1707
+ const { content } = await c.req.json();
1708
+ const result = recordHistory({ worksheet: slug, source: "save", content });
1709
+ await writeAtomic(paths.worksheet(slug), content);
1710
+ const meta = await metaFor(slug);
1711
+ return c.json({ ...meta, historySkipped: result.skipped ?? null });
1712
+ });
1713
+ app.patch("/:slug", async (c) => {
1714
+ const slug = c.req.param("slug");
1715
+ assertSafeSlug(slug);
1716
+ const body = await c.req.json().catch(() => ({}));
1717
+ const raw = (body.name ?? "").trim();
1718
+ if (!raw) return c.json({ error: "empty_name" }, 400);
1719
+ const name = raw.length > MAX_NAME_LEN ? raw.slice(0, MAX_NAME_LEN) : raw;
1720
+ await setName(slug, name);
1721
+ return c.json(await metaFor(slug));
1722
+ });
1723
+ app.post("/:slug/auto-name", async (c) => {
1724
+ const slug = c.req.param("slug");
1725
+ assertSafeSlug(slug);
1726
+ const existing = await getName(slug);
1727
+ if (existing && existing !== slug) {
1728
+ return c.json({ name: existing, skipped: true, reason: "already-named" });
1729
+ }
1730
+ const body = await c.req.json().catch(() => ({}));
1731
+ let sql = (body.sql ?? "").trim();
1732
+ if (!sql) {
1733
+ try {
1734
+ sql = (await fs9.readFile(paths.draft(slug), "utf8")).trim();
1735
+ } catch {
1736
+ }
1737
+ }
1738
+ if (!sql) {
1739
+ try {
1740
+ sql = (await fs9.readFile(paths.worksheet(slug), "utf8")).trim();
1741
+ } catch {
1742
+ return c.json({ error: "not_found" }, 404);
1743
+ }
1744
+ }
1745
+ if (!sql) {
1746
+ return c.json({ name: slug, skipped: true, reason: "empty" });
1747
+ }
1748
+ try {
1749
+ const name = await generateWorksheetName(sql);
1750
+ await setName(slug, name);
1751
+ return c.json({ name, skipped: false });
1752
+ } catch (err) {
1753
+ const message = err.message;
1754
+ console.warn(`auto-name failed for ${slug}:`, message);
1755
+ return c.json({ name: slug, skipped: true, reason: "model-error", error: message });
1756
+ }
1757
+ });
1758
+ app.delete("/:slug", async (c) => {
1759
+ const slug = c.req.param("slug");
1760
+ assertSafeSlug(slug);
1761
+ await fs9.rm(paths.worksheet(slug), { force: true });
1762
+ await fs9.rm(paths.draft(slug), { force: true });
1763
+ deleteWorksheetHistory(slug);
1764
+ await deleteName(slug);
1765
+ return c.json({ ok: true });
1766
+ });
1767
+ var worksheets_default = app;
1768
+
1769
+ // server/api/drafts.ts
1770
+ import { Hono as Hono4 } from "hono";
1771
+ import { promises as fs10 } from "node:fs";
1772
+ var app2 = new Hono4();
1773
+ app2.put("/:slug", async (c) => {
1774
+ const slug = c.req.param("slug");
1775
+ assertSafeSlug(slug);
1776
+ const { content } = await c.req.json();
1777
+ const result = recordHistory({ worksheet: slug, source: "autosave", content });
1778
+ await writeAtomic(paths.draft(slug), content);
1779
+ return c.json({ ok: true, historySkipped: result.skipped ?? null });
1780
+ });
1781
+ app2.delete("/:slug", async (c) => {
1782
+ const slug = c.req.param("slug");
1783
+ assertSafeSlug(slug);
1784
+ await fs10.rm(paths.draft(slug), { force: true });
1785
+ return c.json({ ok: true });
1786
+ });
1787
+ var drafts_default = app2;
1788
+
1789
+ // server/api/session.ts
1790
+ import { Hono as Hono5 } from "hono";
1791
+ import { promises as fs11 } from "node:fs";
1792
+ var app3 = new Hono5();
1793
+ var EMPTY = { openTabs: [], activeSlug: null, resultsPaneSize: null };
1794
+ app3.get("/", async (c) => {
1795
+ try {
1796
+ const raw = await fs11.readFile(paths.session(), "utf8");
1797
+ return c.json(JSON.parse(raw));
1798
+ } catch {
1799
+ return c.json(EMPTY);
1800
+ }
1801
+ });
1802
+ app3.put("/", async (c) => {
1803
+ const body = await c.req.json();
1804
+ await writeAtomic(paths.session(), JSON.stringify(body, null, 2));
1805
+ return c.json({ ok: true });
1806
+ });
1807
+ var session_default = app3;
1808
+
1809
+ // server/api/schema.ts
1810
+ import { Hono as Hono6 } from "hono";
1811
+ import { promises as fs12 } from "node:fs";
1812
+ var app4 = new Hono6();
1813
+ var EMPTY2 = {};
1814
+ app4.get("/", async (c) => {
1815
+ try {
1816
+ const raw = await fs12.readFile(paths.schema(), "utf8");
1817
+ return c.json(JSON.parse(raw));
1818
+ } catch {
1819
+ return c.json(EMPTY2);
1820
+ }
1821
+ });
1822
+ var schema_default = app4;
1823
+
1824
+ // server/api/history.ts
1825
+ import { Hono as Hono7 } from "hono";
1826
+ import { promises as fs13 } from "node:fs";
1827
+
1828
+ // server/history/git.ts
1829
+ import { execFile as execFile2 } from "node:child_process";
1830
+ import { promisify as promisify2 } from "node:util";
1831
+ import path9 from "node:path";
1832
+ var exec2 = promisify2(execFile2);
1833
+ var MAX_BUFFER = 4 * 1024 * 1024;
1834
+ var TIMEOUT_MS = 5e3;
1835
+ function worksheetRelPath(slug) {
1836
+ return path9.posix.join("worksheets", `${slug}.sql`);
1837
+ }
1838
+ var HEADER = "__osdpt_commit__\0";
1839
+ var HEADER_LINE = /^__osdpt_commit__\0[0-9a-f]{40}\0/;
1840
+ async function listGitCommits(slug) {
1841
+ const cwd = workspaceRoot();
1842
+ try {
1843
+ const { stdout } = await exec2(
1844
+ "git",
1845
+ [
1846
+ "log",
1847
+ // --follow walks through renames so a worksheet's pre-rename history
1848
+ // still shows up in the timeline.
1849
+ "--follow",
1850
+ // --raw lines (`:mode mode srcSha dstSha STATUS\tpath`) give us the
1851
+ // post-change blob sha per commit, so the client can drop entries
1852
+ // identical to the current editor buffer.
1853
+ "--raw",
1854
+ "--no-abbrev",
1855
+ `--format=__osdpt_commit__%x00%H%x00%ct%x00%an%x00%s`,
1856
+ "--",
1857
+ worksheetRelPath(slug)
1858
+ ],
1859
+ { cwd, maxBuffer: MAX_BUFFER, timeout: TIMEOUT_MS }
1860
+ );
1861
+ const items = [];
1862
+ let pending = null;
1863
+ const flush = () => {
1864
+ if (pending) items.push(pending);
1865
+ pending = null;
1866
+ };
1867
+ for (const line of stdout.split("\n")) {
1868
+ if (HEADER_LINE.test(line)) {
1869
+ flush();
1870
+ const [sha, ct, an, subject] = line.slice(HEADER.length).split("\0");
1871
+ pending = {
1872
+ kind: "git",
1873
+ sha,
1874
+ ts: Number(ct) * 1e3,
1875
+ author: an || null,
1876
+ subject: subject || "",
1877
+ contentSha: null
1878
+ };
1879
+ } else if (line.startsWith(":") && pending && pending.contentSha === null) {
1880
+ const prefix = line.split(" ", 1)[0];
1881
+ const dst = prefix.split(" ")[3];
1882
+ if (dst && /^[0-9a-f]{40}$/.test(dst)) pending.contentSha = dst;
1883
+ }
1884
+ }
1885
+ flush();
1886
+ return items;
1887
+ } catch {
1888
+ return [];
1889
+ }
1890
+ }
1891
+ async function readFileAtCommit(sha, slug) {
1892
+ const cwd = workspaceRoot();
1893
+ try {
1894
+ const { stdout } = await exec2("git", ["show", `${sha}:${worksheetRelPath(slug)}`], {
1895
+ cwd,
1896
+ maxBuffer: MAX_BUFFER,
1897
+ timeout: TIMEOUT_MS
1898
+ });
1899
+ return stdout;
1900
+ } catch {
1901
+ return null;
1902
+ }
1903
+ }
1904
+
1905
+ // server/history/timeline.ts
1906
+ async function buildTimeline(slug) {
1907
+ const entries = listEntries(slug);
1908
+ const commits = await listGitCommits(slug);
1909
+ const items = [
1910
+ ...entries.map((entry) => ({ kind: "history", entry })),
1911
+ ...commits
1912
+ ];
1913
+ items.sort((a, b) => {
1914
+ const at = a.kind === "history" ? a.entry.ts : a.ts;
1915
+ const bt2 = b.kind === "history" ? b.entry.ts : b.ts;
1916
+ return bt2 - at;
1917
+ });
1918
+ return items;
1919
+ }
1920
+
1921
+ // server/api/history.ts
1922
+ var app5 = new Hono7();
1923
+ app5.get("/:slug", (c) => {
1924
+ const slug = c.req.param("slug");
1925
+ assertSafeSlug(slug);
1926
+ return c.json({ entries: listEntries(slug) });
1927
+ });
1928
+ app5.get("/:slug/timeline", async (c) => {
1929
+ const slug = c.req.param("slug");
1930
+ assertSafeSlug(slug);
1931
+ return c.json({ items: await buildTimeline(slug) });
1932
+ });
1933
+ app5.get("/:slug/entry/:id", (c) => {
1934
+ const slug = c.req.param("slug");
1935
+ assertSafeSlug(slug);
1936
+ const id = Number(c.req.param("id"));
1937
+ if (!Number.isInteger(id) || id <= 0) return c.json({ error: "bad_id" }, 400);
1938
+ const entry = getEntry(slug, id);
1939
+ if (!entry) return c.json({ error: "not_found" }, 404);
1940
+ return c.json({ entry });
1941
+ });
1942
+ app5.get("/:slug/git/:sha", async (c) => {
1943
+ const slug = c.req.param("slug");
1944
+ assertSafeSlug(slug);
1945
+ const sha = c.req.param("sha");
1946
+ if (!/^[a-f0-9]{7,40}$/.test(sha)) return c.json({ error: "bad_sha" }, 400);
1947
+ const content = await readFileAtCommit(sha, slug);
1948
+ if (content === null) return c.json({ error: "not_found" }, 404);
1949
+ return c.json({ sha, content });
1950
+ });
1951
+ app5.post("/:slug/revert/:id", async (c) => {
1952
+ const slug = c.req.param("slug");
1953
+ assertSafeSlug(slug);
1954
+ const id = Number(c.req.param("id"));
1955
+ if (!Number.isInteger(id) || id <= 0) return c.json({ error: "bad_id" }, 400);
1956
+ const entry = await revertToEntry(slug, id);
1957
+ if (!entry) return c.json({ error: "not_found" }, 404);
1958
+ const stat = await fs13.stat(paths.worksheet(slug));
1959
+ const meta = { slug, name: slug, updatedAt: stat.mtime.toISOString() };
1960
+ return c.json({ meta, content: entry.content });
1961
+ });
1962
+ var history_default = app5;
1963
+
1964
+ // server/api/agent.ts
1965
+ import { Hono as Hono8 } from "hono";
1966
+ import { streamSSE } from "hono/streaming";
1967
+
1968
+ // shared/agent.ts
1969
+ function emptyTotals() {
1970
+ return {
1971
+ inputTokens: 0,
1972
+ outputTokens: 0,
1973
+ cacheCreationTokens: 0,
1974
+ cacheReadTokens: 0,
1975
+ costUsd: 0,
1976
+ calls: 0
1977
+ };
1978
+ }
1979
+
1980
+ // server/agent/pricing.ts
1981
+ var PRICING = {
1982
+ "claude-opus-4-7": {
1983
+ inputPer1M: 15,
1984
+ outputPer1M: 75,
1985
+ cacheWritePer1M: 18.75,
1986
+ cacheReadPer1M: 1.5
1987
+ },
1988
+ "claude-sonnet-4-6": {
1989
+ inputPer1M: 3,
1990
+ outputPer1M: 15,
1991
+ cacheWritePer1M: 3.75,
1992
+ cacheReadPer1M: 0.3
1993
+ },
1994
+ "claude-haiku-4-5": {
1995
+ inputPer1M: 1,
1996
+ outputPer1M: 5,
1997
+ cacheWritePer1M: 1.25,
1998
+ cacheReadPer1M: 0.1
1999
+ }
2000
+ };
2001
+ var FALLBACK = PRICING["claude-sonnet-4-6"];
2002
+ function ratesFor(model) {
2003
+ const direct = PRICING[model];
2004
+ if (direct) return direct;
2005
+ for (const key of Object.keys(PRICING)) {
2006
+ if (model.startsWith(key)) return PRICING[key];
2007
+ }
2008
+ return FALLBACK;
2009
+ }
2010
+ function costFromUsage(model, usage) {
2011
+ const r = ratesFor(model);
2012
+ const input = usage.input_tokens ?? 0;
2013
+ const output = usage.output_tokens ?? 0;
2014
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
2015
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
2016
+ return (input * r.inputPer1M + output * r.outputPer1M + cacheWrite * r.cacheWritePer1M + cacheRead * r.cacheReadPer1M) / 1e6;
2017
+ }
2018
+
2019
+ // server/agent/prompt.ts
2020
+ function buildSystemPrompt(session) {
2021
+ const { worksheetSlug, connectionId } = session.meta;
2022
+ const bound = !!worksheetSlug;
2023
+ const lines = [
2024
+ "You are os-dpt's chat-to-SQL agent. You help the user explore and query their database from a local SQL editor.",
2025
+ "",
2026
+ "Operating principles:",
2027
+ "- Be conservative with assumptions. If you do not know a table, column, or business term, call get_schema or get_context before guessing.",
2028
+ "- If you are still ambiguous after those, call ask_user_question instead of guessing.",
2029
+ '- Persist durable knowledge via update_context the moment you learn it, in the same turn \u2014 do NOT wait for the user to ask, and do NOT wait for an explicit "remember this" / "update our context". When the user surfaces a fact or rule in the course of normal analysis, that surfacing IS the trigger; saving is your job, not theirs. Concrete triggers, each one \u2192 save:',
2030
+ ` - The user tells you how to treat the data going forward \u2014 a standing rule: an exclusion, filter, default scope, or an "always/never do X" (e.g. 'strip out our internal test traffic', 'revenue means net of refunds', 'only count paid accounts') \u2192 conventions.md. These must be applied by default in every future session, so file them as conventions \u2014 NOT as a passing schemas.md note.`,
2031
+ " - You enumerate or clarify a table/column's meaning or its set of values \u2192 schemas.md",
2032
+ " - The user corrects you, or a run_sql result contradicts an assumption \u2192 feedback.md",
2033
+ " - A run_sql error reveals a schema or convention fact \u2192 feedback.md",
2034
+ " - You learn how the team writes SQL or what a business term means \u2192 conventions.md",
2035
+ " - You discover a data-quality quirk (NULLs, sentinel values like a literal 'Unknown', year/period handling, per-source anomalies) \u2192 schemas.md or feedback.md. If the takeaway is a recurring filter the user will always want applied (e.g. excluding internal/test traffic), ALSO record it as a standing rule in conventions.md, not only as a schema note.",
2036
+ " Rule of thumb: descriptive facts (what the data IS) \u2192 schemas.md/feedback.md; prescriptive rules (how to always query or treat it) \u2192 conventions.md. A recurring exclusion is a convention.",
2037
+ " After you save, tell the user in one short line what you persisted and where, so they know it will carry forward. Saving is cheap and git-tracked, so the user can always review or revert; prefer over-saving to losing the finding."
2038
+ ];
2039
+ if (bound) {
2040
+ lines.push(
2041
+ "- Prefer iterating: write_sql \u2192 run_sql \u2192 inspect \u2192 write_sql again. Drafts are non-destructive; the user commits with Cmd+S."
2042
+ );
2043
+ } else {
2044
+ lines.push(
2045
+ "- This chat is a standalone explore-and-visualize surface with no worksheet attached. You cannot stage SQL into an editor (write_sql is unavailable). Run read-only SELECTs with run_sql and present findings with render_chart and concise prose. If the user wants to keep a query, tell them to open a worksheet."
2046
+ );
2047
+ }
2048
+ lines.push(
2049
+ "- Keep prose terse. The user sees your tool calls in the UI; do not narrate every step.",
2050
+ "",
2051
+ "Tool usage notes:"
2052
+ );
2053
+ if (bound) {
2054
+ lines.push(
2055
+ "- write_sql stages the full worksheet contents into a draft (overwrite, not patch). Always include the complete query."
2056
+ );
2057
+ }
2058
+ lines.push(
2059
+ "- run_sql results are capped to 50 rows by default; add LIMIT or aggregation if you need a broader view.",
2060
+ "- run_sql executes whatever SQL you pass, including DDL and DML, against the role the user connected with. Treat exploration as read-only \u2014 use SELECT. Before any INSERT/UPDATE/DELETE/TRUNCATE/CREATE/DROP/ALTER, call ask_user_question to confirm.",
2061
+ "- ask_user_question pauses the loop entirely \u2014 only one question per call, and use it sparingly.",
2062
+ "- render_chart draws a chart inline in the chat. After run_sql, when a picture beats a table (trends \u2192 line/area, comparisons \u2192 bar, proportions \u2192 pie), call it with pre-aggregated rows. Pass the data inline, shaped to small row objects (e.g. {month, revenue}); don't dump raw wide rows."
2063
+ );
2064
+ const ctx = [];
2065
+ if (worksheetSlug) ctx.push(`- Active worksheet: ${worksheetSlug}`);
2066
+ if (connectionId) ctx.push(`- Active connection: ${connectionId}`);
2067
+ if (ctx.length > 0) {
2068
+ lines.push("", "Current chat bindings:", ...ctx);
2069
+ } else {
2070
+ lines.push(
2071
+ "",
2072
+ "No worksheet or connection is bound to this chat. Ask the user before running SQL."
2073
+ );
2074
+ }
2075
+ return lines.join("\n");
2076
+ }
2077
+
2078
+ // server/agent/session.ts
2079
+ import { promises as fs14 } from "node:fs";
2080
+ import crypto4 from "node:crypto";
2081
+ function nowIso() {
2082
+ return (/* @__PURE__ */ new Date()).toISOString();
2083
+ }
2084
+ function freshMeta(input) {
2085
+ const now = nowIso();
2086
+ return {
2087
+ id: crypto4.randomUUID(),
2088
+ createdAt: now,
2089
+ updatedAt: now,
2090
+ title: input.title ?? null,
2091
+ titleGenerated: false,
2092
+ worksheetSlug: input.worksheetSlug ?? null,
2093
+ standalone: input.standalone ?? false,
2094
+ connectionId: input.connectionId ?? null,
2095
+ pending: null,
2096
+ usage: [],
2097
+ totals: emptyTotals(),
2098
+ traceParent: null
2099
+ };
2100
+ }
2101
+ function ensureUsageFields(session) {
2102
+ if (!Array.isArray(session.meta.usage)) session.meta.usage = [];
2103
+ if (!session.meta.totals) session.meta.totals = emptyTotals();
2104
+ if (typeof session.meta.standalone !== "boolean") session.meta.standalone = false;
2105
+ if (typeof session.meta.titleGenerated !== "boolean") {
2106
+ session.meta.titleGenerated = session.meta.title != null;
2107
+ }
2108
+ return session;
2109
+ }
2110
+ async function createChat(input) {
2111
+ const session = { meta: freshMeta(input), messages: [] };
2112
+ await persist(session);
2113
+ return session;
2114
+ }
2115
+ async function getChat(id) {
2116
+ assertSafeChatId(id);
2117
+ try {
2118
+ const raw = await fs14.readFile(paths.chat(id), "utf8");
2119
+ return ensureUsageFields(JSON.parse(raw));
2120
+ } catch (err) {
2121
+ if (err.code === "ENOENT") return null;
2122
+ throw err;
2123
+ }
2124
+ }
2125
+ async function listChats() {
2126
+ const entries = await fs14.readdir(paths.chats()).catch(() => []);
2127
+ const out = [];
2128
+ for (const f of entries) {
2129
+ if (!f.endsWith(".json")) continue;
2130
+ try {
2131
+ const raw = await fs14.readFile(paths.chat(f.slice(0, -5)), "utf8");
2132
+ const data = ensureUsageFields(JSON.parse(raw));
2133
+ out.push(data.meta);
2134
+ } catch {
2135
+ }
2136
+ }
2137
+ out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2138
+ return out;
2139
+ }
2140
+ async function deleteChat(id) {
2141
+ assertSafeChatId(id);
2142
+ await fs14.rm(paths.chat(id), { force: true });
2143
+ }
2144
+ async function appendMessage(session, message) {
2145
+ session.messages.push(message);
2146
+ session.meta.updatedAt = nowIso();
2147
+ await persist(session);
2148
+ }
2149
+ async function setPending(session, pending) {
2150
+ session.meta.pending = pending;
2151
+ session.meta.updatedAt = nowIso();
2152
+ await persist(session);
2153
+ }
2154
+ async function clearPending(session) {
2155
+ session.meta.pending = null;
2156
+ session.meta.updatedAt = nowIso();
2157
+ await persist(session);
2158
+ }
2159
+ async function recordUsage(session, entry) {
2160
+ session.meta.usage.push(entry);
2161
+ const t = session.meta.totals;
2162
+ t.inputTokens += entry.inputTokens;
2163
+ t.outputTokens += entry.outputTokens;
2164
+ t.cacheCreationTokens += entry.cacheCreationTokens;
2165
+ t.cacheReadTokens += entry.cacheReadTokens;
2166
+ t.costUsd += entry.costUsd;
2167
+ t.calls += 1;
2168
+ session.meta.updatedAt = nowIso();
2169
+ await persist(session);
2170
+ }
2171
+ async function setTitle(session, title, generated = false) {
2172
+ session.meta.title = title;
2173
+ if (generated) session.meta.titleGenerated = true;
2174
+ session.meta.updatedAt = nowIso();
2175
+ await persist(session);
2176
+ }
2177
+ async function setConnection(session, connectionId) {
2178
+ session.meta.connectionId = connectionId;
2179
+ session.meta.updatedAt = nowIso();
2180
+ await persist(session);
2181
+ }
2182
+ async function persistSession(session) {
2183
+ session.meta.updatedAt = nowIso();
2184
+ await persist(session);
2185
+ }
2186
+ async function persist(session) {
2187
+ assertSafeChatId(session.meta.id);
2188
+ await writeAtomic(paths.chat(session.meta.id), JSON.stringify(session, null, 2));
2189
+ }
2190
+
2191
+ // server/agent/tools/get_context.ts
2192
+ import { promises as fs15 } from "node:fs";
2193
+ async function readOne(file, connectionId) {
2194
+ try {
2195
+ return await fs15.readFile(paths.contextFile(file, connectionId), "utf8");
2196
+ } catch (err) {
2197
+ if (err.code === "ENOENT") return null;
2198
+ throw err;
2199
+ }
2200
+ }
2201
+ var getContextTool = {
2202
+ name: "get_context",
2203
+ description: "Read the agent's persisted context (markdown files in the workspace's context/ directory). These contain prior learnings about schemas, project conventions, and user feedback. Context is scoped to the data source bound to this chat; if no connection is bound, the workspace-level (unassigned) docs are read instead. Call this near the start of a session and any time you might be missing context for a request.",
2204
+ input_schema: {
2205
+ type: "object",
2206
+ properties: {
2207
+ files: {
2208
+ type: "array",
2209
+ description: "Subset of context files to read. Defaults to all files when omitted.",
2210
+ items: { type: "string", enum: [...CONTEXT_FILES] }
2211
+ }
2212
+ }
2213
+ },
2214
+ async execute(rawInput, ctx) {
2215
+ const input = rawInput ?? {};
2216
+ const bound = ctx.session.meta.connectionId;
2217
+ const connectionId = bound && isSafeConnectionId(bound) ? bound : null;
2218
+ const requested = input.files && input.files.length > 0 ? input.files : [...CONTEXT_FILES];
2219
+ const sections = [];
2220
+ let present = 0;
2221
+ for (const f of requested) {
2222
+ const body = await readOne(f, connectionId);
2223
+ if (body === null) {
2224
+ sections.push(`## ${f}.md
2225
+ (not yet written)`);
2226
+ } else {
2227
+ present += 1;
2228
+ sections.push(`## ${f}.md
2229
+ ${body.trim()}`);
2230
+ }
2231
+ }
2232
+ return {
2233
+ toolResult: sections.join("\n\n"),
2234
+ isError: false,
2235
+ uiSummary: `Read ${present}/${requested.length} context files`
2236
+ };
2237
+ }
2238
+ };
2239
+
2240
+ // server/agent/tools/update_context.ts
2241
+ import { promises as fs16 } from "node:fs";
2242
+ function isContextFile(v) {
2243
+ return CONTEXT_FILES.includes(v);
2244
+ }
2245
+ var updateContextTool = {
2246
+ name: "update_context",
2247
+ description: "Write to one of the agent's context files (schemas.md, conventions.md, feedback.md). Call this the moment you learn something durable \u2014 without waiting to be asked: a clarified schema fact or value set, a project convention, a user correction, a data-quality quirk, or an error pattern from running SQL. Saving is cheap and git-tracked; prefer over-saving to losing a finding. Prefer 'append' for new findings; use 'replace' only when restructuring an entire file. Writes are scoped to the data source bound to this chat; with no connection bound they go to the workspace-level (unassigned) docs.",
2248
+ input_schema: {
2249
+ type: "object",
2250
+ required: ["file", "mode", "content"],
2251
+ properties: {
2252
+ file: {
2253
+ type: "string",
2254
+ enum: [...CONTEXT_FILES],
2255
+ description: "schemas.md = table/column facts; conventions.md = how the team writes SQL / what business terms mean; feedback.md = corrections and run_sql errors and what was learned."
2256
+ },
2257
+ mode: {
2258
+ type: "string",
2259
+ enum: ["append", "replace"]
2260
+ },
2261
+ content: {
2262
+ type: "string",
2263
+ description: "Markdown to append (added under a dated heading) or replace the whole file with."
2264
+ }
2265
+ }
2266
+ },
2267
+ async execute(rawInput, ctx) {
2268
+ const input = rawInput ?? {};
2269
+ const bound = ctx.session.meta.connectionId;
2270
+ const connectionId = bound && isSafeConnectionId(bound) ? bound : null;
2271
+ if (typeof input.file !== "string" || !isContextFile(input.file)) {
2272
+ return {
2273
+ toolResult: `Invalid file: must be one of ${CONTEXT_FILES.join(", ")}`,
2274
+ isError: true,
2275
+ uiSummary: "update_context: invalid file"
2276
+ };
2277
+ }
2278
+ if (input.mode !== "append" && input.mode !== "replace") {
2279
+ return {
2280
+ toolResult: "Invalid mode: must be 'append' or 'replace'",
2281
+ isError: true,
2282
+ uiSummary: "update_context: invalid mode"
2283
+ };
2284
+ }
2285
+ if (typeof input.content !== "string" || input.content.trim() === "") {
2286
+ return {
2287
+ toolResult: "Invalid content: must be non-empty string",
2288
+ isError: true,
2289
+ uiSummary: "update_context: empty content"
2290
+ };
2291
+ }
2292
+ const target = paths.contextFile(input.file, connectionId);
2293
+ let next;
2294
+ if (input.mode === "replace") {
2295
+ next = input.content.endsWith("\n") ? input.content : input.content + "\n";
2296
+ } else {
2297
+ let current = "";
2298
+ try {
2299
+ current = await fs16.readFile(target, "utf8");
2300
+ } catch (err) {
2301
+ if (err.code !== "ENOENT") throw err;
2302
+ }
2303
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2304
+ const block = `## ${stamp}
2305
+
2306
+ ${input.content.trim()}
2307
+ `;
2308
+ next = current === "" ? block : current.replace(/\n*$/, "\n\n") + block;
2309
+ }
2310
+ await writeAtomic(target, next);
2311
+ return {
2312
+ toolResult: `Wrote ${next.length} bytes to context/${input.file}.md (${input.mode})`,
2313
+ isError: false,
2314
+ uiSummary: `Updated context/${input.file}.md (${input.mode})`
2315
+ };
2316
+ }
2317
+ };
2318
+
2319
+ // server/agent/tools/get_schema.ts
2320
+ var cache = /* @__PURE__ */ new Map();
2321
+ var TTL_MS2 = 6e4;
2322
+ function cacheKey(connId, schemas) {
2323
+ return `${connId}::${schemas.slice().sort().join(",")}`;
2324
+ }
2325
+ async function introspect2(connId, schemas) {
2326
+ const pool = getPool(connId);
2327
+ if (!pool) throw new Error(`Connection not active: ${connId}`);
2328
+ const sql = `
2329
+ SELECT table_schema, table_name, column_name, data_type, is_nullable
2330
+ FROM information_schema.columns
2331
+ WHERE table_schema = ANY($1)
2332
+ ORDER BY table_schema, table_name, ordinal_position
2333
+ `;
2334
+ const { rows } = await pool.query(sql, [schemas]);
2335
+ const tree = {};
2336
+ for (const r of rows) {
2337
+ const s = tree[r.table_schema] ??= {};
2338
+ const t = s[r.table_name] ??= { columns: [] };
2339
+ t.columns.push({
2340
+ name: r.column_name,
2341
+ type: r.data_type,
2342
+ nullable: r.is_nullable === "YES"
2343
+ });
2344
+ }
2345
+ return tree;
2346
+ }
2347
+ function summarize(tree, includeCols) {
2348
+ const out = [];
2349
+ for (const schema of Object.keys(tree).sort()) {
2350
+ out.push(`# ${schema}`);
2351
+ for (const table of Object.keys(tree[schema]).sort()) {
2352
+ const cols = tree[schema][table].columns;
2353
+ if (includeCols) {
2354
+ out.push(
2355
+ `- ${table}(${cols.map((c) => `${c.name} ${c.type}${c.nullable ? "" : " NOT NULL"}`).join(", ")})`
2356
+ );
2357
+ } else {
2358
+ out.push(`- ${table} [${cols.length} cols]`);
2359
+ }
2360
+ }
2361
+ }
2362
+ return out.join("\n");
2363
+ }
2364
+ var getSchemaTool = {
2365
+ name: "get_schema",
2366
+ description: "Introspect the live database schema via information_schema. Use this when you need to know what tables and columns exist before writing SQL. Results are cached per session for a minute. Defaults to the active connection bound to this chat and the 'public' schema.",
2367
+ input_schema: {
2368
+ type: "object",
2369
+ properties: {
2370
+ connection_id: {
2371
+ type: "string",
2372
+ description: "Connection UUID to introspect. Defaults to the chat's bound connection."
2373
+ },
2374
+ schemas: {
2375
+ type: "array",
2376
+ items: { type: "string" },
2377
+ description: "Postgres schemas to include. Defaults to ['public']."
2378
+ },
2379
+ include_columns: {
2380
+ type: "boolean",
2381
+ description: "If true, return column names + types. If false, return only table names with column counts. Defaults to true."
2382
+ }
2383
+ }
2384
+ },
2385
+ async execute(rawInput, ctx) {
2386
+ const input = rawInput ?? {};
2387
+ const connId = input.connection_id ?? ctx.session.meta.connectionId;
2388
+ if (!connId) {
2389
+ return {
2390
+ toolResult: "No connection bound. Ask the user which connection to use, or set connection_id explicitly.",
2391
+ isError: true,
2392
+ uiSummary: "get_schema: no connection"
2393
+ };
2394
+ }
2395
+ const schemas = input.schemas && input.schemas.length > 0 ? input.schemas : ["public"];
2396
+ const includeCols = input.include_columns ?? true;
2397
+ const key = cacheKey(connId, schemas);
2398
+ let entry = cache.get(key);
2399
+ if (!entry || Date.now() - entry.at > TTL_MS2) {
2400
+ try {
2401
+ entry = { at: Date.now(), tree: await introspect2(connId, schemas) };
2402
+ cache.set(key, entry);
2403
+ } catch (err) {
2404
+ return {
2405
+ toolResult: `Schema introspection failed: ${err.message}`,
2406
+ isError: true,
2407
+ uiSummary: "get_schema: failed"
2408
+ };
2409
+ }
2410
+ }
2411
+ const tableCount = Object.values(entry.tree).reduce(
2412
+ (n, s) => n + Object.keys(s).length,
2413
+ 0
2414
+ );
2415
+ return {
2416
+ toolResult: summarize(entry.tree, includeCols),
2417
+ isError: false,
2418
+ uiSummary: `Loaded ${tableCount} tables from ${schemas.join(", ")}`
2419
+ };
2420
+ }
2421
+ };
2422
+
2423
+ // server/agent/tools/ask_user_question.ts
2424
+ var askUserQuestionTool = {
2425
+ name: "ask_user_question",
2426
+ description: "Ask the user a clarifying question when you lack context to proceed safely. Calling this PAUSES the agent \u2014 no further tools run this turn and the user is prompted to reply. Use it for genuine ambiguity (which table did they mean, what time window, etc.), not for confirmation of obvious next steps.",
2427
+ input_schema: {
2428
+ type: "object",
2429
+ required: ["question"],
2430
+ properties: {
2431
+ question: {
2432
+ type: "string",
2433
+ description: "One specific question, phrased plainly."
2434
+ }
2435
+ }
2436
+ },
2437
+ async execute(rawInput) {
2438
+ const input = rawInput ?? {};
2439
+ const question = typeof input.question === "string" ? input.question.trim() : "";
2440
+ if (question === "") {
2441
+ return {
2442
+ toolResult: "Invalid question: must be non-empty string",
2443
+ isError: true,
2444
+ uiSummary: "ask_user_question: empty"
2445
+ };
2446
+ }
2447
+ return {
2448
+ toolResult: "(awaiting user response)",
2449
+ isError: false,
2450
+ uiSummary: question,
2451
+ pause: { question }
2452
+ };
2453
+ }
2454
+ };
2455
+
2456
+ // server/agent/tools/write_sql.ts
2457
+ var writeSqlTool = {
2458
+ name: "write_sql",
2459
+ description: "Stage SQL into the editor. Writes to the worksheet's draft (autosave channel), so the user sees the SQL appear in the editor without losing their saved version until they hit Cmd+S. Defaults to the chat's bound worksheet. Pass the COMPLETE worksheet contents \u2014 drafts are overwrites, not patches.",
2460
+ input_schema: {
2461
+ type: "object",
2462
+ required: ["sql"],
2463
+ properties: {
2464
+ sql: {
2465
+ type: "string",
2466
+ description: "Full SQL text to stage into the worksheet draft."
2467
+ },
2468
+ worksheet_slug: {
2469
+ type: "string",
2470
+ description: "Worksheet to write to. Defaults to the chat's bound worksheet."
2471
+ }
2472
+ }
2473
+ },
2474
+ async execute(rawInput, ctx) {
2475
+ const input = rawInput ?? {};
2476
+ if (typeof input.sql !== "string") {
2477
+ return {
2478
+ toolResult: "Invalid sql: must be a string",
2479
+ isError: true,
2480
+ uiSummary: "write_sql: invalid sql"
2481
+ };
2482
+ }
2483
+ const slug = input.worksheet_slug ?? ctx.session.meta.worksheetSlug;
2484
+ if (!slug) {
2485
+ return {
2486
+ toolResult: "No worksheet bound. Ask the user which worksheet to write to, or set worksheet_slug.",
2487
+ isError: true,
2488
+ uiSummary: "write_sql: no worksheet"
2489
+ };
2490
+ }
2491
+ try {
2492
+ assertSafeSlug(slug);
2493
+ } catch (err) {
2494
+ return {
2495
+ toolResult: `Invalid worksheet slug: ${err.message}`,
2496
+ isError: true,
2497
+ uiSummary: "write_sql: bad slug"
2498
+ };
2499
+ }
2500
+ await writeAtomic(paths.draft(slug), input.sql);
2501
+ return {
2502
+ toolResult: `Wrote ${input.sql.length} bytes to draft for worksheet '${slug}'.`,
2503
+ isError: false,
2504
+ uiSummary: `Staged ${input.sql.length} chars into '${slug}' draft`,
2505
+ events: [{ type: "sql_written", worksheetSlug: slug, sql: input.sql }]
2506
+ };
2507
+ }
2508
+ };
2509
+
2510
+ // server/db/sql-safety.ts
2511
+ var READ_ONLY_LEADERS = /* @__PURE__ */ new Set([
2512
+ "select",
2513
+ "with",
2514
+ "table",
2515
+ "values",
2516
+ "show",
2517
+ "explain"
2518
+ ]);
2519
+ function stripComments(sql) {
2520
+ return sql.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n]*/g, " ");
2521
+ }
2522
+ function isReadOnlyStatement(sql) {
2523
+ const oneStatement = stripComments(sql).trim().replace(/;\s*$/, "");
2524
+ if (oneStatement === "") return false;
2525
+ if (oneStatement.includes(";")) return false;
2526
+ const leader = oneStatement.toLowerCase().match(/^[a-z]+/)?.[0];
2527
+ if (!leader || !READ_ONLY_LEADERS.has(leader)) return false;
2528
+ if (leader === "explain" && /\banalyze\b/i.test(oneStatement)) return false;
2529
+ return true;
2530
+ }
2531
+
2532
+ // server/agent/tools/run_sql.ts
2533
+ var DEFAULT_ROW_LIMIT = 50;
2534
+ var MAX_ROW_LIMIT = 500;
2535
+ var MAX_RESULT_CHARS = 16e3;
2536
+ function format(rows, columns, rowCount) {
2537
+ const header = `columns: ${columns.join(", ")}
2538
+ rowCount: ${rowCount ?? rows.length}
2539
+ `;
2540
+ const body = rows.map((r) => JSON.stringify(r)).join("\n");
2541
+ let out = header + (body ? `rows:
2542
+ ${body}` : "(no rows)");
2543
+ if (out.length > MAX_RESULT_CHARS) {
2544
+ out = out.slice(0, MAX_RESULT_CHARS) + "\n\u2026(truncated)";
2545
+ }
2546
+ return out;
2547
+ }
2548
+ var runSqlTool = {
2549
+ name: "run_sql",
2550
+ description: "Execute SQL against the active database connection and return the result. Use this to validate queries you've written and to inspect data. If the query errors, capture what you learned by calling update_context against feedback.md so future runs avoid the same mistake. Row results are capped (default 50, max 500) \u2014 add LIMIT or aggregation if you need a wider view. IMPORTANT: this tool executes the SQL verbatim against whatever role the connection uses. Connections default to read-only, which rejects anything but a single read statement (SELECT/WITH/SHOW/EXPLAIN/TABLE/VALUES). On a read-write connection it will also run DDL (CREATE/DROP/ALTER) and DML (INSERT/UPDATE/DELETE/TRUNCATE), so before running anything that mutates data or schema, call ask_user_question to confirm.",
2551
+ input_schema: {
2552
+ type: "object",
2553
+ required: ["sql"],
2554
+ properties: {
2555
+ sql: { type: "string", description: "SQL to execute." },
2556
+ connection_id: {
2557
+ type: "string",
2558
+ description: "Connection UUID. Defaults to the chat's bound connection."
2559
+ },
2560
+ row_limit: {
2561
+ type: "number",
2562
+ description: `Cap on rows returned to the agent (default ${DEFAULT_ROW_LIMIT}, max ${MAX_ROW_LIMIT}).`
2563
+ }
2564
+ }
2565
+ },
2566
+ async execute(rawInput, ctx) {
2567
+ const input = rawInput ?? {};
2568
+ if (typeof input.sql !== "string" || input.sql.trim() === "") {
2569
+ return {
2570
+ toolResult: "Invalid sql: must be a non-empty string",
2571
+ isError: true,
2572
+ uiSummary: "run_sql: empty sql"
2573
+ };
2574
+ }
2575
+ const connId = input.connection_id ?? ctx.session.meta.connectionId;
2576
+ if (!connId) {
2577
+ return {
2578
+ toolResult: "No connection bound. Ask the user which connection to use, or set connection_id.",
2579
+ isError: true,
2580
+ uiSummary: "run_sql: no connection"
2581
+ };
2582
+ }
2583
+ const pool = getPool(connId);
2584
+ if (!pool) {
2585
+ return {
2586
+ toolResult: `Connection not active: ${connId}. The user must connect it before SQL can run.`,
2587
+ isError: true,
2588
+ uiSummary: "run_sql: connection inactive"
2589
+ };
2590
+ }
2591
+ if (getAccessMode(connId) === "read-only" && !isReadOnlyStatement(input.sql)) {
2592
+ return {
2593
+ toolResult: "This connection is read-only, so only a single read statement (SELECT / WITH / SHOW / EXPLAIN / TABLE / VALUES) is allowed. Do not attempt to bypass this. If the user wants to run writes or DDL, tell them to switch the connection to read-write in the Connections view.",
2594
+ isError: true,
2595
+ uiSummary: "run_sql: blocked (read-only connection)"
2596
+ };
2597
+ }
2598
+ const cap = Math.min(
2599
+ MAX_ROW_LIMIT,
2600
+ Math.max(1, Math.floor(input.row_limit ?? DEFAULT_ROW_LIMIT))
2601
+ );
2602
+ try {
2603
+ const res = await pool.query(input.sql);
2604
+ const columns = res.fields.map((f) => f.name);
2605
+ const capped = res.rows.slice(0, cap);
2606
+ const truncated = res.rows.length > cap;
2607
+ const note = truncated ? `
2608
+ (showing ${cap} of ${res.rows.length} rows)` : "";
2609
+ return {
2610
+ toolResult: format(capped, columns, res.rowCount) + note,
2611
+ isError: false,
2612
+ uiSummary: `Ran SQL: ${columns.length} cols, ${res.rowCount ?? capped.length} rows`
2613
+ };
2614
+ } catch (err) {
2615
+ const message = normalizePgError(err).message;
2616
+ return {
2617
+ toolResult: `Query failed: ${message}`,
2618
+ isError: true,
2619
+ uiSummary: `run_sql error: ${message}`
2620
+ };
2621
+ }
2622
+ }
2623
+ };
2624
+
2625
+ // server/agent/tools/render_chart.ts
2626
+ var CHART_TYPES = ["bar", "line", "area", "pie"];
2627
+ var MAX_ROWS2 = 500;
2628
+ var MAX_SERIES = 8;
2629
+ function parseSeries(raw) {
2630
+ if (!Array.isArray(raw) || raw.length === 0) {
2631
+ return "series must be a non-empty array of { key, label? }";
2632
+ }
2633
+ if (raw.length > MAX_SERIES) {
2634
+ return `too many series (${raw.length}); cap is ${MAX_SERIES}`;
2635
+ }
2636
+ const out = [];
2637
+ for (const item of raw) {
2638
+ if (typeof item === "string") {
2639
+ out.push({ key: item });
2640
+ continue;
2641
+ }
2642
+ if (item && typeof item === "object" && typeof item.key === "string") {
2643
+ const s = item;
2644
+ out.push(s.label ? { key: s.key, label: s.label } : { key: s.key });
2645
+ continue;
2646
+ }
2647
+ return "each series must be a string key or an object with a string `key`";
2648
+ }
2649
+ return out;
2650
+ }
2651
+ function parseData(raw) {
2652
+ if (!Array.isArray(raw) || raw.length === 0) {
2653
+ return "data must be a non-empty array of row objects";
2654
+ }
2655
+ if (raw.length > MAX_ROWS2) {
2656
+ return `too many rows (${raw.length}); cap is ${MAX_ROWS2}. Aggregate or LIMIT before charting.`;
2657
+ }
2658
+ for (const row of raw) {
2659
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
2660
+ return "each data element must be a plain object keyed by column name";
2661
+ }
2662
+ }
2663
+ return raw;
2664
+ }
2665
+ var renderChartTool = {
2666
+ name: "render_chart",
2667
+ description: `Render a chart inline in the chat to visualize query results. Supply the data rows directly (shaped from your run_sql results) along with the chart spec \u2014 the chart is self-contained. Use this after run_sql when a visualization communicates the answer better than a table: trends over time (line/area), category comparisons (bar), or proportions (pie). Keep data small and pre-aggregated (cap ${MAX_ROWS2} rows). Each data row is an object keyed by column name, e.g. \`[{ "month": "Jan", "revenue": 120 }, \u2026]\`. \`x\` names the category column; \`series\` names the numeric column(s) to plot. For pie charts only the first series is used.`,
2668
+ input_schema: {
2669
+ type: "object",
2670
+ required: ["type", "x", "series", "data"],
2671
+ properties: {
2672
+ type: {
2673
+ type: "string",
2674
+ enum: CHART_TYPES,
2675
+ description: "Chart kind: bar, line, area, or pie."
2676
+ },
2677
+ title: { type: "string", description: "Optional title shown above the chart." },
2678
+ x: {
2679
+ type: "string",
2680
+ description: "Row key for the category axis (x-axis for bar/line/area; slice label for pie)."
2681
+ },
2682
+ series: {
2683
+ type: "array",
2684
+ description: "Numeric series to plot. Each item is { key, label? } or a bare column-name string.",
2685
+ items: {
2686
+ type: "object",
2687
+ required: ["key"],
2688
+ properties: {
2689
+ key: { type: "string", description: "Row key holding this series' value." },
2690
+ label: { type: "string", description: "Legend/tooltip label (defaults to key)." }
2691
+ }
2692
+ }
2693
+ },
2694
+ data: {
2695
+ type: "array",
2696
+ description: "Row objects to chart, keyed by column name.",
2697
+ items: { type: "object" }
2698
+ }
2699
+ }
2700
+ },
2701
+ async execute(rawInput) {
2702
+ const input = rawInput ?? {};
2703
+ if (typeof input.type !== "string" || !CHART_TYPES.includes(input.type)) {
2704
+ return {
2705
+ toolResult: `Invalid type: must be one of ${CHART_TYPES.join(", ")}`,
2706
+ isError: true,
2707
+ uiSummary: "render_chart: bad type"
2708
+ };
2709
+ }
2710
+ if (typeof input.x !== "string" || input.x.trim() === "") {
2711
+ return {
2712
+ toolResult: "Invalid x: must be a non-empty string naming the category column",
2713
+ isError: true,
2714
+ uiSummary: "render_chart: bad x"
2715
+ };
2716
+ }
2717
+ const series = parseSeries(input.series);
2718
+ if (typeof series === "string") {
2719
+ return { toolResult: `Invalid series: ${series}`, isError: true, uiSummary: "render_chart: bad series" };
2720
+ }
2721
+ const data = parseData(input.data);
2722
+ if (typeof data === "string") {
2723
+ return { toolResult: `Invalid data: ${data}`, isError: true, uiSummary: "render_chart: bad data" };
2724
+ }
2725
+ const spec = {
2726
+ type: input.type,
2727
+ x: input.x,
2728
+ series,
2729
+ data,
2730
+ ...typeof input.title === "string" && input.title.trim() !== "" ? { title: input.title } : {}
2731
+ };
2732
+ const seriesLabel = series.map((s) => s.label ?? s.key).join(", ");
2733
+ return {
2734
+ toolResult: `Rendered ${spec.type} chart of ${seriesLabel} by ${spec.x} (${data.length} rows) in the chat.`,
2735
+ isError: false,
2736
+ uiSummary: `Charted ${seriesLabel} by ${spec.x} (${spec.type})`,
2737
+ events: [{ type: "chart_rendered", spec }]
2738
+ };
2739
+ }
2740
+ };
2741
+
2742
+ // server/agent/tools/index.ts
2743
+ var TOOLS = [
2744
+ getContextTool,
2745
+ updateContextTool,
2746
+ getSchemaTool,
2747
+ askUserQuestionTool,
2748
+ writeSqlTool,
2749
+ runSqlTool,
2750
+ renderChartTool
2751
+ ];
2752
+ var TOOL_BY_NAME = Object.fromEntries(
2753
+ TOOLS.map((t) => [t.name, t])
2754
+ );
2755
+ function findTool(name) {
2756
+ return TOOL_BY_NAME[name];
2757
+ }
2758
+ function anthropicToolDefs(opts = {}) {
2759
+ const { worksheetBound = true } = opts;
2760
+ return TOOLS.filter((t) => worksheetBound || t.name !== "write_sql").map((t) => ({
2761
+ name: t.name,
2762
+ description: t.description,
2763
+ input_schema: t.input_schema
2764
+ }));
2765
+ }
2766
+
2767
+ // server/agent/loop.ts
2768
+ var MAX_STEPS = 20;
2769
+ function toolUseBlocks(msg) {
2770
+ return msg.content.filter(
2771
+ (b) => b.type === "tool_use"
2772
+ );
2773
+ }
2774
+ function latestUserInput(session) {
2775
+ const last = session.messages[session.messages.length - 1];
2776
+ if (!last || last.role !== "user") return null;
2777
+ return last.content;
2778
+ }
2779
+ async function conversationParent(session) {
2780
+ if (!tracingEnabled()) return void 0;
2781
+ if (session.meta.traceParent) return session.meta.traceParent;
2782
+ const first = session.messages.find((m) => m.role === "user");
2783
+ const handle = await startConversationSpan({
2784
+ name: "conversation",
2785
+ event: {
2786
+ input: first ? first.content : null,
2787
+ metadata: {
2788
+ chatId: session.meta.id,
2789
+ title: session.meta.title,
2790
+ worksheetSlug: session.meta.worksheetSlug,
2791
+ connectionId: session.meta.connectionId,
2792
+ standalone: session.meta.standalone
2793
+ }
2794
+ }
2795
+ });
2796
+ if (handle) {
2797
+ session.meta.traceParent = handle;
2798
+ await persistSession(session);
2799
+ }
2800
+ return handle;
2801
+ }
2802
+ async function runAgentTurn(opts) {
2803
+ const parent = await conversationParent(opts.session);
2804
+ return traced(
2805
+ {
2806
+ name: "agent.turn",
2807
+ type: "task",
2808
+ parent,
2809
+ event: {
2810
+ input: latestUserInput(opts.session),
2811
+ metadata: {
2812
+ chatId: opts.session.meta.id,
2813
+ worksheetSlug: opts.session.meta.worksheetSlug,
2814
+ connectionId: opts.session.meta.connectionId,
2815
+ standalone: opts.session.meta.standalone
2816
+ }
2817
+ }
2818
+ },
2819
+ () => runTurn(opts)
2820
+ );
2821
+ }
2822
+ async function runTurn(opts) {
2823
+ const { session, emit } = opts;
2824
+ let apiKey;
2825
+ try {
2826
+ apiKey = await getAnthropicKey();
2827
+ } catch (err) {
2828
+ await emit({ type: "error", message: err.message });
2829
+ return;
2830
+ }
2831
+ const system = buildSystemPrompt(session);
2832
+ const tools = anthropicToolDefs({ worksheetBound: !!session.meta.worksheetSlug });
2833
+ for (let step = 0; step < MAX_STEPS; step += 1) {
2834
+ let final;
2835
+ let model;
2836
+ let usage;
2837
+ let textEmitChain = Promise.resolve();
2838
+ try {
2839
+ const result = await streamAssistantMessage({
2840
+ apiKey,
2841
+ system,
2842
+ tools,
2843
+ messages: session.messages,
2844
+ onTextDelta: (delta) => {
2845
+ textEmitChain = textEmitChain.then(
2846
+ () => Promise.resolve(emit({ type: "text_delta", text: delta }))
2847
+ );
2848
+ }
2849
+ });
2850
+ await textEmitChain;
2851
+ final = result.message;
2852
+ model = result.model;
2853
+ usage = result.usage;
2854
+ } catch (err) {
2855
+ await emit({ type: "error", message: err.message });
2856
+ return;
2857
+ }
2858
+ await appendMessage(session, { role: "assistant", content: final.content });
2859
+ const entry = {
2860
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2861
+ model,
2862
+ inputTokens: usage.input_tokens ?? 0,
2863
+ outputTokens: usage.output_tokens ?? 0,
2864
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
2865
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
2866
+ costUsd: costFromUsage(model, usage)
2867
+ };
2868
+ await recordUsage(session, entry);
2869
+ await emit({ type: "usage", entry, totals: session.meta.totals });
2870
+ const toolUses = toolUseBlocks(final);
2871
+ if (toolUses.length === 0) {
2872
+ await emit({ type: "done" });
2873
+ return;
2874
+ }
2875
+ const resultBlocks = [];
2876
+ let paused = null;
2877
+ for (const block of toolUses) {
2878
+ const tool = findTool(block.name);
2879
+ if (!tool) {
2880
+ await emit({
2881
+ type: "tool_result",
2882
+ toolUseId: block.id,
2883
+ name: block.name,
2884
+ ok: false,
2885
+ summary: `Unknown tool: ${block.name}`
2886
+ });
2887
+ resultBlocks.push({
2888
+ type: "tool_result",
2889
+ tool_use_id: block.id,
2890
+ content: `Unknown tool: ${block.name}`,
2891
+ is_error: true
2892
+ });
2893
+ continue;
2894
+ }
2895
+ await emit({
2896
+ type: "tool_start",
2897
+ toolUseId: block.id,
2898
+ name: tool.name,
2899
+ input: block.input
2900
+ });
2901
+ let execution;
2902
+ try {
2903
+ execution = await traced(
2904
+ {
2905
+ name: `tool.${tool.name}`,
2906
+ type: "tool",
2907
+ event: { input: block.input }
2908
+ },
2909
+ async (span) => {
2910
+ const result = await tool.execute(block.input, { session });
2911
+ span.log({
2912
+ output: result.toolResult,
2913
+ metadata: { isError: result.isError, summary: result.uiSummary }
2914
+ });
2915
+ return result;
2916
+ }
2917
+ );
2918
+ } catch (err) {
2919
+ const message = err.message;
2920
+ await emit({
2921
+ type: "tool_result",
2922
+ toolUseId: block.id,
2923
+ name: tool.name,
2924
+ ok: false,
2925
+ summary: `${tool.name} threw: ${message}`
2926
+ });
2927
+ resultBlocks.push({
2928
+ type: "tool_result",
2929
+ tool_use_id: block.id,
2930
+ content: `Tool threw: ${message}`,
2931
+ is_error: true
2932
+ });
2933
+ continue;
2934
+ }
2935
+ if (execution.events) {
2936
+ for (const e of execution.events) await emit(e);
2937
+ }
2938
+ await emit({
2939
+ type: "tool_result",
2940
+ toolUseId: block.id,
2941
+ name: tool.name,
2942
+ ok: !execution.isError,
2943
+ summary: execution.uiSummary
2944
+ });
2945
+ resultBlocks.push({
2946
+ type: "tool_result",
2947
+ tool_use_id: block.id,
2948
+ content: execution.toolResult,
2949
+ is_error: execution.isError
2950
+ });
2951
+ if (execution.pause) {
2952
+ paused = {
2953
+ toolUseId: block.id,
2954
+ question: execution.pause.question
2955
+ };
2956
+ }
2957
+ }
2958
+ await appendMessage(session, { role: "user", content: resultBlocks });
2959
+ if (paused) {
2960
+ await setPending(session, {
2961
+ toolUseId: paused.toolUseId,
2962
+ question: paused.question,
2963
+ askedAt: (/* @__PURE__ */ new Date()).toISOString()
2964
+ });
2965
+ await emit({
2966
+ type: "ask_user",
2967
+ toolUseId: paused.toolUseId,
2968
+ question: paused.question
2969
+ });
2970
+ return;
2971
+ }
2972
+ }
2973
+ await emit({
2974
+ type: "error",
2975
+ message: `Agent exceeded ${MAX_STEPS} steps without finishing.`
2976
+ });
2977
+ }
2978
+ async function resumeWithAnswer(opts) {
2979
+ const { session, userAnswer, emit } = opts;
2980
+ const pending = session.meta.pending;
2981
+ if (!pending) {
2982
+ await emit({ type: "error", message: "No pending question to resume." });
2983
+ return;
2984
+ }
2985
+ const last = session.messages[session.messages.length - 1];
2986
+ if (last && last.role === "user" && Array.isArray(last.content)) {
2987
+ for (const block of last.content) {
2988
+ if (block.type === "tool_result" && block.tool_use_id === pending.toolUseId) {
2989
+ block.content = `User answered: ${userAnswer}`;
2990
+ block.is_error = false;
2991
+ }
2992
+ }
2993
+ }
2994
+ await persistSession(session);
2995
+ await clearPending(session);
2996
+ await runAgentTurn({ session, emit });
2997
+ }
2998
+
2999
+ // server/api/agent.ts
3000
+ var app6 = new Hono8();
3001
+ var sessionLocks = /* @__PURE__ */ new Map();
3002
+ async function withSessionLock(id, fn) {
3003
+ const prev = sessionLocks.get(id) ?? Promise.resolve();
3004
+ const ours = prev.catch(() => {
3005
+ }).then(fn);
3006
+ sessionLocks.set(id, ours);
3007
+ try {
3008
+ return await ours;
3009
+ } finally {
3010
+ if (sessionLocks.get(id) === ours) sessionLocks.delete(id);
3011
+ }
3012
+ }
3013
+ app6.get("/sessions", async (c) => {
3014
+ const sessions = await listChats();
3015
+ return c.json({ sessions });
3016
+ });
3017
+ function addTotals(a, b) {
3018
+ return {
3019
+ inputTokens: a.inputTokens + b.inputTokens,
3020
+ outputTokens: a.outputTokens + b.outputTokens,
3021
+ cacheCreationTokens: a.cacheCreationTokens + b.cacheCreationTokens,
3022
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
3023
+ costUsd: a.costUsd + b.costUsd,
3024
+ calls: a.calls + b.calls
3025
+ };
3026
+ }
3027
+ app6.get("/usage", async (c) => {
3028
+ const slug = c.req.query("worksheetSlug");
3029
+ const all = await listChats();
3030
+ const sessions = slug ? all.filter((s) => s.worksheetSlug === slug) : all;
3031
+ const totals = sessions.reduce(
3032
+ (acc, s) => addTotals(acc, s.totals),
3033
+ emptyTotals()
3034
+ );
3035
+ const bySession = sessions.map((s) => ({
3036
+ id: s.id,
3037
+ title: s.title,
3038
+ worksheetSlug: s.worksheetSlug,
3039
+ updatedAt: s.updatedAt,
3040
+ totals: s.totals
3041
+ }));
3042
+ return c.json({ totals, bySession });
3043
+ });
3044
+ app6.get("/sessions/:id/usage", async (c) => {
3045
+ const session = await getChat(c.req.param("id"));
3046
+ if (!session) return c.json({ error: "not_found" }, 404);
3047
+ return c.json({
3048
+ totals: session.meta.totals,
3049
+ entries: session.meta.usage
3050
+ });
3051
+ });
3052
+ app6.post("/sessions", async (c) => {
3053
+ const body = await c.req.json().catch(() => ({}));
3054
+ const session = await createChat({
3055
+ worksheetSlug: body.worksheetSlug ?? null,
3056
+ connectionId: body.connectionId ?? null,
3057
+ title: body.title ?? null,
3058
+ standalone: body.standalone ?? false
3059
+ });
3060
+ return c.json(session, 201);
3061
+ });
3062
+ app6.get("/sessions/:id", async (c) => {
3063
+ const session = await getChat(c.req.param("id"));
3064
+ if (!session) return c.json({ error: "not_found" }, 404);
3065
+ return c.json(session);
3066
+ });
3067
+ app6.patch("/sessions/:id", async (c) => {
3068
+ const id = c.req.param("id");
3069
+ const body = await c.req.json().catch(() => ({}));
3070
+ return withSessionLock(id, async () => {
3071
+ const session = await getChat(id);
3072
+ if (!session) return c.json({ error: "not_found" }, 404);
3073
+ if ("connectionId" in body) {
3074
+ const cid = body.connectionId;
3075
+ if (cid !== null && typeof cid !== "string") {
3076
+ return c.json({ error: "connectionId must be a string or null" }, 400);
3077
+ }
3078
+ if (cid !== null && !activeIds().has(cid)) {
3079
+ return c.json({ error: "connection is not active" }, 400);
3080
+ }
3081
+ await setConnection(session, cid);
3082
+ }
3083
+ return c.json(session.meta);
3084
+ });
3085
+ });
3086
+ app6.delete("/sessions/:id", async (c) => {
3087
+ await deleteChat(c.req.param("id"));
3088
+ return c.json({ ok: true });
3089
+ });
3090
+ function firstUserText(messages) {
3091
+ const first = messages.find((m) => m.role === "user");
3092
+ if (!first) return "";
3093
+ const { content } = first;
3094
+ if (typeof content === "string") return content.trim();
3095
+ if (Array.isArray(content)) {
3096
+ return content.map(
3097
+ (b) => b && typeof b === "object" && "type" in b && b.type === "text" && "text" in b ? String(b.text) : ""
3098
+ ).join("").trim();
3099
+ }
3100
+ return "";
3101
+ }
3102
+ app6.post("/sessions/:id/auto-name", async (c) => {
3103
+ const id = c.req.param("id");
3104
+ return withSessionLock(id, async () => {
3105
+ const session = await getChat(id);
3106
+ if (!session) return c.json({ error: "not_found" }, 404);
3107
+ if (session.meta.titleGenerated) {
3108
+ return c.json({
3109
+ title: session.meta.title,
3110
+ skipped: true,
3111
+ reason: "already-named"
3112
+ });
3113
+ }
3114
+ const prompt = firstUserText(session.messages);
3115
+ if (!prompt) {
3116
+ return c.json({ title: session.meta.title, skipped: true, reason: "empty" });
3117
+ }
3118
+ try {
3119
+ const title = await generateChatTitle(prompt);
3120
+ await setTitle(session, title, true);
3121
+ return c.json({ title, skipped: false });
3122
+ } catch (err) {
3123
+ const message = err.message;
3124
+ console.warn(`auto-name chat ${id} failed:`, message);
3125
+ return c.json({
3126
+ title: session.meta.title,
3127
+ skipped: true,
3128
+ reason: "model-error",
3129
+ error: message
3130
+ });
3131
+ }
3132
+ });
3133
+ });
3134
+ app6.post("/sessions/:id/messages", async (c) => {
3135
+ const id = c.req.param("id");
3136
+ const body = await c.req.json().catch(() => ({}));
3137
+ const message = typeof body.message === "string" ? body.message.trim() : "";
3138
+ if (message === "") {
3139
+ return c.json({ error: "Missing required field: message" }, 400);
3140
+ }
3141
+ const initial = await getChat(id);
3142
+ if (!initial) return c.json({ error: "not_found" }, 404);
3143
+ if (initial.meta.pending) {
3144
+ return c.json(
3145
+ {
3146
+ error: "Session is waiting on a pending question. POST /respond to answer it first."
3147
+ },
3148
+ 409
3149
+ );
3150
+ }
3151
+ return streamSSE(c, async (stream) => {
3152
+ const emit = async (event) => {
3153
+ await stream.writeSSE({ data: JSON.stringify(event) });
3154
+ };
3155
+ try {
3156
+ await withSessionLock(id, async () => {
3157
+ const session = await getChat(id);
3158
+ if (!session) {
3159
+ await emit({ type: "error", message: "Session no longer exists." });
3160
+ return;
3161
+ }
3162
+ if (session.meta.pending) {
3163
+ await emit({
3164
+ type: "error",
3165
+ message: "Session is waiting on a pending question \u2014 answer via /respond first."
3166
+ });
3167
+ return;
3168
+ }
3169
+ await appendMessage(session, { role: "user", content: message });
3170
+ if (!session.meta.title) {
3171
+ await setTitle(session, message.slice(0, 60));
3172
+ }
3173
+ await runAgentTurn({ session, emit });
3174
+ });
3175
+ } catch (err) {
3176
+ await emit({ type: "error", message: err.message });
3177
+ }
3178
+ });
3179
+ });
3180
+ app6.post("/sessions/:id/respond", async (c) => {
3181
+ const id = c.req.param("id");
3182
+ const body = await c.req.json().catch(() => ({}));
3183
+ const answer = typeof body.answer === "string" ? body.answer.trim() : "";
3184
+ if (answer === "") {
3185
+ return c.json({ error: "Missing required field: answer" }, 400);
3186
+ }
3187
+ const initial = await getChat(id);
3188
+ if (!initial) return c.json({ error: "not_found" }, 404);
3189
+ if (!initial.meta.pending) {
3190
+ return c.json({ error: "No pending question to answer." }, 409);
3191
+ }
3192
+ return streamSSE(c, async (stream) => {
3193
+ const emit = async (event) => {
3194
+ await stream.writeSSE({ data: JSON.stringify(event) });
3195
+ };
3196
+ try {
3197
+ await withSessionLock(id, async () => {
3198
+ const session = await getChat(id);
3199
+ if (!session) {
3200
+ await emit({ type: "error", message: "Session no longer exists." });
3201
+ return;
3202
+ }
3203
+ if (!session.meta.pending) {
3204
+ await emit({
3205
+ type: "error",
3206
+ message: "No pending question to answer (already resolved)."
3207
+ });
3208
+ return;
3209
+ }
3210
+ await resumeWithAnswer({ session, userAnswer: answer, emit });
3211
+ });
3212
+ } catch (err) {
3213
+ await emit({ type: "error", message: err.message });
3214
+ }
3215
+ });
3216
+ });
3217
+ var agent_default = app6;
3218
+
3219
+ // server/api/context.ts
3220
+ import { Hono as Hono9 } from "hono";
3221
+ import { promises as fs17 } from "node:fs";
3222
+ import { HTTPException as HTTPException3 } from "hono/http-exception";
3223
+ var app7 = new Hono9();
3224
+ var MAX_DOC_BYTES = 256 * 1024;
3225
+ function defFor(name) {
3226
+ return CONTEXT_DOCS.find((d) => d.name === name);
3227
+ }
3228
+ function scopeOf(c) {
3229
+ const id = c.req.query("connectionId");
3230
+ if (!id) return null;
3231
+ assertSafeConnectionId(id);
3232
+ return id;
3233
+ }
3234
+ async function readMeta(name, connectionId) {
3235
+ const def = CONTEXT_DOCS.find((d) => d.name === name);
3236
+ try {
3237
+ const st = await fs17.stat(paths.contextFile(name, connectionId));
3238
+ return { ...def, updatedAt: st.mtime.toISOString(), size: st.size };
3239
+ } catch (err) {
3240
+ if (err.code !== "ENOENT") throw err;
3241
+ return { ...def, updatedAt: null, size: 0 };
3242
+ }
3243
+ }
3244
+ async function readContent(name, connectionId) {
3245
+ try {
3246
+ return await fs17.readFile(paths.contextFile(name, connectionId), "utf8");
3247
+ } catch (err) {
3248
+ if (err.code === "ENOENT") return "";
3249
+ throw err;
3250
+ }
3251
+ }
3252
+ app7.get("/", async (c) => {
3253
+ const connectionId = scopeOf(c);
3254
+ const docs = await Promise.all(CONTEXT_DOCS.map((d) => readMeta(d.name, connectionId)));
3255
+ return c.json({ connectionId, docs });
3256
+ });
3257
+ app7.get("/:name", async (c) => {
3258
+ const name = c.req.param("name");
3259
+ if (!defFor(name)) throw new HTTPException3(404, { message: `Unknown context doc: ${name}` });
3260
+ const docName = name;
3261
+ const connectionId = scopeOf(c);
3262
+ const [meta, content] = await Promise.all([
3263
+ readMeta(docName, connectionId),
3264
+ readContent(docName, connectionId)
3265
+ ]);
3266
+ return c.json({ meta, content });
3267
+ });
3268
+ app7.put("/:name", async (c) => {
3269
+ const name = c.req.param("name");
3270
+ if (!defFor(name)) throw new HTTPException3(404, { message: `Unknown context doc: ${name}` });
3271
+ const docName = name;
3272
+ const connectionId = scopeOf(c);
3273
+ const body = await c.req.json().catch(() => ({}));
3274
+ if (typeof body.content !== "string") {
3275
+ throw new HTTPException3(400, { message: "content must be a string" });
3276
+ }
3277
+ if (Buffer.byteLength(body.content, "utf8") > MAX_DOC_BYTES) {
3278
+ throw new HTTPException3(413, { message: "context doc exceeds 256 KiB" });
3279
+ }
3280
+ const text = body.content === "" ? "" : body.content.replace(/\n*$/, "") + "\n";
3281
+ await writeAtomic(paths.contextFile(docName, connectionId), text);
3282
+ const meta = await readMeta(docName, connectionId);
3283
+ return c.json({ meta });
3284
+ });
3285
+ var context_default = app7;
3286
+
3287
+ // server/history/watcher.ts
3288
+ import { promises as fs18, watch } from "node:fs";
3289
+ import path10 from "node:path";
3290
+ import { createHash as createHash2 } from "node:crypto";
3291
+ var SLUG_RE2 = /^[a-z0-9][a-z0-9_-]*$/i;
3292
+ var watcher = null;
3293
+ function startWorksheetsWatcher() {
3294
+ const dir = paths.worksheets();
3295
+ watcher = watch(dir, (eventType, filename) => {
3296
+ if (!filename || !filename.endsWith(".sql")) return;
3297
+ if (eventType !== "change" && eventType !== "rename") return;
3298
+ const slug = filename.slice(0, -4);
3299
+ if (!SLUG_RE2.test(slug)) return;
3300
+ void handleEvent(slug, path10.join(dir, filename));
3301
+ });
3302
+ watcher.on("error", (err) => {
3303
+ console.warn("[history watcher]", err.message);
3304
+ });
3305
+ }
3306
+ async function handleEvent(slug, filePath2) {
3307
+ let content;
3308
+ try {
3309
+ content = await fs18.readFile(filePath2, "utf8");
3310
+ } catch {
3311
+ return;
3312
+ }
3313
+ const sha = createHash2("sha256").update(content, "utf8").digest("hex");
3314
+ if (isRecentWrite(slug, sha)) return;
3315
+ recordHistory({ worksheet: slug, source: "external", content });
3316
+ }
3317
+ function stopWorksheetsWatcher() {
3318
+ watcher?.close();
3319
+ watcher = null;
3320
+ }
3321
+
3322
+ // server/static.ts
3323
+ import { existsSync } from "node:fs";
3324
+ import { readFile } from "node:fs/promises";
3325
+ import { fileURLToPath } from "node:url";
3326
+ import path11 from "node:path";
3327
+ function defaultClientDir() {
3328
+ if (process.env.OSDPT_CLIENT_DIST) return path11.resolve(process.env.OSDPT_CLIENT_DIST);
3329
+ return fileURLToPath(new URL("./client", import.meta.url));
3330
+ }
3331
+ var MIME = {
3332
+ ".html": "text/html; charset=utf-8",
3333
+ ".js": "text/javascript; charset=utf-8",
3334
+ ".mjs": "text/javascript; charset=utf-8",
3335
+ ".css": "text/css; charset=utf-8",
3336
+ ".json": "application/json; charset=utf-8",
3337
+ ".svg": "image/svg+xml",
3338
+ ".png": "image/png",
3339
+ ".jpg": "image/jpeg",
3340
+ ".jpeg": "image/jpeg",
3341
+ ".gif": "image/gif",
3342
+ ".ico": "image/x-icon",
3343
+ ".webp": "image/webp",
3344
+ ".woff": "font/woff",
3345
+ ".woff2": "font/woff2",
3346
+ ".ttf": "font/ttf",
3347
+ ".map": "application/json; charset=utf-8"
3348
+ };
3349
+ function contentType(file) {
3350
+ return MIME[path11.extname(file).toLowerCase()] ?? "application/octet-stream";
3351
+ }
3352
+ function cacheControl(urlPath) {
3353
+ return urlPath.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache";
3354
+ }
3355
+ function serveClient(app8, clientDir = defaultClientDir()) {
3356
+ if (!existsSync(path11.join(clientDir, "index.html"))) return false;
3357
+ const indexHtml = path11.join(clientDir, "index.html");
3358
+ app8.get("*", async (c) => {
3359
+ if (c.req.path.startsWith("/api")) return c.notFound();
3360
+ const rel = decodeURIComponent(c.req.path).replace(/^\/+/, "");
3361
+ const candidate = path11.resolve(clientDir, rel);
3362
+ const isInside = candidate === clientDir || candidate.startsWith(clientDir + path11.sep);
3363
+ let file = indexHtml;
3364
+ if (isInside && rel !== "" && existsSync(candidate) && !candidate.endsWith(path11.sep)) {
3365
+ file = candidate;
3366
+ }
3367
+ try {
3368
+ const body = await readFile(file);
3369
+ return new Response(body, {
3370
+ status: 200,
3371
+ headers: {
3372
+ "content-type": contentType(file),
3373
+ "cache-control": cacheControl(c.req.path)
3374
+ }
3375
+ });
3376
+ } catch {
3377
+ return c.notFound();
3378
+ }
3379
+ });
3380
+ return true;
3381
+ }
3382
+
3383
+ // server/lib/open-browser.ts
3384
+ import { spawn } from "node:child_process";
3385
+ function openBrowser(url) {
3386
+ const platform = process.platform;
3387
+ const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
3388
+ return new Promise((resolve) => {
3389
+ try {
3390
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
3391
+ child.on("error", () => resolve(false));
3392
+ child.unref();
3393
+ setTimeout(() => resolve(true), 0);
3394
+ } catch {
3395
+ resolve(false);
3396
+ }
3397
+ });
3398
+ }
3399
+
3400
+ // server/index.ts
3401
+ var DEFAULT_PORT = 3756;
3402
+ function listen(app8, port) {
3403
+ return new Promise((resolve, reject) => {
3404
+ let retried = false;
3405
+ const attempt = (p) => {
3406
+ const server = serve({ fetch: app8.fetch, port: p, hostname: "127.0.0.1" });
3407
+ server.once("listening", () => resolve(server));
3408
+ server.once("error", (err) => {
3409
+ if (err.code === "EADDRINUSE" && !retried) {
3410
+ retried = true;
3411
+ attempt(0);
3412
+ } else {
3413
+ reject(err);
3414
+ }
3415
+ });
3416
+ };
3417
+ attempt(port);
3418
+ });
3419
+ }
3420
+ async function startServer(argv = process.argv.slice(2)) {
3421
+ const workspace = await initWorkspace(argv);
3422
+ await initTracing();
3423
+ openHistoryDb();
3424
+ startWorksheetsWatcher();
3425
+ void autoConnectAll(workspace);
3426
+ const app8 = new Hono10();
3427
+ app8.get("/api/health", (c) => c.json({ ok: true, workspace }));
3428
+ app8.route("/api/connections", connectionsRouter(workspace));
3429
+ app8.route("/api/ai-providers", aiProvidersRouter(workspace));
3430
+ app8.route("/api/worksheets", worksheets_default);
3431
+ app8.route("/api/drafts", drafts_default);
3432
+ app8.route("/api/session", session_default);
3433
+ app8.route("/api/schema", schema_default);
3434
+ app8.route("/api/history", history_default);
3435
+ app8.route("/api/agent", agent_default);
3436
+ app8.route("/api/context", context_default);
3437
+ app8.onError((err, c) => {
3438
+ if (err instanceof HTTPException4) return err.getResponse();
3439
+ console.error("[os-dpt]", err);
3440
+ return c.json({ error: err.message }, 500);
3441
+ });
3442
+ serveClient(app8);
3443
+ const preferredPort = Number(process.env.PORT ?? DEFAULT_PORT);
3444
+ const server = await listen(app8, preferredPort);
3445
+ const addr = server.address();
3446
+ const port = typeof addr === "object" && addr ? addr.port : preferredPort;
3447
+ const url = `http://127.0.0.1:${port}`;
3448
+ let shuttingDown = false;
3449
+ const close = async () => {
3450
+ if (shuttingDown) return;
3451
+ shuttingDown = true;
3452
+ stopWorksheetsWatcher();
3453
+ closeHistoryDb();
3454
+ await flushTracing();
3455
+ await closeAll();
3456
+ await new Promise((res) => server.close(() => res()));
3457
+ };
3458
+ return { url, port, workspace, close };
3459
+ }
3460
+ var isMain = process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href;
3461
+ if (isMain) {
3462
+ const boot = await startServer();
3463
+ console.log(`os-dpt server listening on ${boot.url}`);
3464
+ console.log(`workspace: ${boot.workspace}`);
3465
+ const shutdown = async (signal) => {
3466
+ console.log(`
3467
+ received ${signal}, closing pools\u2026`);
3468
+ await boot.close();
3469
+ process.exit(0);
3470
+ };
3471
+ process.on("SIGINT", shutdown);
3472
+ process.on("SIGTERM", shutdown);
3473
+ }
3474
+ export {
3475
+ openBrowser,
3476
+ startServer
3477
+ };