os-dpt 0.1.0 → 0.2.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.
- package/README.md +54 -23
- package/package.json +26 -12
- package/server/dist/client/assets/{highlighted-body-OFNGDK62-CgyhRXYP.js → highlighted-body-OFNGDK62-DVsDtUg7.js} +1 -1
- package/server/dist/client/assets/index-BbfkzqpQ.css +1 -0
- package/server/dist/client/assets/index-BejCAhC3.js +251 -0
- package/server/dist/client/index.html +3 -3
- package/server/dist/index.mjs +292 -43
- package/server/dist/client/assets/index-CsUxdPx_.css +0 -1
- package/server/dist/client/assets/index-G634ntMY.js +0 -245
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/dpt.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<title>Data Profile Tool</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BejCAhC3.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BbfkzqpQ.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/server/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ for (const envPath of [".env", "../.env"]) {
|
|
|
9
9
|
// server/index.ts
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
11
|
import { serve } from "@hono/node-server";
|
|
12
|
-
import { Hono as
|
|
12
|
+
import { Hono as Hono11 } from "hono";
|
|
13
13
|
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
14
14
|
|
|
15
15
|
// server/api/ai-providers.ts
|
|
@@ -221,6 +221,7 @@ var CONTEXT_DOC_NAMES = CONTEXT_DOCS.map((d) => d.name);
|
|
|
221
221
|
// server/workspace.ts
|
|
222
222
|
var exec = promisify(execFile);
|
|
223
223
|
var WORKSHEETS_DIR = "worksheets";
|
|
224
|
+
var DASHBOARDS_DIR = "dashboards";
|
|
224
225
|
var CONTEXT_DIR = "context";
|
|
225
226
|
var CONTEXT_BY_SOURCE = "by-source";
|
|
226
227
|
var OSDPT_DIR = ".os-dpt";
|
|
@@ -244,6 +245,7 @@ function resolveWorkspace(argv) {
|
|
|
244
245
|
async function initWorkspace(argv = process.argv.slice(2)) {
|
|
245
246
|
const root = resolveWorkspace(argv);
|
|
246
247
|
await fs4.mkdir(path4.join(root, WORKSHEETS_DIR), { recursive: true });
|
|
248
|
+
await fs4.mkdir(path4.join(root, DASHBOARDS_DIR), { recursive: true });
|
|
247
249
|
await fs4.mkdir(path4.join(root, DRAFTS_DIR), { recursive: true });
|
|
248
250
|
await fs4.mkdir(path4.join(root, CHATS_DIR), { recursive: true });
|
|
249
251
|
await fs4.mkdir(path4.join(root, CONTEXT_DIR), { recursive: true });
|
|
@@ -300,6 +302,8 @@ function workspacePath(...segments) {
|
|
|
300
302
|
var paths = {
|
|
301
303
|
worksheets: () => workspacePath(WORKSHEETS_DIR),
|
|
302
304
|
worksheet: (slug) => workspacePath(WORKSHEETS_DIR, `${slug}.sql`),
|
|
305
|
+
dashboards: () => workspacePath(DASHBOARDS_DIR),
|
|
306
|
+
dashboard: (slug) => workspacePath(DASHBOARDS_DIR, `${slug}.json`),
|
|
303
307
|
drafts: () => workspacePath(DRAFTS_DIR),
|
|
304
308
|
draft: (slug) => workspacePath(DRAFTS_DIR, `${slug}.sql`),
|
|
305
309
|
schemas: () => workspacePath(SCHEMAS_DIR),
|
|
@@ -634,6 +638,7 @@ async function introspect(pool) {
|
|
|
634
638
|
|
|
635
639
|
// server/db/postgres.ts
|
|
636
640
|
import pg from "pg";
|
|
641
|
+
pg.types.setTypeParser(pg.types.builtins.DATE, (value) => value);
|
|
637
642
|
function clientConfig(conn, password) {
|
|
638
643
|
return {
|
|
639
644
|
host: conn.host,
|
|
@@ -1183,9 +1188,9 @@ import { promises as fs9 } from "node:fs";
|
|
|
1183
1188
|
import path8 from "node:path";
|
|
1184
1189
|
|
|
1185
1190
|
// server/lib/slug.ts
|
|
1186
|
-
function slugify(name) {
|
|
1191
|
+
function slugify(name, fallback = "worksheet") {
|
|
1187
1192
|
const base = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1188
|
-
return base ||
|
|
1193
|
+
return base || fallback;
|
|
1189
1194
|
}
|
|
1190
1195
|
function dedupeSlug(slug, existing) {
|
|
1191
1196
|
if (!existing.has(slug)) return slug;
|
|
@@ -1969,6 +1974,14 @@ import { Hono as Hono8 } from "hono";
|
|
|
1969
1974
|
import { streamSSE } from "hono/streaming";
|
|
1970
1975
|
|
|
1971
1976
|
// shared/agent.ts
|
|
1977
|
+
var CHART_TYPES = [
|
|
1978
|
+
"bar",
|
|
1979
|
+
"stacked-bar",
|
|
1980
|
+
"line",
|
|
1981
|
+
"area",
|
|
1982
|
+
"pie",
|
|
1983
|
+
"funnel"
|
|
1984
|
+
];
|
|
1972
1985
|
function emptyTotals() {
|
|
1973
1986
|
return {
|
|
1974
1987
|
inputTokens: 0,
|
|
@@ -2063,7 +2076,7 @@ function buildSystemPrompt(session) {
|
|
|
2063
2076
|
"- run_sql results are capped to 50 rows by default; add LIMIT or aggregation if you need a broader view.",
|
|
2064
2077
|
"- 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.",
|
|
2065
2078
|
"- ask_user_question pauses the loop entirely \u2014 only one question per call, and use it sparingly.",
|
|
2066
|
-
"- 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."
|
|
2079
|
+
"- 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, part-to-whole \u2192 stacked-bar, proportions \u2192 pie, stage drop-off \u2192 funnel), 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."
|
|
2067
2080
|
);
|
|
2068
2081
|
const ctx = [];
|
|
2069
2082
|
if (worksheetSlug) ctx.push(`- Active worksheet: ${worksheetSlug}`);
|
|
@@ -2080,16 +2093,17 @@ function buildSystemPrompt(session) {
|
|
|
2080
2093
|
}
|
|
2081
2094
|
function buildQuickEditPrompt(session) {
|
|
2082
2095
|
const { worksheetSlug, connectionId } = session.meta;
|
|
2096
|
+
const noun = worksheetSlug ? "worksheet" : "query";
|
|
2083
2097
|
const lines = [
|
|
2084
|
-
"You are os-dpt's inline SQL-editing agent. The user prompts you from a small floating box inside their SQL editor. Your ONLY deliverable is SQL staged into their worksheet via write_sql \u2014 the user never reads chat output in this mode, they watch the editor update.",
|
|
2098
|
+
worksheetSlug ? "You are os-dpt's inline SQL-editing agent. The user prompts you from a small floating box inside their SQL editor. Your ONLY deliverable is SQL staged into their worksheet via write_sql \u2014 the user never reads chat output in this mode, they watch the editor update." : "You are os-dpt's inline SQL-editing agent. The user is editing a standalone SQL query (e.g. a saved chart's source query). Your ONLY deliverable is SQL staged back into their editor via write_sql \u2014 the user never reads chat output in this mode, they watch the editor update.",
|
|
2085
2099
|
"",
|
|
2086
2100
|
"Output rules (hard requirements):",
|
|
2087
2101
|
"- Emit NO prose. No preamble, no explanation, no closing summary \u2014 no text blocks at all.",
|
|
2088
2102
|
"- Anything worth flagging (an assumption you made, a caveat, a data-quality quirk) goes in a SQL comment inside the query itself.",
|
|
2089
|
-
|
|
2103
|
+
`- Always finish by calling write_sql with the COMPLETE ${noun} contents (writes are overwrites, not patches), then end the turn.`,
|
|
2090
2104
|
"",
|
|
2091
2105
|
"Method:",
|
|
2092
|
-
|
|
2106
|
+
`- Each user message ends with the ${noun}'s current contents. Treat the request as an edit to that SQL unless it clearly asks for something new; preserve parts of the ${noun} the request doesn't touch.`,
|
|
2093
2107
|
"- Check get_context first when the request depends on schema, business terms, or conventions you have not verified this session; call get_schema when tables or columns are uncertain. Do not guess names.",
|
|
2094
2108
|
"- Verify your SQL with run_sql before staging it. If it errors, fix and re-run until it works. Treat verification as read-only \u2014 SELECT only; never run DDL or DML in this mode.",
|
|
2095
2109
|
"- If no connection is bound or verification is impossible, stage your best SQL with a leading '-- not verified' comment instead of stopping.",
|
|
@@ -2290,6 +2304,32 @@ import { promises as fs16 } from "node:fs";
|
|
|
2290
2304
|
function isContextFile(v) {
|
|
2291
2305
|
return CONTEXT_FILES.includes(v);
|
|
2292
2306
|
}
|
|
2307
|
+
var DETAIL_CONTEXT_LINES = 8;
|
|
2308
|
+
function toLines(text) {
|
|
2309
|
+
return text === "" ? [] : text.replace(/\n$/, "").split("\n");
|
|
2310
|
+
}
|
|
2311
|
+
function detailSnapshots(before, after) {
|
|
2312
|
+
const a = toLines(before);
|
|
2313
|
+
const b = toLines(after);
|
|
2314
|
+
let prefix = 0;
|
|
2315
|
+
while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) prefix++;
|
|
2316
|
+
let suffix = 0;
|
|
2317
|
+
while (suffix < a.length - prefix && suffix < b.length - prefix && a[a.length - 1 - suffix] === b[b.length - 1 - suffix]) {
|
|
2318
|
+
suffix++;
|
|
2319
|
+
}
|
|
2320
|
+
const leading = Math.max(0, prefix - DETAIL_CONTEXT_LINES);
|
|
2321
|
+
const trailing = Math.max(0, suffix - DETAIL_CONTEXT_LINES);
|
|
2322
|
+
if (leading === 0 && trailing === 0) return { before, after };
|
|
2323
|
+
let fences = 0;
|
|
2324
|
+
for (let i = 0; i < leading; i++) {
|
|
2325
|
+
if (/^\s{0,3}(```|~~~)/.test(a[i])) fences++;
|
|
2326
|
+
}
|
|
2327
|
+
return {
|
|
2328
|
+
before: a.slice(leading, a.length - trailing).join("\n"),
|
|
2329
|
+
after: b.slice(leading, b.length - trailing).join("\n"),
|
|
2330
|
+
trimmed: { leading, trailing, ...fences % 2 === 1 ? { inFence: true } : {} }
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2293
2333
|
var updateContextTool = {
|
|
2294
2334
|
name: "update_context",
|
|
2295
2335
|
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.",
|
|
@@ -2338,16 +2378,16 @@ var updateContextTool = {
|
|
|
2338
2378
|
};
|
|
2339
2379
|
}
|
|
2340
2380
|
const target = paths.contextFile(input.file, connectionId);
|
|
2381
|
+
let current = "";
|
|
2382
|
+
try {
|
|
2383
|
+
current = await fs16.readFile(target, "utf8");
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
if (err.code !== "ENOENT") throw err;
|
|
2386
|
+
}
|
|
2341
2387
|
let next;
|
|
2342
2388
|
if (input.mode === "replace") {
|
|
2343
2389
|
next = input.content.endsWith("\n") ? input.content : input.content + "\n";
|
|
2344
2390
|
} else {
|
|
2345
|
-
let current = "";
|
|
2346
|
-
try {
|
|
2347
|
-
current = await fs16.readFile(target, "utf8");
|
|
2348
|
-
} catch (err) {
|
|
2349
|
-
if (err.code !== "ENOENT") throw err;
|
|
2350
|
-
}
|
|
2351
2391
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2352
2392
|
const block = `## ${stamp}
|
|
2353
2393
|
|
|
@@ -2359,7 +2399,8 @@ ${input.content.trim()}
|
|
|
2359
2399
|
return {
|
|
2360
2400
|
toolResult: `Wrote ${next.length} bytes to context/${input.file}.md (${input.mode})`,
|
|
2361
2401
|
isError: false,
|
|
2362
|
-
uiSummary: `Updated context/${input.file}.md (${input.mode})
|
|
2402
|
+
uiSummary: `Updated context/${input.file}.md (${input.mode})`,
|
|
2403
|
+
detail: { file: `${input.file}.md`, mode: input.mode, ...detailSnapshots(current, next) }
|
|
2363
2404
|
};
|
|
2364
2405
|
}
|
|
2365
2406
|
};
|
|
@@ -2504,7 +2545,7 @@ var askUserQuestionTool = {
|
|
|
2504
2545
|
// server/agent/tools/write_sql.ts
|
|
2505
2546
|
var writeSqlTool = {
|
|
2506
2547
|
name: "write_sql",
|
|
2507
|
-
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.",
|
|
2548
|
+
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. When no worksheet is bound, the SQL streams straight back to the editor that launched this session.",
|
|
2508
2549
|
input_schema: {
|
|
2509
2550
|
type: "object",
|
|
2510
2551
|
required: ["sql"],
|
|
@@ -2530,6 +2571,14 @@ var writeSqlTool = {
|
|
|
2530
2571
|
}
|
|
2531
2572
|
const slug = input.worksheet_slug ?? ctx.session.meta.worksheetSlug;
|
|
2532
2573
|
if (!slug) {
|
|
2574
|
+
if (ctx.session.meta.mode === "quick-edit") {
|
|
2575
|
+
return {
|
|
2576
|
+
toolResult: `Staged ${input.sql.length} bytes back to the editor.`,
|
|
2577
|
+
isError: false,
|
|
2578
|
+
uiSummary: `Staged ${input.sql.length} chars into the editor`,
|
|
2579
|
+
events: [{ type: "sql_written", worksheetSlug: null, sql: input.sql }]
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2533
2582
|
return {
|
|
2534
2583
|
toolResult: "No worksheet bound. Ask the user which worksheet to write to, or set worksheet_slug.",
|
|
2535
2584
|
isError: true,
|
|
@@ -2675,7 +2724,6 @@ var runSqlTool = {
|
|
|
2675
2724
|
};
|
|
2676
2725
|
|
|
2677
2726
|
// server/agent/tools/render_chart.ts
|
|
2678
|
-
var CHART_TYPES = ["bar", "line", "area", "pie"];
|
|
2679
2727
|
var MAX_ROWS2 = 500;
|
|
2680
2728
|
var MAX_SERIES = 8;
|
|
2681
2729
|
function parseSeries(raw) {
|
|
@@ -2716,7 +2764,7 @@ function parseData(raw) {
|
|
|
2716
2764
|
}
|
|
2717
2765
|
var renderChartTool = {
|
|
2718
2766
|
name: "render_chart",
|
|
2719
|
-
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),
|
|
2767
|
+
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), part-to-whole across categories (stacked-bar), proportions (pie), or stage-by-stage drop-off like a conversion pipeline (funnel). 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 and funnel charts only the first series is used; for funnel, order rows from the widest stage to the narrowest. Pass \`sourceQuery\` \u2014 the \`name\` you gave the run_sql call the data came from \u2014 so the chart links back to its query.`,
|
|
2720
2768
|
input_schema: {
|
|
2721
2769
|
type: "object",
|
|
2722
2770
|
required: ["type", "x", "series", "data"],
|
|
@@ -2724,7 +2772,7 @@ var renderChartTool = {
|
|
|
2724
2772
|
type: {
|
|
2725
2773
|
type: "string",
|
|
2726
2774
|
enum: CHART_TYPES,
|
|
2727
|
-
description: "Chart kind: bar, line, area, or
|
|
2775
|
+
description: "Chart kind: bar, stacked-bar, line, area, pie, or funnel."
|
|
2728
2776
|
},
|
|
2729
2777
|
title: { type: "string", description: "Optional title shown above the chart." },
|
|
2730
2778
|
x: {
|
|
@@ -2747,6 +2795,10 @@ var renderChartTool = {
|
|
|
2747
2795
|
type: "array",
|
|
2748
2796
|
description: "Row objects to chart, keyed by column name.",
|
|
2749
2797
|
items: { type: "object" }
|
|
2798
|
+
},
|
|
2799
|
+
sourceQuery: {
|
|
2800
|
+
type: "string",
|
|
2801
|
+
description: "The `name` of the run_sql call whose results this chart plots. Lets the UI link the chart back to its source query."
|
|
2750
2802
|
}
|
|
2751
2803
|
}
|
|
2752
2804
|
},
|
|
@@ -2779,7 +2831,8 @@ var renderChartTool = {
|
|
|
2779
2831
|
x: input.x,
|
|
2780
2832
|
series,
|
|
2781
2833
|
data,
|
|
2782
|
-
...typeof input.title === "string" && input.title.trim() !== "" ? { title: input.title } : {}
|
|
2834
|
+
...typeof input.title === "string" && input.title.trim() !== "" ? { title: input.title } : {},
|
|
2835
|
+
...typeof input.sourceQuery === "string" && input.sourceQuery.trim() !== "" ? { sourceQuery: input.sourceQuery.trim() } : {}
|
|
2783
2836
|
};
|
|
2784
2837
|
const seriesLabel = series.map((s) => s.label ?? s.key).join(", ");
|
|
2785
2838
|
return {
|
|
@@ -2888,7 +2941,10 @@ async function runTurn(opts) {
|
|
|
2888
2941
|
}
|
|
2889
2942
|
const system = buildSystemPrompt(session);
|
|
2890
2943
|
const tools = anthropicToolDefs({
|
|
2891
|
-
|
|
2944
|
+
// Quick-edit always keeps write_sql — it's the mode's only deliverable.
|
|
2945
|
+
// Without a worksheet bound the tool skips the draft write and streams
|
|
2946
|
+
// the SQL back to the launching editor via the sql_written event.
|
|
2947
|
+
worksheetBound: !!session.meta.worksheetSlug || session.meta.mode === "quick-edit",
|
|
2892
2948
|
mode: session.meta.mode
|
|
2893
2949
|
});
|
|
2894
2950
|
for (let step = 0; step < MAX_STEPS; step += 1) {
|
|
@@ -3013,7 +3069,8 @@ async function runTurn(opts) {
|
|
|
3013
3069
|
toolUseId: block.id,
|
|
3014
3070
|
name: tool.name,
|
|
3015
3071
|
ok: !execution.isError,
|
|
3016
|
-
summary: execution.uiSummary
|
|
3072
|
+
summary: execution.uiSummary,
|
|
3073
|
+
...execution.detail ? { detail: execution.detail } : {}
|
|
3017
3074
|
});
|
|
3018
3075
|
resultBlocks.push({
|
|
3019
3076
|
type: "tool_result",
|
|
@@ -3363,10 +3420,201 @@ app7.put("/:name", async (c) => {
|
|
|
3363
3420
|
});
|
|
3364
3421
|
var context_default = app7;
|
|
3365
3422
|
|
|
3423
|
+
// server/api/dashboards.ts
|
|
3424
|
+
import { Hono as Hono10 } from "hono";
|
|
3425
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
3426
|
+
import { promises as fs18 } from "node:fs";
|
|
3427
|
+
var app8 = new Hono10();
|
|
3428
|
+
var MAX_NAME_LEN2 = 80;
|
|
3429
|
+
var MAX_TITLE_LEN = 120;
|
|
3430
|
+
async function readDashboard(slug) {
|
|
3431
|
+
try {
|
|
3432
|
+
const raw = await fs18.readFile(paths.dashboard(slug), "utf8");
|
|
3433
|
+
const parsed = JSON.parse(raw);
|
|
3434
|
+
return normalize2(slug, parsed);
|
|
3435
|
+
} catch {
|
|
3436
|
+
return null;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
function normalize2(slug, d) {
|
|
3440
|
+
return {
|
|
3441
|
+
slug,
|
|
3442
|
+
name: typeof d.name === "string" && d.name.trim() !== "" ? d.name : slug,
|
|
3443
|
+
createdAt: d.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3444
|
+
updatedAt: d.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3445
|
+
charts: (Array.isArray(d.charts) ? d.charts : []).flatMap((chart, i) => {
|
|
3446
|
+
const parsed = parseChart(chart);
|
|
3447
|
+
if (typeof parsed === "string") return [];
|
|
3448
|
+
const c = chart;
|
|
3449
|
+
return [
|
|
3450
|
+
{
|
|
3451
|
+
...parsed,
|
|
3452
|
+
id: typeof c.id === "string" && c.id !== "" ? c.id : deriveChartId(slug, i, chart),
|
|
3453
|
+
position: typeof c.position === "number" ? c.position : i
|
|
3454
|
+
}
|
|
3455
|
+
];
|
|
3456
|
+
})
|
|
3457
|
+
};
|
|
3458
|
+
}
|
|
3459
|
+
function deriveChartId(slug, index, chart) {
|
|
3460
|
+
const hash = createHash2("sha256").update(`${slug}
|
|
3461
|
+
${index}
|
|
3462
|
+
${JSON.stringify(chart)}`).digest("hex");
|
|
3463
|
+
return `gen-${hash.slice(0, 12)}`;
|
|
3464
|
+
}
|
|
3465
|
+
async function persist2(dashboard) {
|
|
3466
|
+
dashboard.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3467
|
+
await writeAtomic(paths.dashboard(dashboard.slug), JSON.stringify(dashboard, null, 2));
|
|
3468
|
+
return dashboard;
|
|
3469
|
+
}
|
|
3470
|
+
async function listSlugs() {
|
|
3471
|
+
const entries = await fs18.readdir(paths.dashboards()).catch(() => []);
|
|
3472
|
+
return entries.filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
|
|
3473
|
+
}
|
|
3474
|
+
var mutationTails = /* @__PURE__ */ new Map();
|
|
3475
|
+
function withDashboardLock(slug, fn) {
|
|
3476
|
+
const prev = mutationTails.get(slug) ?? Promise.resolve();
|
|
3477
|
+
const result = prev.then(fn);
|
|
3478
|
+
const tail = result.then(
|
|
3479
|
+
() => void 0,
|
|
3480
|
+
() => void 0
|
|
3481
|
+
);
|
|
3482
|
+
mutationTails.set(slug, tail);
|
|
3483
|
+
void tail.then(() => {
|
|
3484
|
+
if (mutationTails.get(slug) === tail) mutationTails.delete(slug);
|
|
3485
|
+
});
|
|
3486
|
+
return result;
|
|
3487
|
+
}
|
|
3488
|
+
function parseChart(raw) {
|
|
3489
|
+
if (!raw || typeof raw !== "object") return "chart must be an object";
|
|
3490
|
+
const c = raw;
|
|
3491
|
+
if (!CHART_TYPES.includes(c.type)) {
|
|
3492
|
+
return `type must be one of: ${CHART_TYPES.join(", ")}`;
|
|
3493
|
+
}
|
|
3494
|
+
if (typeof c.x !== "string" || c.x.trim() === "") return "x must be a non-empty string";
|
|
3495
|
+
if (typeof c.sql !== "string" || c.sql.trim() === "") return "sql must be a non-empty string";
|
|
3496
|
+
if (!Array.isArray(c.series) || c.series.length === 0) {
|
|
3497
|
+
return "series must be a non-empty array of { key, label? }";
|
|
3498
|
+
}
|
|
3499
|
+
const series = [];
|
|
3500
|
+
for (const item of c.series) {
|
|
3501
|
+
if (!item || typeof item !== "object" || typeof item.key !== "string") {
|
|
3502
|
+
return "each series must be an object with a string `key`";
|
|
3503
|
+
}
|
|
3504
|
+
const s = item;
|
|
3505
|
+
series.push(typeof s.label === "string" && s.label !== "" ? { key: s.key, label: s.label } : { key: s.key });
|
|
3506
|
+
}
|
|
3507
|
+
const title = typeof c.title === "string" && c.title.trim() !== "" ? c.title.trim() : "Untitled chart";
|
|
3508
|
+
const connectionId = typeof c.connectionId === "string" && c.connectionId !== "" ? c.connectionId : null;
|
|
3509
|
+
return {
|
|
3510
|
+
title: title.length > MAX_TITLE_LEN ? title.slice(0, MAX_TITLE_LEN) : title,
|
|
3511
|
+
type: c.type,
|
|
3512
|
+
x: c.x,
|
|
3513
|
+
series,
|
|
3514
|
+
sql: c.sql,
|
|
3515
|
+
connectionId
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
app8.get("/", async (c) => {
|
|
3519
|
+
const slugs = await listSlugs();
|
|
3520
|
+
const metas = [];
|
|
3521
|
+
for (const slug of slugs) {
|
|
3522
|
+
const d = await readDashboard(slug);
|
|
3523
|
+
if (!d) continue;
|
|
3524
|
+
metas.push({ slug, name: d.name, updatedAt: d.updatedAt, chartCount: d.charts.length });
|
|
3525
|
+
}
|
|
3526
|
+
metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
3527
|
+
return c.json({ dashboards: metas });
|
|
3528
|
+
});
|
|
3529
|
+
app8.post("/", async (c) => {
|
|
3530
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3531
|
+
const raw = (body.name ?? "").trim();
|
|
3532
|
+
const name = raw === "" ? "Dashboard" : raw.length > MAX_NAME_LEN2 ? raw.slice(0, MAX_NAME_LEN2) : raw;
|
|
3533
|
+
const existing = new Set(await listSlugs());
|
|
3534
|
+
const slug = dedupeSlug(slugify(name, "dashboard"), existing);
|
|
3535
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3536
|
+
const dashboard = { slug, name, createdAt: now, updatedAt: now, charts: [] };
|
|
3537
|
+
await writeAtomic(paths.dashboard(slug), JSON.stringify(dashboard, null, 2));
|
|
3538
|
+
return c.json(dashboard, 201);
|
|
3539
|
+
});
|
|
3540
|
+
app8.get("/:slug", async (c) => {
|
|
3541
|
+
const slug = c.req.param("slug");
|
|
3542
|
+
assertSafeSlug(slug);
|
|
3543
|
+
const dashboard = await readDashboard(slug);
|
|
3544
|
+
if (!dashboard) return c.json({ error: "not_found" }, 404);
|
|
3545
|
+
return c.json(dashboard);
|
|
3546
|
+
});
|
|
3547
|
+
app8.patch("/:slug", async (c) => {
|
|
3548
|
+
const slug = c.req.param("slug");
|
|
3549
|
+
assertSafeSlug(slug);
|
|
3550
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3551
|
+
const raw = (body.name ?? "").trim();
|
|
3552
|
+
if (!raw) return c.json({ error: "empty_name" }, 400);
|
|
3553
|
+
return withDashboardLock(slug, async () => {
|
|
3554
|
+
const dashboard = await readDashboard(slug);
|
|
3555
|
+
if (!dashboard) return c.json({ error: "not_found" }, 404);
|
|
3556
|
+
dashboard.name = raw.length > MAX_NAME_LEN2 ? raw.slice(0, MAX_NAME_LEN2) : raw;
|
|
3557
|
+
return c.json(await persist2(dashboard));
|
|
3558
|
+
});
|
|
3559
|
+
});
|
|
3560
|
+
app8.delete("/:slug", async (c) => {
|
|
3561
|
+
const slug = c.req.param("slug");
|
|
3562
|
+
assertSafeSlug(slug);
|
|
3563
|
+
return withDashboardLock(slug, async () => {
|
|
3564
|
+
await fs18.rm(paths.dashboard(slug), { force: true });
|
|
3565
|
+
return c.json({ ok: true });
|
|
3566
|
+
});
|
|
3567
|
+
});
|
|
3568
|
+
app8.post("/:slug/charts", async (c) => {
|
|
3569
|
+
const slug = c.req.param("slug");
|
|
3570
|
+
assertSafeSlug(slug);
|
|
3571
|
+
const body = await c.req.json().catch(() => null);
|
|
3572
|
+
const parsed = parseChart(body);
|
|
3573
|
+
if (typeof parsed === "string") return c.json({ error: parsed }, 400);
|
|
3574
|
+
return withDashboardLock(slug, async () => {
|
|
3575
|
+
const dashboard = await readDashboard(slug);
|
|
3576
|
+
if (!dashboard) return c.json({ error: "not_found" }, 404);
|
|
3577
|
+
const position = dashboard.charts.reduce((max, ch) => Math.max(max, ch.position), -1) + 1;
|
|
3578
|
+
const chart = { ...parsed, id: randomUUID(), position };
|
|
3579
|
+
dashboard.charts.push(chart);
|
|
3580
|
+
return c.json(await persist2(dashboard));
|
|
3581
|
+
});
|
|
3582
|
+
});
|
|
3583
|
+
app8.put("/:slug/charts/:chartId", async (c) => {
|
|
3584
|
+
const slug = c.req.param("slug");
|
|
3585
|
+
assertSafeSlug(slug);
|
|
3586
|
+
const chartId = c.req.param("chartId");
|
|
3587
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3588
|
+
return withDashboardLock(slug, async () => {
|
|
3589
|
+
const dashboard = await readDashboard(slug);
|
|
3590
|
+
if (!dashboard) return c.json({ error: "not_found" }, 404);
|
|
3591
|
+
const chart = dashboard.charts.find((ch) => ch.id === chartId);
|
|
3592
|
+
if (!chart) return c.json({ error: "not_found" }, 404);
|
|
3593
|
+
const parsed = parseChart({ ...chart, ...body });
|
|
3594
|
+
if (typeof parsed === "string") return c.json({ error: parsed }, 400);
|
|
3595
|
+
Object.assign(chart, parsed);
|
|
3596
|
+
return c.json(await persist2(dashboard));
|
|
3597
|
+
});
|
|
3598
|
+
});
|
|
3599
|
+
app8.delete("/:slug/charts/:chartId", async (c) => {
|
|
3600
|
+
const slug = c.req.param("slug");
|
|
3601
|
+
assertSafeSlug(slug);
|
|
3602
|
+
const chartId = c.req.param("chartId");
|
|
3603
|
+
return withDashboardLock(slug, async () => {
|
|
3604
|
+
const dashboard = await readDashboard(slug);
|
|
3605
|
+
if (!dashboard) return c.json({ error: "not_found" }, 404);
|
|
3606
|
+
const before = dashboard.charts.length;
|
|
3607
|
+
dashboard.charts = dashboard.charts.filter((ch) => ch.id !== chartId);
|
|
3608
|
+
if (dashboard.charts.length === before) return c.json({ error: "not_found" }, 404);
|
|
3609
|
+
return c.json(await persist2(dashboard));
|
|
3610
|
+
});
|
|
3611
|
+
});
|
|
3612
|
+
var dashboards_default = app8;
|
|
3613
|
+
|
|
3366
3614
|
// server/history/watcher.ts
|
|
3367
|
-
import { promises as
|
|
3615
|
+
import { promises as fs19, watch } from "node:fs";
|
|
3368
3616
|
import path10 from "node:path";
|
|
3369
|
-
import { createHash as
|
|
3617
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3370
3618
|
var SLUG_RE2 = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
3371
3619
|
var watcher = null;
|
|
3372
3620
|
function startWorksheetsWatcher() {
|
|
@@ -3385,11 +3633,11 @@ function startWorksheetsWatcher() {
|
|
|
3385
3633
|
async function handleEvent(slug, filePath2) {
|
|
3386
3634
|
let content;
|
|
3387
3635
|
try {
|
|
3388
|
-
content = await
|
|
3636
|
+
content = await fs19.readFile(filePath2, "utf8");
|
|
3389
3637
|
} catch {
|
|
3390
3638
|
return;
|
|
3391
3639
|
}
|
|
3392
|
-
const sha =
|
|
3640
|
+
const sha = createHash3("sha256").update(content, "utf8").digest("hex");
|
|
3393
3641
|
if (isRecentWrite(slug, sha)) return;
|
|
3394
3642
|
recordHistory({ worksheet: slug, source: "external", content });
|
|
3395
3643
|
}
|
|
@@ -3431,10 +3679,10 @@ function contentType(file) {
|
|
|
3431
3679
|
function cacheControl(urlPath) {
|
|
3432
3680
|
return urlPath.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache";
|
|
3433
3681
|
}
|
|
3434
|
-
function serveClient(
|
|
3682
|
+
function serveClient(app9, clientDir = defaultClientDir()) {
|
|
3435
3683
|
if (!existsSync(path11.join(clientDir, "index.html"))) return false;
|
|
3436
3684
|
const indexHtml = path11.join(clientDir, "index.html");
|
|
3437
|
-
|
|
3685
|
+
app9.get("*", async (c) => {
|
|
3438
3686
|
if (c.req.path.startsWith("/api")) return c.notFound();
|
|
3439
3687
|
const rel = decodeURIComponent(c.req.path).replace(/^\/+/, "");
|
|
3440
3688
|
const candidate = path11.resolve(clientDir, rel);
|
|
@@ -3478,11 +3726,11 @@ function openBrowser(url) {
|
|
|
3478
3726
|
|
|
3479
3727
|
// server/index.ts
|
|
3480
3728
|
var DEFAULT_PORT = 3756;
|
|
3481
|
-
function listen(
|
|
3729
|
+
function listen(app9, port) {
|
|
3482
3730
|
return new Promise((resolve, reject) => {
|
|
3483
3731
|
let retried = false;
|
|
3484
3732
|
const attempt = (p) => {
|
|
3485
|
-
const server = serve({ fetch:
|
|
3733
|
+
const server = serve({ fetch: app9.fetch, port: p, hostname: "127.0.0.1" });
|
|
3486
3734
|
server.once("listening", () => resolve(server));
|
|
3487
3735
|
server.once("error", (err) => {
|
|
3488
3736
|
if (err.code === "EADDRINUSE" && !retried) {
|
|
@@ -3502,25 +3750,26 @@ async function startServer(argv = process.argv.slice(2)) {
|
|
|
3502
3750
|
openHistoryDb();
|
|
3503
3751
|
startWorksheetsWatcher();
|
|
3504
3752
|
void autoConnectAll(workspace);
|
|
3505
|
-
const
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3753
|
+
const app9 = new Hono11();
|
|
3754
|
+
app9.get("/api/health", (c) => c.json({ ok: true, workspace }));
|
|
3755
|
+
app9.route("/api/connections", connectionsRouter(workspace));
|
|
3756
|
+
app9.route("/api/ai-providers", aiProvidersRouter(workspace));
|
|
3757
|
+
app9.route("/api/worksheets", worksheets_default);
|
|
3758
|
+
app9.route("/api/drafts", drafts_default);
|
|
3759
|
+
app9.route("/api/session", session_default);
|
|
3760
|
+
app9.route("/api/schema", schema_default);
|
|
3761
|
+
app9.route("/api/history", history_default);
|
|
3762
|
+
app9.route("/api/agent", agent_default);
|
|
3763
|
+
app9.route("/api/context", context_default);
|
|
3764
|
+
app9.route("/api/dashboards", dashboards_default);
|
|
3765
|
+
app9.onError((err, c) => {
|
|
3517
3766
|
if (err instanceof HTTPException4) return err.getResponse();
|
|
3518
3767
|
console.error("[os-dpt]", err);
|
|
3519
3768
|
return c.json({ error: err.message }, 500);
|
|
3520
3769
|
});
|
|
3521
|
-
serveClient(
|
|
3770
|
+
serveClient(app9);
|
|
3522
3771
|
const preferredPort = Number(process.env.PORT ?? DEFAULT_PORT);
|
|
3523
|
-
const server = await listen(
|
|
3772
|
+
const server = await listen(app9, preferredPort);
|
|
3524
3773
|
const addr = server.address();
|
|
3525
3774
|
const port = typeof addr === "object" && addr ? addr.port : preferredPort;
|
|
3526
3775
|
const url = `http://127.0.0.1:${port}`;
|