stagent 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/dist/cli.js +272 -1
- package/docs/.coverage-gaps.json +66 -16
- package/docs/.last-generated +1 -1
- package/docs/features/dashboard-kanban.md +13 -7
- package/docs/features/settings.md +15 -3
- package/docs/features/tables.md +122 -0
- package/docs/index.md +3 -2
- package/docs/journeys/developer.md +26 -16
- package/docs/journeys/personal-use.md +23 -9
- package/docs/journeys/power-user.md +40 -14
- package/docs/journeys/work-use.md +43 -15
- package/docs/manifest.json +27 -17
- package/package.json +3 -2
- package/src/app/api/chat/entities/search/route.ts +12 -3
- package/src/app/api/documents/[id]/route.ts +5 -1
- package/src/app/api/documents/[id]/versions/route.ts +53 -0
- package/src/app/api/documents/route.ts +5 -1
- package/src/app/api/projects/[id]/documents/route.ts +124 -0
- package/src/app/api/projects/[id]/route.ts +72 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
- package/src/app/api/snapshots/[id]/route.ts +44 -0
- package/src/app/api/snapshots/route.ts +54 -0
- package/src/app/api/snapshots/settings/route.ts +67 -0
- package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
- package/src/app/api/tables/[id]/charts/route.ts +72 -0
- package/src/app/api/tables/[id]/columns/route.ts +70 -0
- package/src/app/api/tables/[id]/export/route.ts +94 -0
- package/src/app/api/tables/[id]/history/route.ts +15 -0
- package/src/app/api/tables/[id]/import/route.ts +111 -0
- package/src/app/api/tables/[id]/route.ts +86 -0
- package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
- package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
- package/src/app/api/tables/[id]/rows/route.ts +101 -0
- package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
- package/src/app/api/tables/[id]/triggers/route.ts +122 -0
- package/src/app/api/tables/route.ts +65 -0
- package/src/app/api/tables/templates/route.ts +92 -0
- package/src/app/api/tasks/[id]/route.ts +37 -2
- package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
- package/src/app/api/tasks/route.ts +8 -9
- package/src/app/api/workflows/[id]/documents/route.ts +209 -0
- package/src/app/api/workflows/[id]/execute/route.ts +6 -2
- package/src/app/api/workflows/[id]/route.ts +16 -3
- package/src/app/api/workflows/[id]/status/route.ts +18 -2
- package/src/app/api/workflows/route.ts +13 -2
- package/src/app/documents/page.tsx +5 -1
- package/src/app/layout.tsx +0 -1
- package/src/app/manifest.ts +3 -3
- package/src/app/projects/[id]/page.tsx +62 -2
- package/src/app/settings/page.tsx +2 -0
- package/src/app/tables/[id]/page.tsx +67 -0
- package/src/app/tables/page.tsx +21 -0
- package/src/app/tables/templates/page.tsx +19 -0
- package/src/components/chat/chat-table-result.tsx +139 -0
- package/src/components/documents/document-browser.tsx +1 -1
- package/src/components/documents/document-chip-bar.tsx +17 -1
- package/src/components/documents/document-detail-view.tsx +51 -0
- package/src/components/documents/document-grid.tsx +5 -0
- package/src/components/documents/document-table.tsx +4 -0
- package/src/components/documents/types.ts +3 -0
- package/src/components/projects/project-form-sheet.tsx +109 -2
- package/src/components/schedules/schedule-form.tsx +91 -1
- package/src/components/settings/data-management-section.tsx +17 -12
- package/src/components/settings/database-snapshots-section.tsx +469 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/document-picker-sheet.tsx +486 -0
- package/src/components/tables/table-browser.tsx +234 -0
- package/src/components/tables/table-cell-editor.tsx +226 -0
- package/src/components/tables/table-chart-builder.tsx +288 -0
- package/src/components/tables/table-chart-view.tsx +146 -0
- package/src/components/tables/table-column-header.tsx +103 -0
- package/src/components/tables/table-column-sheet.tsx +331 -0
- package/src/components/tables/table-create-sheet.tsx +240 -0
- package/src/components/tables/table-detail-sheet.tsx +144 -0
- package/src/components/tables/table-detail-tabs.tsx +278 -0
- package/src/components/tables/table-grid.tsx +61 -0
- package/src/components/tables/table-history-tab.tsx +148 -0
- package/src/components/tables/table-import-wizard.tsx +542 -0
- package/src/components/tables/table-list-table.tsx +95 -0
- package/src/components/tables/table-relation-combobox.tsx +217 -0
- package/src/components/tables/table-spreadsheet.tsx +499 -0
- package/src/components/tables/table-template-gallery.tsx +162 -0
- package/src/components/tables/table-template-preview.tsx +219 -0
- package/src/components/tables/table-toolbar.tsx +79 -0
- package/src/components/tables/table-triggers-tab.tsx +446 -0
- package/src/components/tables/types.ts +6 -0
- package/src/components/tables/use-spreadsheet-keys.ts +171 -0
- package/src/components/tables/utils.ts +29 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +111 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +103 -2
- package/src/components/workflows/workflow-form-view.tsx +207 -7
- package/src/components/workflows/workflow-kanban-card.tsx +8 -1
- package/src/components/workflows/workflow-list.tsx +90 -45
- package/src/components/workflows/workflow-status-view.tsx +168 -23
- package/src/instrumentation.ts +3 -0
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
- package/src/lib/agents/claude-agent.ts +3 -1
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
- package/src/lib/book/chapter-generator.ts +4 -19
- package/src/lib/book/chapter-mapping.ts +17 -0
- package/src/lib/book/content.ts +5 -16
- package/src/lib/book/update-detector.ts +3 -16
- package/src/lib/chat/engine.ts +1 -0
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +35 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/constants/table-status.ts +68 -0
- package/src/lib/data/__tests__/clear.test.ts +1 -1
- package/src/lib/data/clear.ts +57 -0
- package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
- package/src/lib/data/seed-data/conversations.ts +350 -42
- package/src/lib/data/seed-data/documents.ts +564 -591
- package/src/lib/data/seed-data/learned-context.ts +101 -22
- package/src/lib/data/seed-data/notifications.ts +344 -70
- package/src/lib/data/seed-data/profile-test-results.ts +92 -11
- package/src/lib/data/seed-data/profiles.ts +144 -46
- package/src/lib/data/seed-data/projects.ts +50 -18
- package/src/lib/data/seed-data/repo-imports.ts +28 -13
- package/src/lib/data/seed-data/schedules.ts +208 -41
- package/src/lib/data/seed-data/table-templates.ts +234 -0
- package/src/lib/data/seed-data/tasks.ts +614 -116
- package/src/lib/data/seed-data/usage-ledger.ts +182 -103
- package/src/lib/data/seed-data/user-tables.ts +203 -0
- package/src/lib/data/seed-data/views.ts +52 -7
- package/src/lib/data/seed-data/workflows.ts +231 -84
- package/src/lib/data/seed.ts +55 -14
- package/src/lib/data/tables.ts +417 -0
- package/src/lib/db/bootstrap.ts +275 -0
- package/src/lib/db/index.ts +9 -0
- package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
- package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
- package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
- package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
- package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
- package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
- package/src/lib/db/schema.ts +445 -0
- package/src/lib/docs/reader.ts +2 -3
- package/src/lib/documents/context-builder.ts +75 -2
- package/src/lib/documents/document-resolver.ts +119 -0
- package/src/lib/documents/processors/spreadsheet.ts +2 -1
- package/src/lib/schedules/scheduler.ts +31 -1
- package/src/lib/snapshots/auto-backup.ts +132 -0
- package/src/lib/snapshots/retention.ts +64 -0
- package/src/lib/snapshots/snapshot-manager.ts +429 -0
- package/src/lib/tables/computed.ts +61 -0
- package/src/lib/tables/context-builder.ts +139 -0
- package/src/lib/tables/formula-engine.ts +415 -0
- package/src/lib/tables/history.ts +115 -0
- package/src/lib/tables/import.ts +343 -0
- package/src/lib/tables/query-builder.ts +152 -0
- package/src/lib/tables/trigger-evaluator.ts +146 -0
- package/src/lib/tables/types.ts +141 -0
- package/src/lib/tables/validation.ts +119 -0
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/utils/stagent-paths.ts +20 -0
- package/src/lib/validators/__tests__/task.test.ts +43 -10
- package/src/lib/validators/task.ts +7 -1
- package/src/lib/workflows/blueprints/registry.ts +3 -3
- package/src/lib/workflows/engine.ts +24 -8
- package/src/lib/workflows/types.ts +14 -0
- package/tsconfig.json +3 -1
- package/public/icon.svg +0 -13
- package/src/components/tasks/file-upload.tsx +0 -120
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import { defineTool } from "../tool-registry";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ok, err, type ToolContext } from "./helpers";
|
|
4
|
+
import {
|
|
5
|
+
listTables,
|
|
6
|
+
getTable,
|
|
7
|
+
createTable,
|
|
8
|
+
updateTable,
|
|
9
|
+
deleteTable,
|
|
10
|
+
listRows,
|
|
11
|
+
addRows,
|
|
12
|
+
updateRow,
|
|
13
|
+
deleteRows,
|
|
14
|
+
listTemplates,
|
|
15
|
+
cloneFromTemplate,
|
|
16
|
+
addColumn,
|
|
17
|
+
updateColumn,
|
|
18
|
+
deleteColumn,
|
|
19
|
+
reorderColumns,
|
|
20
|
+
} from "@/lib/data/tables";
|
|
21
|
+
import { getTableHistory } from "@/lib/tables/history";
|
|
22
|
+
import {
|
|
23
|
+
extractStructuredData,
|
|
24
|
+
inferColumnTypes,
|
|
25
|
+
importRows,
|
|
26
|
+
createImportRecord,
|
|
27
|
+
} from "@/lib/tables/import";
|
|
28
|
+
import type { ColumnDef } from "@/lib/tables/types";
|
|
29
|
+
|
|
30
|
+
export function tableTools(ctx: ToolContext) {
|
|
31
|
+
return [
|
|
32
|
+
// ── Read operations ──────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
defineTool(
|
|
35
|
+
"list_tables",
|
|
36
|
+
"List all user-defined tables, optionally filtered by project. Returns table name, description, column count, row count, and source.",
|
|
37
|
+
{
|
|
38
|
+
projectId: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Filter by project ID. Omit to use active project."),
|
|
42
|
+
source: z
|
|
43
|
+
.enum(["manual", "imported", "agent", "template"])
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Filter by how the table was created"),
|
|
46
|
+
},
|
|
47
|
+
async (args) => {
|
|
48
|
+
try {
|
|
49
|
+
const effectiveProjectId = args.projectId ?? ctx.projectId ?? undefined;
|
|
50
|
+
const tables = await listTables({
|
|
51
|
+
projectId: effectiveProjectId,
|
|
52
|
+
source: args.source,
|
|
53
|
+
});
|
|
54
|
+
return ok(
|
|
55
|
+
tables.map((t) => ({
|
|
56
|
+
id: t.id,
|
|
57
|
+
name: t.name,
|
|
58
|
+
description: t.description,
|
|
59
|
+
projectName: t.projectName,
|
|
60
|
+
columnCount: t.columnCount,
|
|
61
|
+
rowCount: t.rowCount,
|
|
62
|
+
source: t.source,
|
|
63
|
+
}))
|
|
64
|
+
);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return err(e instanceof Error ? e.message : "Failed to list tables");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
|
|
71
|
+
defineTool(
|
|
72
|
+
"get_table_schema",
|
|
73
|
+
"Get the full schema of a table including all column definitions, types, and configurations.",
|
|
74
|
+
{
|
|
75
|
+
tableId: z.string().describe("The table ID to inspect"),
|
|
76
|
+
},
|
|
77
|
+
async (args) => {
|
|
78
|
+
try {
|
|
79
|
+
const table = await getTable(args.tableId);
|
|
80
|
+
if (!table) return err("Table not found");
|
|
81
|
+
const columns = JSON.parse(table.columnSchema) as ColumnDef[];
|
|
82
|
+
return ok({
|
|
83
|
+
id: table.id,
|
|
84
|
+
name: table.name,
|
|
85
|
+
description: table.description,
|
|
86
|
+
rowCount: table.rowCount,
|
|
87
|
+
columns: columns.map((c) => ({
|
|
88
|
+
name: c.name,
|
|
89
|
+
displayName: c.displayName,
|
|
90
|
+
dataType: c.dataType,
|
|
91
|
+
required: c.required,
|
|
92
|
+
config: c.config,
|
|
93
|
+
})),
|
|
94
|
+
});
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return err(e instanceof Error ? e.message : "Failed to get table schema");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
),
|
|
100
|
+
|
|
101
|
+
defineTool(
|
|
102
|
+
"query_table",
|
|
103
|
+
"Query rows from a table with optional filters and sorting. Filters use structured operators (eq, neq, gt, gte, lt, lte, contains, starts_with, in, is_empty, is_not_empty).",
|
|
104
|
+
{
|
|
105
|
+
tableId: z.string().describe("Table ID to query"),
|
|
106
|
+
filters: z
|
|
107
|
+
.array(
|
|
108
|
+
z.object({
|
|
109
|
+
column: z.string(),
|
|
110
|
+
operator: z.enum([
|
|
111
|
+
"eq", "neq", "gt", "gte", "lt", "lte",
|
|
112
|
+
"contains", "starts_with", "in", "is_empty", "is_not_empty",
|
|
113
|
+
]),
|
|
114
|
+
value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]).optional(),
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Filter conditions"),
|
|
119
|
+
sorts: z
|
|
120
|
+
.array(z.object({ column: z.string(), direction: z.enum(["asc", "desc"]) }))
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Sort order"),
|
|
123
|
+
limit: z.number().min(1).max(500).optional().describe("Max rows to return (default 100)"),
|
|
124
|
+
},
|
|
125
|
+
async (args) => {
|
|
126
|
+
try {
|
|
127
|
+
const rows = await listRows(args.tableId, {
|
|
128
|
+
filters: args.filters,
|
|
129
|
+
sorts: args.sorts,
|
|
130
|
+
limit: args.limit,
|
|
131
|
+
});
|
|
132
|
+
return ok(
|
|
133
|
+
rows.map((r) => ({
|
|
134
|
+
id: r.id,
|
|
135
|
+
data: JSON.parse(r.data),
|
|
136
|
+
position: r.position,
|
|
137
|
+
}))
|
|
138
|
+
);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return err(e instanceof Error ? e.message : "Failed to query table");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
),
|
|
144
|
+
|
|
145
|
+
defineTool(
|
|
146
|
+
"search_table",
|
|
147
|
+
"Search table rows for text matching a query across all text columns.",
|
|
148
|
+
{
|
|
149
|
+
tableId: z.string().describe("Table ID to search"),
|
|
150
|
+
query: z.string().describe("Search text"),
|
|
151
|
+
limit: z.number().min(1).max(100).optional().describe("Max results (default 20)"),
|
|
152
|
+
},
|
|
153
|
+
async (args) => {
|
|
154
|
+
try {
|
|
155
|
+
const table = await getTable(args.tableId);
|
|
156
|
+
if (!table) return err("Table not found");
|
|
157
|
+
const columns = JSON.parse(table.columnSchema) as ColumnDef[];
|
|
158
|
+
const textCols = columns.filter((c) =>
|
|
159
|
+
["text", "email", "url"].includes(c.dataType)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (textCols.length === 0) return ok([]);
|
|
163
|
+
|
|
164
|
+
// Search using contains filter on first text column (basic approach)
|
|
165
|
+
const rows = await listRows(args.tableId, {
|
|
166
|
+
filters: [{ column: textCols[0].name, operator: "contains", value: args.query }],
|
|
167
|
+
limit: args.limit ?? 20,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return ok(
|
|
171
|
+
rows.map((r) => ({
|
|
172
|
+
id: r.id,
|
|
173
|
+
data: JSON.parse(r.data),
|
|
174
|
+
}))
|
|
175
|
+
);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
return err(e instanceof Error ? e.message : "Failed to search table");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
),
|
|
181
|
+
|
|
182
|
+
defineTool(
|
|
183
|
+
"aggregate_table",
|
|
184
|
+
"Compute aggregate values (sum, avg, count, min, max) on a numeric column, with optional filters.",
|
|
185
|
+
{
|
|
186
|
+
tableId: z.string().describe("Table ID"),
|
|
187
|
+
column: z.string().describe("Column name to aggregate (must be numeric)"),
|
|
188
|
+
operation: z.enum(["sum", "avg", "count", "min", "max"]).describe("Aggregation operation"),
|
|
189
|
+
filters: z
|
|
190
|
+
.array(
|
|
191
|
+
z.object({
|
|
192
|
+
column: z.string(),
|
|
193
|
+
operator: z.enum([
|
|
194
|
+
"eq", "neq", "gt", "gte", "lt", "lte",
|
|
195
|
+
"contains", "starts_with", "in", "is_empty", "is_not_empty",
|
|
196
|
+
]),
|
|
197
|
+
value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]).optional(),
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
.optional(),
|
|
201
|
+
},
|
|
202
|
+
async (args) => {
|
|
203
|
+
try {
|
|
204
|
+
const rows = await listRows(args.tableId, {
|
|
205
|
+
filters: args.filters,
|
|
206
|
+
limit: 10000,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const values = rows
|
|
210
|
+
.map((r) => {
|
|
211
|
+
const data = JSON.parse(r.data) as Record<string, unknown>;
|
|
212
|
+
return Number(data[args.column]);
|
|
213
|
+
})
|
|
214
|
+
.filter((v) => !isNaN(v));
|
|
215
|
+
|
|
216
|
+
if (values.length === 0) return ok({ result: null, count: 0 });
|
|
217
|
+
|
|
218
|
+
let result: number;
|
|
219
|
+
switch (args.operation) {
|
|
220
|
+
case "sum":
|
|
221
|
+
result = values.reduce((a, b) => a + b, 0);
|
|
222
|
+
break;
|
|
223
|
+
case "avg":
|
|
224
|
+
result = values.reduce((a, b) => a + b, 0) / values.length;
|
|
225
|
+
break;
|
|
226
|
+
case "count":
|
|
227
|
+
result = values.length;
|
|
228
|
+
break;
|
|
229
|
+
case "min":
|
|
230
|
+
result = Math.min(...values);
|
|
231
|
+
break;
|
|
232
|
+
case "max":
|
|
233
|
+
result = Math.max(...values);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return ok({ result, count: values.length, operation: args.operation, column: args.column });
|
|
238
|
+
} catch (e) {
|
|
239
|
+
return err(e instanceof Error ? e.message : "Failed to aggregate");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
),
|
|
243
|
+
|
|
244
|
+
// ── Write operations ─────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
defineTool(
|
|
247
|
+
"add_rows",
|
|
248
|
+
"Add one or more rows to a table. Each row is an object mapping column names to values.",
|
|
249
|
+
{
|
|
250
|
+
tableId: z.string().describe("Table ID"),
|
|
251
|
+
rows: z
|
|
252
|
+
.array(z.record(z.string(), z.unknown()))
|
|
253
|
+
.min(1)
|
|
254
|
+
.max(100)
|
|
255
|
+
.describe("Array of row data objects"),
|
|
256
|
+
},
|
|
257
|
+
async (args) => {
|
|
258
|
+
try {
|
|
259
|
+
const ids = await addRows(
|
|
260
|
+
args.tableId,
|
|
261
|
+
args.rows.map((data) => ({ data, createdBy: "agent" }))
|
|
262
|
+
);
|
|
263
|
+
return ok({ added: ids.length, rowIds: ids });
|
|
264
|
+
} catch (e) {
|
|
265
|
+
return err(e instanceof Error ? e.message : "Failed to add rows");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
),
|
|
269
|
+
|
|
270
|
+
defineTool(
|
|
271
|
+
"update_row",
|
|
272
|
+
"Update specific fields of a row. Only the provided fields are changed; others are preserved.",
|
|
273
|
+
{
|
|
274
|
+
rowId: z.string().describe("Row ID to update"),
|
|
275
|
+
data: z.record(z.string(), z.unknown()).describe("Fields to update"),
|
|
276
|
+
},
|
|
277
|
+
async (args) => {
|
|
278
|
+
try {
|
|
279
|
+
const row = await updateRow(args.rowId, { data: args.data });
|
|
280
|
+
if (!row) return err("Row not found");
|
|
281
|
+
return ok({ id: row.id, data: JSON.parse(row.data) });
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return err(e instanceof Error ? e.message : "Failed to update row");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
),
|
|
287
|
+
|
|
288
|
+
defineTool(
|
|
289
|
+
"delete_rows",
|
|
290
|
+
"Delete one or more rows from a table by their IDs.",
|
|
291
|
+
{
|
|
292
|
+
rowIds: z.array(z.string()).min(1).describe("Row IDs to delete"),
|
|
293
|
+
},
|
|
294
|
+
async (args) => {
|
|
295
|
+
try {
|
|
296
|
+
await deleteRows(args.rowIds);
|
|
297
|
+
return ok({ deleted: args.rowIds.length });
|
|
298
|
+
} catch (e) {
|
|
299
|
+
return err(e instanceof Error ? e.message : "Failed to delete rows");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
),
|
|
303
|
+
|
|
304
|
+
// ── Creation operations ──────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
defineTool(
|
|
307
|
+
"create_table",
|
|
308
|
+
"Create a new empty table with specified columns.",
|
|
309
|
+
{
|
|
310
|
+
name: z.string().min(1).max(256).describe("Table name"),
|
|
311
|
+
description: z.string().max(1024).optional().describe("Table description"),
|
|
312
|
+
projectId: z.string().optional().describe("Project ID. Omit for active project."),
|
|
313
|
+
columns: z
|
|
314
|
+
.array(
|
|
315
|
+
z.object({
|
|
316
|
+
name: z.string(),
|
|
317
|
+
displayName: z.string(),
|
|
318
|
+
dataType: z.enum([
|
|
319
|
+
"text", "number", "date", "boolean", "select", "url", "email",
|
|
320
|
+
]),
|
|
321
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
322
|
+
})
|
|
323
|
+
)
|
|
324
|
+
.min(1)
|
|
325
|
+
.describe("Column definitions"),
|
|
326
|
+
},
|
|
327
|
+
async (args) => {
|
|
328
|
+
try {
|
|
329
|
+
const effectiveProjectId = args.projectId ?? ctx.projectId ?? undefined;
|
|
330
|
+
const table = await createTable({
|
|
331
|
+
name: args.name,
|
|
332
|
+
description: args.description,
|
|
333
|
+
projectId: effectiveProjectId,
|
|
334
|
+
columns: args.columns.map((c, i) => ({
|
|
335
|
+
...c,
|
|
336
|
+
position: i,
|
|
337
|
+
})),
|
|
338
|
+
source: "agent",
|
|
339
|
+
});
|
|
340
|
+
return ok({ id: table.id, name: table.name });
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return err(e instanceof Error ? e.message : "Failed to create table");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
),
|
|
346
|
+
|
|
347
|
+
defineTool(
|
|
348
|
+
"import_document_as_table",
|
|
349
|
+
"Import a document (CSV, XLSX, TSV) into an existing table. Automatically detects column types from the document content.",
|
|
350
|
+
{
|
|
351
|
+
tableId: z.string().describe("Table ID to import into"),
|
|
352
|
+
documentId: z.string().describe("Document ID to import from"),
|
|
353
|
+
},
|
|
354
|
+
async (args) => {
|
|
355
|
+
try {
|
|
356
|
+
const { headers, rows } = await extractStructuredData(args.documentId);
|
|
357
|
+
const sampleRows = rows.slice(0, 100);
|
|
358
|
+
const inferredColumns = inferColumnTypes(headers, sampleRows);
|
|
359
|
+
const result = await importRows(args.tableId, rows, inferredColumns);
|
|
360
|
+
await createImportRecord(args.tableId, args.documentId, result);
|
|
361
|
+
return ok({
|
|
362
|
+
importId: result.importId,
|
|
363
|
+
rowsImported: result.rowsImported,
|
|
364
|
+
rowsSkipped: result.rowsSkipped,
|
|
365
|
+
});
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return err(e instanceof Error ? e.message : "Failed to import document");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
),
|
|
371
|
+
|
|
372
|
+
defineTool(
|
|
373
|
+
"list_table_templates",
|
|
374
|
+
"List available table templates that can be used to quickly create pre-structured tables.",
|
|
375
|
+
{
|
|
376
|
+
category: z
|
|
377
|
+
.enum(["business", "personal", "pm", "finance", "content"])
|
|
378
|
+
.optional()
|
|
379
|
+
.describe("Filter by template category"),
|
|
380
|
+
},
|
|
381
|
+
async (args) => {
|
|
382
|
+
try {
|
|
383
|
+
const templates = await listTemplates({ category: args.category });
|
|
384
|
+
return ok(
|
|
385
|
+
templates.map((t) => ({
|
|
386
|
+
id: t.id,
|
|
387
|
+
name: t.name,
|
|
388
|
+
description: t.description,
|
|
389
|
+
category: t.category,
|
|
390
|
+
}))
|
|
391
|
+
);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return err(e instanceof Error ? e.message : "Failed to list templates");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
),
|
|
397
|
+
|
|
398
|
+
defineTool(
|
|
399
|
+
"create_table_from_template",
|
|
400
|
+
"Create a new table from a template, optionally including sample data.",
|
|
401
|
+
{
|
|
402
|
+
templateId: z.string().describe("Template ID to clone from"),
|
|
403
|
+
name: z.string().min(1).max(256).describe("Name for the new table"),
|
|
404
|
+
projectId: z.string().optional().describe("Project ID. Omit for active project."),
|
|
405
|
+
includeSampleData: z.boolean().optional().describe("Whether to include sample rows"),
|
|
406
|
+
},
|
|
407
|
+
async (args) => {
|
|
408
|
+
try {
|
|
409
|
+
const effectiveProjectId = args.projectId ?? ctx.projectId ?? undefined;
|
|
410
|
+
const table = await cloneFromTemplate({
|
|
411
|
+
templateId: args.templateId,
|
|
412
|
+
name: args.name,
|
|
413
|
+
projectId: effectiveProjectId,
|
|
414
|
+
includeSampleData: args.includeSampleData,
|
|
415
|
+
});
|
|
416
|
+
return ok({ id: table.id, name: table.name });
|
|
417
|
+
} catch (e) {
|
|
418
|
+
return err(e instanceof Error ? e.message : "Failed to create from template");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
),
|
|
422
|
+
|
|
423
|
+
// ── NL-to-schema creation ──────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
defineTool(
|
|
426
|
+
"create_table_from_description",
|
|
427
|
+
`Create a table by inferring column schema from a natural language description. You (the LLM) should infer appropriate column names, data types, and constraints from the description.
|
|
428
|
+
|
|
429
|
+
Guidelines for schema inference:
|
|
430
|
+
- Use "email" type for email-like fields, "url" for URLs, "date" for dates, "number" for numeric fields, "boolean" for yes/no
|
|
431
|
+
- Use "select" type with options for fields with a known set of values (e.g., status, priority, category)
|
|
432
|
+
- Use "text" as the default for free-form text fields
|
|
433
|
+
- Always include a primary descriptive column (e.g., "name", "title") as the first column
|
|
434
|
+
- Include 5-10 columns that make sense for the described use case`,
|
|
435
|
+
{
|
|
436
|
+
description: z.string().min(3).describe("Natural language description of the table to create, e.g. 'a table for tracking job applications'"),
|
|
437
|
+
name: z.string().min(1).max(256).describe("Table name inferred from the description"),
|
|
438
|
+
columns: z.array(z.object({
|
|
439
|
+
name: z.string().describe("Machine-readable column name (snake_case)"),
|
|
440
|
+
displayName: z.string().describe("Human-readable column name"),
|
|
441
|
+
dataType: z.enum(["text", "number", "date", "boolean", "select", "url", "email"]).describe("Inferred data type"),
|
|
442
|
+
config: z.object({ options: z.array(z.string()).optional() }).optional().describe("Config for select columns"),
|
|
443
|
+
})).describe("Inferred column definitions"),
|
|
444
|
+
projectId: z.string().optional().describe("Project ID. Omit for active project."),
|
|
445
|
+
},
|
|
446
|
+
async (args) => {
|
|
447
|
+
try {
|
|
448
|
+
const effectiveProjectId = args.projectId ?? ctx.projectId ?? undefined;
|
|
449
|
+
const table = await createTable({
|
|
450
|
+
name: args.name,
|
|
451
|
+
projectId: effectiveProjectId,
|
|
452
|
+
columns: args.columns.map((c, i) => ({
|
|
453
|
+
name: c.name,
|
|
454
|
+
displayName: c.displayName,
|
|
455
|
+
dataType: c.dataType,
|
|
456
|
+
position: i,
|
|
457
|
+
config: c.config,
|
|
458
|
+
})),
|
|
459
|
+
source: "agent",
|
|
460
|
+
});
|
|
461
|
+
return ok({
|
|
462
|
+
id: table.id,
|
|
463
|
+
name: table.name,
|
|
464
|
+
columns: args.columns.length,
|
|
465
|
+
description: `Created from: "${args.description}"`,
|
|
466
|
+
});
|
|
467
|
+
} catch (e) {
|
|
468
|
+
return err(e instanceof Error ? e.message : "Failed to create table");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
),
|
|
472
|
+
|
|
473
|
+
// ── Export ──────────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
defineTool(
|
|
476
|
+
"export_table",
|
|
477
|
+
"Export a table's data as CSV, JSON, or XLSX. Returns the download URL.",
|
|
478
|
+
{
|
|
479
|
+
tableId: z.string().describe("Table ID to export"),
|
|
480
|
+
format: z.enum(["csv", "json", "xlsx"]).describe("Export format"),
|
|
481
|
+
},
|
|
482
|
+
async (args) => {
|
|
483
|
+
try {
|
|
484
|
+
const table = await getTable(args.tableId);
|
|
485
|
+
if (!table) return err("Table not found");
|
|
486
|
+
const baseUrl = process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
487
|
+
return ok({
|
|
488
|
+
url: `${baseUrl}/api/tables/${args.tableId}/export?format=${args.format}`,
|
|
489
|
+
table: table.name,
|
|
490
|
+
format: args.format,
|
|
491
|
+
});
|
|
492
|
+
} catch (e) {
|
|
493
|
+
return err(e instanceof Error ? e.message : "Failed to export");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
),
|
|
497
|
+
|
|
498
|
+
// ── Column CRUD ────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
defineTool(
|
|
501
|
+
"add_column",
|
|
502
|
+
"Add a new column to a table.",
|
|
503
|
+
{
|
|
504
|
+
tableId: z.string().describe("Table ID"),
|
|
505
|
+
name: z.string().min(1).max(64).describe("Column name (snake_case)"),
|
|
506
|
+
displayName: z.string().min(1).max(128).describe("Display name"),
|
|
507
|
+
dataType: z.enum(["text", "number", "date", "boolean", "select", "url", "email", "relation", "computed"]).describe("Column data type"),
|
|
508
|
+
required: z.boolean().optional().describe("Whether the column is required"),
|
|
509
|
+
config: z.record(z.string(), z.unknown()).optional().describe("Type-specific config (options for select, formula for computed, targetTableId for relation)"),
|
|
510
|
+
},
|
|
511
|
+
async (args) => {
|
|
512
|
+
try {
|
|
513
|
+
const col = await addColumn(args.tableId, {
|
|
514
|
+
name: args.name,
|
|
515
|
+
displayName: args.displayName,
|
|
516
|
+
dataType: args.dataType,
|
|
517
|
+
required: args.required,
|
|
518
|
+
config: args.config as ColumnDef["config"],
|
|
519
|
+
});
|
|
520
|
+
return ok({ id: col.id, name: col.name, displayName: col.displayName });
|
|
521
|
+
} catch (e) {
|
|
522
|
+
return err(e instanceof Error ? e.message : "Failed to add column");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
),
|
|
526
|
+
|
|
527
|
+
defineTool(
|
|
528
|
+
"update_column",
|
|
529
|
+
"Update an existing column's display name, type, or configuration.",
|
|
530
|
+
{
|
|
531
|
+
columnId: z.string().describe("Column ID to update"),
|
|
532
|
+
displayName: z.string().optional().describe("New display name"),
|
|
533
|
+
dataType: z.enum(["text", "number", "date", "boolean", "select", "url", "email", "relation", "computed"]).optional().describe("New data type"),
|
|
534
|
+
config: z.record(z.string(), z.unknown()).optional().describe("Updated config"),
|
|
535
|
+
},
|
|
536
|
+
async (args) => {
|
|
537
|
+
try {
|
|
538
|
+
const col = await updateColumn(args.columnId, {
|
|
539
|
+
displayName: args.displayName,
|
|
540
|
+
dataType: args.dataType,
|
|
541
|
+
config: args.config as ColumnDef["config"],
|
|
542
|
+
});
|
|
543
|
+
if (!col) return err("Column not found");
|
|
544
|
+
return ok({ id: col.id, name: col.name });
|
|
545
|
+
} catch (e) {
|
|
546
|
+
return err(e instanceof Error ? e.message : "Failed to update column");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
),
|
|
550
|
+
|
|
551
|
+
defineTool(
|
|
552
|
+
"delete_column",
|
|
553
|
+
"Delete a column from a table. This removes the column definition but preserves row data.",
|
|
554
|
+
{
|
|
555
|
+
columnId: z.string().describe("Column ID to delete"),
|
|
556
|
+
},
|
|
557
|
+
async (args) => {
|
|
558
|
+
try {
|
|
559
|
+
await deleteColumn(args.columnId);
|
|
560
|
+
return ok({ deleted: true });
|
|
561
|
+
} catch (e) {
|
|
562
|
+
return err(e instanceof Error ? e.message : "Failed to delete column");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
),
|
|
566
|
+
|
|
567
|
+
defineTool(
|
|
568
|
+
"reorder_columns",
|
|
569
|
+
"Reorder columns in a table by providing column IDs in the desired order.",
|
|
570
|
+
{
|
|
571
|
+
tableId: z.string().describe("Table ID"),
|
|
572
|
+
columnIds: z.array(z.string()).min(1).describe("Column IDs in desired order"),
|
|
573
|
+
},
|
|
574
|
+
async (args) => {
|
|
575
|
+
try {
|
|
576
|
+
await reorderColumns(args.tableId, args.columnIds);
|
|
577
|
+
return ok({ reordered: true, count: args.columnIds.length });
|
|
578
|
+
} catch (e) {
|
|
579
|
+
return err(e instanceof Error ? e.message : "Failed to reorder columns");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
),
|
|
583
|
+
|
|
584
|
+
// ── Table management ───────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
defineTool(
|
|
587
|
+
"update_table",
|
|
588
|
+
"Update a table's name or description.",
|
|
589
|
+
{
|
|
590
|
+
tableId: z.string().describe("Table ID to update"),
|
|
591
|
+
name: z.string().optional().describe("New table name"),
|
|
592
|
+
description: z.string().nullable().optional().describe("New description"),
|
|
593
|
+
},
|
|
594
|
+
async (args) => {
|
|
595
|
+
try {
|
|
596
|
+
const updated = await updateTable(args.tableId, {
|
|
597
|
+
name: args.name,
|
|
598
|
+
description: args.description,
|
|
599
|
+
});
|
|
600
|
+
if (!updated) return err("Table not found");
|
|
601
|
+
return ok({ id: updated.id, name: updated.name });
|
|
602
|
+
} catch (e) {
|
|
603
|
+
return err(e instanceof Error ? e.message : "Failed to update table");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
),
|
|
607
|
+
|
|
608
|
+
defineTool(
|
|
609
|
+
"delete_table",
|
|
610
|
+
"Permanently delete a table and all its rows, columns, views, and triggers.",
|
|
611
|
+
{
|
|
612
|
+
tableId: z.string().describe("Table ID to delete"),
|
|
613
|
+
},
|
|
614
|
+
async (args) => {
|
|
615
|
+
try {
|
|
616
|
+
await deleteTable(args.tableId);
|
|
617
|
+
return ok({ deleted: true });
|
|
618
|
+
} catch (e) {
|
|
619
|
+
return err(e instanceof Error ? e.message : "Failed to delete table");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
),
|
|
623
|
+
|
|
624
|
+
// ── Charts ─────────────────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
defineTool(
|
|
627
|
+
"list_charts",
|
|
628
|
+
"List saved chart views for a table.",
|
|
629
|
+
{
|
|
630
|
+
tableId: z.string().describe("Table ID"),
|
|
631
|
+
},
|
|
632
|
+
async (args) => {
|
|
633
|
+
try {
|
|
634
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/charts`);
|
|
635
|
+
if (!res.ok) return err("Failed to list charts");
|
|
636
|
+
return ok(await res.json());
|
|
637
|
+
} catch (e) {
|
|
638
|
+
return err(e instanceof Error ? e.message : "Failed to list charts");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
),
|
|
642
|
+
|
|
643
|
+
defineTool(
|
|
644
|
+
"create_chart",
|
|
645
|
+
"Create a chart visualization for a table. Supports bar, line, pie, and scatter chart types with aggregation.",
|
|
646
|
+
{
|
|
647
|
+
tableId: z.string().describe("Table ID"),
|
|
648
|
+
type: z.enum(["bar", "line", "pie", "scatter"]).describe("Chart type"),
|
|
649
|
+
title: z.string().min(1).describe("Chart title"),
|
|
650
|
+
xColumn: z.string().describe("Column for X axis / categories"),
|
|
651
|
+
yColumn: z.string().optional().describe("Column for Y axis / values"),
|
|
652
|
+
aggregation: z.enum(["sum", "avg", "count", "min", "max"]).optional().describe("Aggregation operation"),
|
|
653
|
+
},
|
|
654
|
+
async (args) => {
|
|
655
|
+
try {
|
|
656
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/charts`, {
|
|
657
|
+
method: "POST",
|
|
658
|
+
headers: { "Content-Type": "application/json" },
|
|
659
|
+
body: JSON.stringify({
|
|
660
|
+
type: args.type,
|
|
661
|
+
title: args.title,
|
|
662
|
+
xColumn: args.xColumn,
|
|
663
|
+
yColumn: args.yColumn,
|
|
664
|
+
aggregation: args.aggregation,
|
|
665
|
+
}),
|
|
666
|
+
});
|
|
667
|
+
if (!res.ok) return err("Failed to create chart");
|
|
668
|
+
return ok(await res.json());
|
|
669
|
+
} catch (e) {
|
|
670
|
+
return err(e instanceof Error ? e.message : "Failed to create chart");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
),
|
|
674
|
+
|
|
675
|
+
defineTool(
|
|
676
|
+
"update_chart",
|
|
677
|
+
"Update an existing chart's title, type, columns, or aggregation.",
|
|
678
|
+
{
|
|
679
|
+
tableId: z.string().describe("Table ID"),
|
|
680
|
+
chartId: z.string().describe("Chart ID to update"),
|
|
681
|
+
title: z.string().optional().describe("New chart title"),
|
|
682
|
+
type: z.enum(["bar", "line", "pie", "scatter"]).optional().describe("New chart type"),
|
|
683
|
+
xColumn: z.string().optional().describe("New X axis column"),
|
|
684
|
+
yColumn: z.string().optional().describe("New Y axis column"),
|
|
685
|
+
aggregation: z.enum(["sum", "avg", "count", "min", "max"]).optional().describe("New aggregation"),
|
|
686
|
+
},
|
|
687
|
+
async (args) => {
|
|
688
|
+
try {
|
|
689
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/charts/${args.chartId}`, {
|
|
690
|
+
method: "PATCH",
|
|
691
|
+
headers: { "Content-Type": "application/json" },
|
|
692
|
+
body: JSON.stringify({
|
|
693
|
+
title: args.title,
|
|
694
|
+
type: args.type,
|
|
695
|
+
xColumn: args.xColumn,
|
|
696
|
+
yColumn: args.yColumn,
|
|
697
|
+
aggregation: args.aggregation,
|
|
698
|
+
}),
|
|
699
|
+
});
|
|
700
|
+
if (!res.ok) return err("Failed to update chart");
|
|
701
|
+
return ok(await res.json());
|
|
702
|
+
} catch (e) {
|
|
703
|
+
return err(e instanceof Error ? e.message : "Failed to update chart");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
),
|
|
707
|
+
|
|
708
|
+
defineTool(
|
|
709
|
+
"delete_chart",
|
|
710
|
+
"Delete a chart from a table.",
|
|
711
|
+
{
|
|
712
|
+
tableId: z.string().describe("Table ID"),
|
|
713
|
+
chartId: z.string().describe("Chart ID to delete"),
|
|
714
|
+
},
|
|
715
|
+
async (args) => {
|
|
716
|
+
try {
|
|
717
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/charts/${args.chartId}`, {
|
|
718
|
+
method: "DELETE",
|
|
719
|
+
});
|
|
720
|
+
if (!res.ok && res.status !== 204) return err("Failed to delete chart");
|
|
721
|
+
return ok({ deleted: true, chartId: args.chartId });
|
|
722
|
+
} catch (e) {
|
|
723
|
+
return err(e instanceof Error ? e.message : "Failed to delete chart");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
),
|
|
727
|
+
|
|
728
|
+
defineTool(
|
|
729
|
+
"render_chart",
|
|
730
|
+
"Render a chart inline by fetching chart config and table data. Returns the chart configuration and aggregated data points for display.",
|
|
731
|
+
{
|
|
732
|
+
tableId: z.string().describe("Table ID"),
|
|
733
|
+
chartId: z.string().describe("Chart ID to render"),
|
|
734
|
+
},
|
|
735
|
+
async (args) => {
|
|
736
|
+
try {
|
|
737
|
+
// Fetch chart config
|
|
738
|
+
const chartsRes = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/charts`);
|
|
739
|
+
if (!chartsRes.ok) return err("Failed to fetch charts");
|
|
740
|
+
const charts = await chartsRes.json() as Array<{ id: string; name: string; config: { type: string; xColumn: string; yColumn?: string; aggregation?: string } }>;
|
|
741
|
+
const chart = charts.find((c: { id: string }) => c.id === args.chartId);
|
|
742
|
+
if (!chart) return err("Chart not found");
|
|
743
|
+
|
|
744
|
+
// Fetch table rows
|
|
745
|
+
const rowsRes = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/rows`);
|
|
746
|
+
if (!rowsRes.ok) return err("Failed to fetch table rows");
|
|
747
|
+
const rows = await rowsRes.json() as Array<{ data: string | Record<string, unknown> }>;
|
|
748
|
+
|
|
749
|
+
// Parse row data
|
|
750
|
+
const parsedRows = rows.map((r) => {
|
|
751
|
+
const data = typeof r.data === "string" ? JSON.parse(r.data) : r.data;
|
|
752
|
+
return data as Record<string, unknown>;
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Aggregate data
|
|
756
|
+
const { xColumn, yColumn, aggregation = "count" } = chart.config;
|
|
757
|
+
const groups = new Map<string, number[]>();
|
|
758
|
+
for (const row of parsedRows) {
|
|
759
|
+
const key = String(row[xColumn] ?? "Unknown");
|
|
760
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
761
|
+
if (yColumn && row[yColumn] != null) {
|
|
762
|
+
groups.get(key)!.push(Number(row[yColumn]));
|
|
763
|
+
} else {
|
|
764
|
+
groups.get(key)!.push(1);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const dataPoints = Array.from(groups.entries()).map(([label, values]) => {
|
|
769
|
+
let value: number;
|
|
770
|
+
switch (aggregation) {
|
|
771
|
+
case "sum": value = values.reduce((a, b) => a + b, 0); break;
|
|
772
|
+
case "avg": value = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; break;
|
|
773
|
+
case "min": value = Math.min(...values); break;
|
|
774
|
+
case "max": value = Math.max(...values); break;
|
|
775
|
+
case "count": default: value = values.length; break;
|
|
776
|
+
}
|
|
777
|
+
return { label, value: Math.round(value * 100) / 100 };
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
return ok({
|
|
781
|
+
chart: { id: chart.id, name: chart.name, type: chart.config.type },
|
|
782
|
+
xAxis: xColumn,
|
|
783
|
+
yAxis: yColumn ?? "count",
|
|
784
|
+
aggregation,
|
|
785
|
+
dataPoints,
|
|
786
|
+
});
|
|
787
|
+
} catch (e) {
|
|
788
|
+
return err(e instanceof Error ? e.message : "Failed to render chart");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
),
|
|
792
|
+
|
|
793
|
+
// ── Triggers ───────────────────────────────────────────────────────
|
|
794
|
+
|
|
795
|
+
defineTool(
|
|
796
|
+
"list_triggers",
|
|
797
|
+
"List all triggers configured for a table.",
|
|
798
|
+
{
|
|
799
|
+
tableId: z.string().describe("Table ID"),
|
|
800
|
+
},
|
|
801
|
+
async (args) => {
|
|
802
|
+
try {
|
|
803
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/triggers`);
|
|
804
|
+
if (!res.ok) return err("Failed to list triggers");
|
|
805
|
+
return ok(await res.json());
|
|
806
|
+
} catch (e) {
|
|
807
|
+
return err(e instanceof Error ? e.message : "Failed to list triggers");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
),
|
|
811
|
+
|
|
812
|
+
defineTool(
|
|
813
|
+
"create_trigger",
|
|
814
|
+
"Create a trigger that fires when table rows change. Triggers can create tasks or start workflows.",
|
|
815
|
+
{
|
|
816
|
+
tableId: z.string().describe("Table ID"),
|
|
817
|
+
name: z.string().min(1).describe("Trigger name"),
|
|
818
|
+
triggerEvent: z.enum(["row_added", "row_updated", "row_deleted"]).describe("Event that fires the trigger"),
|
|
819
|
+
condition: z.object({
|
|
820
|
+
column: z.string(),
|
|
821
|
+
operator: z.string(),
|
|
822
|
+
value: z.string(),
|
|
823
|
+
}).optional().describe("Optional condition — trigger only fires when this filter matches"),
|
|
824
|
+
actionType: z.enum(["create_task", "run_workflow"]).describe("Action to perform"),
|
|
825
|
+
actionConfig: z.record(z.string(), z.unknown()).describe("Action config: {title, description, projectId} for create_task, {workflowId} for run_workflow"),
|
|
826
|
+
},
|
|
827
|
+
async (args) => {
|
|
828
|
+
try {
|
|
829
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/triggers`, {
|
|
830
|
+
method: "POST",
|
|
831
|
+
headers: { "Content-Type": "application/json" },
|
|
832
|
+
body: JSON.stringify({
|
|
833
|
+
name: args.name,
|
|
834
|
+
triggerEvent: args.triggerEvent,
|
|
835
|
+
condition: args.condition ?? null,
|
|
836
|
+
actionType: args.actionType,
|
|
837
|
+
actionConfig: args.actionConfig,
|
|
838
|
+
}),
|
|
839
|
+
});
|
|
840
|
+
if (!res.ok) return err("Failed to create trigger");
|
|
841
|
+
return ok(await res.json());
|
|
842
|
+
} catch (e) {
|
|
843
|
+
return err(e instanceof Error ? e.message : "Failed to create trigger");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
),
|
|
847
|
+
|
|
848
|
+
defineTool(
|
|
849
|
+
"update_trigger",
|
|
850
|
+
"Update a trigger's status (active/paused) or configuration.",
|
|
851
|
+
{
|
|
852
|
+
tableId: z.string().describe("Table ID"),
|
|
853
|
+
triggerId: z.string().describe("Trigger ID to update"),
|
|
854
|
+
status: z.enum(["active", "paused"]).optional().describe("New status"),
|
|
855
|
+
name: z.string().optional().describe("New trigger name"),
|
|
856
|
+
},
|
|
857
|
+
async (args) => {
|
|
858
|
+
try {
|
|
859
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/triggers/${args.triggerId}`, {
|
|
860
|
+
method: "PATCH",
|
|
861
|
+
headers: { "Content-Type": "application/json" },
|
|
862
|
+
body: JSON.stringify({
|
|
863
|
+
status: args.status,
|
|
864
|
+
name: args.name,
|
|
865
|
+
}),
|
|
866
|
+
});
|
|
867
|
+
if (!res.ok) return err("Failed to update trigger");
|
|
868
|
+
return ok(await res.json());
|
|
869
|
+
} catch (e) {
|
|
870
|
+
return err(e instanceof Error ? e.message : "Failed to update trigger");
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
),
|
|
874
|
+
|
|
875
|
+
defineTool(
|
|
876
|
+
"delete_trigger",
|
|
877
|
+
"Delete a trigger from a table.",
|
|
878
|
+
{
|
|
879
|
+
tableId: z.string().describe("Table ID"),
|
|
880
|
+
triggerId: z.string().describe("Trigger ID to delete"),
|
|
881
|
+
},
|
|
882
|
+
async (args) => {
|
|
883
|
+
try {
|
|
884
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/${args.tableId}/triggers/${args.triggerId}`, {
|
|
885
|
+
method: "DELETE",
|
|
886
|
+
});
|
|
887
|
+
if (!res.ok) return err("Failed to delete trigger");
|
|
888
|
+
return ok({ deleted: true });
|
|
889
|
+
} catch (e) {
|
|
890
|
+
return err(e instanceof Error ? e.message : "Failed to delete trigger");
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
),
|
|
894
|
+
|
|
895
|
+
// ── History ─────────────────────────────────────────────────────────
|
|
896
|
+
|
|
897
|
+
defineTool(
|
|
898
|
+
"get_table_history",
|
|
899
|
+
"Get recent change history for a table, showing row updates and deletions with previous data snapshots.",
|
|
900
|
+
{
|
|
901
|
+
tableId: z.string().describe("Table ID"),
|
|
902
|
+
limit: z.number().int().min(1).max(200).optional().describe("Max entries to return (default 50)"),
|
|
903
|
+
},
|
|
904
|
+
async (args) => {
|
|
905
|
+
try {
|
|
906
|
+
const history = getTableHistory(args.tableId, args.limit ?? 50);
|
|
907
|
+
return ok(history.map((h) => ({
|
|
908
|
+
id: h.id,
|
|
909
|
+
rowId: h.rowId,
|
|
910
|
+
changeType: h.changeType,
|
|
911
|
+
changedBy: h.changedBy,
|
|
912
|
+
createdAt: h.createdAt,
|
|
913
|
+
})));
|
|
914
|
+
} catch (e) {
|
|
915
|
+
return err(e instanceof Error ? e.message : "Failed to get history");
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
),
|
|
919
|
+
|
|
920
|
+
// ── Save as template ───────────────────────────────────────────────
|
|
921
|
+
|
|
922
|
+
defineTool(
|
|
923
|
+
"save_as_template",
|
|
924
|
+
"Save a table as a reusable user template, optionally including sample data from the first 5 rows.",
|
|
925
|
+
{
|
|
926
|
+
tableId: z.string().describe("Table ID to save as template"),
|
|
927
|
+
name: z.string().min(1).describe("Template name"),
|
|
928
|
+
category: z.enum(["business", "personal", "pm", "finance", "content"]).optional().describe("Template category (default: personal)"),
|
|
929
|
+
includeSampleData: z.boolean().optional().describe("Include first 5 rows as sample data"),
|
|
930
|
+
},
|
|
931
|
+
async (args) => {
|
|
932
|
+
try {
|
|
933
|
+
const res = await fetch(`${getBaseUrl()}/api/tables/templates`, {
|
|
934
|
+
method: "POST",
|
|
935
|
+
headers: { "Content-Type": "application/json" },
|
|
936
|
+
body: JSON.stringify({
|
|
937
|
+
tableId: args.tableId,
|
|
938
|
+
name: args.name,
|
|
939
|
+
category: args.category ?? "personal",
|
|
940
|
+
includeSampleData: args.includeSampleData ?? false,
|
|
941
|
+
}),
|
|
942
|
+
});
|
|
943
|
+
if (!res.ok) return err("Failed to save template");
|
|
944
|
+
return ok(await res.json());
|
|
945
|
+
} catch (e) {
|
|
946
|
+
return err(e instanceof Error ? e.message : "Failed to save template");
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
),
|
|
950
|
+
];
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function getBaseUrl(): string {
|
|
954
|
+
return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
955
|
+
}
|