@ganju/utils 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/constants.d.ts +26 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +107 -9
  4. package/dist/customCodeToken.d.ts +11 -0
  5. package/dist/customCodeToken.d.ts.map +1 -1
  6. package/dist/customCodeToken.js +41 -1
  7. package/dist/data.d.ts +13 -0
  8. package/dist/data.d.ts.map +1 -0
  9. package/dist/data.js +103 -0
  10. package/dist/dataApi.d.ts +578 -0
  11. package/dist/dataApi.d.ts.map +1 -0
  12. package/dist/dataApi.js +399 -0
  13. package/dist/dataCompiler.d.ts +68 -0
  14. package/dist/dataCompiler.d.ts.map +1 -0
  15. package/dist/dataCompiler.js +730 -0
  16. package/dist/dataCsv.d.ts +20 -0
  17. package/dist/dataCsv.d.ts.map +1 -0
  18. package/dist/dataCsv.js +102 -0
  19. package/dist/dataDdl.d.ts +29 -0
  20. package/dist/dataDdl.d.ts.map +1 -0
  21. package/dist/dataDdl.js +354 -0
  22. package/dist/dataEngine.d.ts +143 -0
  23. package/dist/dataEngine.d.ts.map +1 -0
  24. package/dist/dataEngine.js +2042 -0
  25. package/dist/dataIdentifiers.d.ts +8 -0
  26. package/dist/dataIdentifiers.d.ts.map +1 -0
  27. package/dist/dataIdentifiers.js +55 -0
  28. package/dist/dataImport.d.ts +79 -0
  29. package/dist/dataImport.d.ts.map +1 -0
  30. package/dist/dataImport.js +453 -0
  31. package/dist/dataQueries.d.ts +73 -0
  32. package/dist/dataQueries.d.ts.map +1 -0
  33. package/dist/dataQueries.js +513 -0
  34. package/dist/dataRpc.d.ts +150 -0
  35. package/dist/dataRpc.d.ts.map +1 -0
  36. package/dist/dataRpc.js +2 -0
  37. package/dist/dataTools.d.ts +40 -0
  38. package/dist/dataTools.d.ts.map +1 -0
  39. package/dist/dataTools.js +280 -0
  40. package/dist/dataTypes.d.ts +159 -0
  41. package/dist/dataTypes.d.ts.map +1 -0
  42. package/dist/dataTypes.js +160 -0
  43. package/dist/index.d.ts +394 -8
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -0
  46. package/dist/oauth.d.ts +14 -0
  47. package/dist/oauth.d.ts.map +1 -1
  48. package/dist/oauth.js +42 -2
  49. package/dist/resourceRerank.d.ts +3 -2
  50. package/dist/resourceRerank.d.ts.map +1 -1
  51. package/dist/resourceRerank.js +5 -2
  52. package/dist/schema.d.ts +376 -4
  53. package/dist/schema.d.ts.map +1 -1
  54. package/dist/schema.js +268 -1
  55. package/dist/sdkConstants.d.ts +1 -0
  56. package/dist/sdkConstants.d.ts.map +1 -1
  57. package/dist/sdkConstants.js +2 -1
  58. package/package.json +17 -2
  59. package/src/constants.ts +121 -9
  60. package/src/customCodeToken.ts +74 -0
  61. package/src/data.ts +108 -0
  62. package/src/dataApi.ts +479 -0
  63. package/src/dataCompiler.ts +952 -0
  64. package/src/dataCsv.ts +99 -0
  65. package/src/dataDdl.ts +419 -0
  66. package/src/dataEngine.ts +2773 -0
  67. package/src/dataIdentifiers.ts +63 -0
  68. package/src/dataImport.ts +557 -0
  69. package/src/dataQueries.ts +730 -0
  70. package/src/dataRpc.ts +132 -0
  71. package/src/dataTools.ts +338 -0
  72. package/src/dataTypes.ts +318 -0
  73. package/src/index.ts +13 -1
  74. package/src/oauth.ts +42 -1
  75. package/src/resourceRerank.ts +5 -2
  76. package/src/schema.ts +315 -1
  77. package/src/sdkConstants.ts +1 -0
package/src/dataRpc.ts ADDED
@@ -0,0 +1,132 @@
1
+ // The methods a project's database object answers over RPC. apps/data's
2
+ // ProjectDatabase implements it; apps/api, apps/mcp and apps/tool-broker call
3
+ // it through databaseStub. Every method resolves to a DataResult.
4
+ import type {
5
+ DataCaller,
6
+ DataDeleteImpact,
7
+ DataResult,
8
+ DataSchema,
9
+ DataTable,
10
+ DataTableCopy
11
+ } from './dataTypes';
12
+ import type { DataQuery } from './dataQueries';
13
+
14
+ export type ExportFormat = 'jsonl' | 'csv';
15
+
16
+ export interface ExportFile {
17
+ // null for schema.json
18
+ table: string | null;
19
+ key: string;
20
+ rows: number;
21
+ bytes: number;
22
+ }
23
+
24
+ export interface ExportState {
25
+ id: string;
26
+ format: ExportFormat;
27
+ status: 'running' | 'done' | 'failed';
28
+ startedAt: string;
29
+ finishedAt: string | null;
30
+ actor: string;
31
+ tables: string[];
32
+ tableIndex: number;
33
+ afterId: number;
34
+ upload: {
35
+ key: string;
36
+ uploadId: string;
37
+ parts: { partNumber: number; etag: string }[];
38
+ rows: number;
39
+ bytes: number;
40
+ // Bytes read but not yet uploaded, kept in R2 between steps: every part
41
+ // but the last must be exactly the same size.
42
+ carryKey: string | null;
43
+ } | null;
44
+ files: ExportFile[];
45
+ error: string | null;
46
+ }
47
+
48
+ export interface DatabaseInfo {
49
+ artifactId: string | null;
50
+ name: string | null;
51
+ initializedAt: string | null;
52
+ deletedAt: string | null;
53
+ firstWriteAt: string | null;
54
+ // The zone set for the database, or null for UTC.
55
+ timeZone: string | null;
56
+ schemaVersion: number;
57
+ size: number;
58
+ tables: { name: string; label: string | null; columns: number }[];
59
+ // A column change copying a large table in the background: table changes,
60
+ // and writes that would change its rows, wait while it runs.
61
+ copy: DataTableCopy | null;
62
+ }
63
+
64
+ export interface RestoreResult {
65
+ bookmark: string;
66
+ undoBookmark: string;
67
+ }
68
+
69
+ type R<T> = Promise<DataResult<T>>;
70
+
71
+ export interface ProjectDatabaseRpc {
72
+ init(args: { artifactId: string; name: string }): R<DatabaseInfo>;
73
+ info(): R<DatabaseInfo>;
74
+ rename(args: { name: string }): R<DatabaseInfo>;
75
+ size(): R<{ bytes: number }>;
76
+ setTimeZone(caller: DataCaller, args: { timeZone: string | null }): R<DatabaseInfo>;
77
+ markDeleted(
78
+ caller: DataCaller
79
+ ): R<{ deletedAt: string | null; bookmark: string }>;
80
+ undelete(caller: DataCaller): R<DatabaseInfo>;
81
+ purge(): R<{ purged: true; bookmark: string }>;
82
+ currentBookmark(): R<{ bookmark: string }>;
83
+ restoreTo(caller: DataCaller, args: { timestamp: number }): R<RestoreResult>;
84
+ restoreToBookmark(args: { bookmark: string }): R<RestoreResult>;
85
+ schemaRead(): R<DataSchema>;
86
+ createTable(caller: DataCaller, input: unknown): R<DataTable>;
87
+ updateTable(caller: DataCaller, table: string, patch: unknown): R<DataTable>;
88
+ dropTable(
89
+ caller: DataCaller,
90
+ table: string
91
+ ): R<{ dropped: string; bookmark: string }>;
92
+ addColumn(caller: DataCaller, table: string, input: unknown): R<DataTable>;
93
+ updateColumn(
94
+ caller: DataCaller,
95
+ table: string,
96
+ column: string,
97
+ patch: unknown
98
+ ): R<{ table: DataTable; bookmark: string | null; copy: DataTableCopy | null }>;
99
+ dropColumn(
100
+ caller: DataCaller,
101
+ table: string,
102
+ column: string
103
+ ): R<{ table: DataTable; bookmark: string; copy: DataTableCopy | null }>;
104
+ // Forgets a failed background copy once the owner has seen why.
105
+ dismissCopy(caller: DataCaller): R<{ copy: null }>;
106
+ describe(caller: DataCaller, args?: unknown): R<unknown>;
107
+ query(caller: DataCaller, args: unknown): R<unknown>;
108
+ write(
109
+ caller: DataCaller,
110
+ args: unknown
111
+ ): R<{ ok: true; results: unknown[] }>;
112
+ deleteImpact(
113
+ caller: DataCaller,
114
+ args: { table: string; ids?: number[] }
115
+ ): R<DataDeleteImpact>;
116
+ history(
117
+ caller: DataCaller,
118
+ args: { table: string; id: number; limit?: number; cursor?: number }
119
+ ): R<unknown>;
120
+ listQueries(caller: DataCaller): R<DataQuery[]>;
121
+ saveQuery(caller: DataCaller, input: unknown): R<DataQuery>;
122
+ dropQuery(caller: DataCaller, name: string): R<{ dropped: string }>;
123
+ runQuery(
124
+ caller: DataCaller,
125
+ args: { name: string; params?: Record<string, unknown>; limit?: number }
126
+ ): R<{ query: string; rows: Record<string, unknown>[]; truncated?: string }>;
127
+ exportStart(
128
+ caller: DataCaller,
129
+ args?: { format?: ExportFormat }
130
+ ): R<{ exportId: string; status: ExportState['status'] }>;
131
+ exportStatus(args?: { exportId?: string }): R<ExportState>;
132
+ }
@@ -0,0 +1,338 @@
1
+ // The data tools as a model sees them: names, descriptions and input schemas,
2
+ // built from the database's schema for one caller. The MCP worker registers
3
+ // them and the tool-design eval measures them, so the text the eval scored is
4
+ // the text that ships.
5
+ import { DATA_ACCESS_RANK } from './dataTypes';
6
+ import type {
7
+ DataCaller,
8
+ DataColumn,
9
+ DataSchema,
10
+ DataTable
11
+ } from './dataTypes';
12
+ import type { DataQuery } from './dataQueries';
13
+
14
+ export const DATA_TOOL_DESCRIBE = 'data-describe';
15
+ export const DATA_TOOL_QUERY = 'data-query';
16
+ export const DATA_TOOL_WRITE = 'data-write';
17
+ export const DATA_TOOL_ADMIN = 'data-admin';
18
+ export const DATA_TOOL_NAMES = [
19
+ DATA_TOOL_DESCRIBE,
20
+ DATA_TOOL_QUERY,
21
+ DATA_TOOL_WRITE,
22
+ DATA_TOOL_ADMIN
23
+ ] as const;
24
+
25
+ // How Tool health names them: they have no install row, and aren't a name
26
+ // the model made up.
27
+ export const DATA_TOOL_TITLES: Record<string, string> = {
28
+ [DATA_TOOL_DESCRIBE]: 'Describe the database',
29
+ [DATA_TOOL_QUERY]: 'Query the database',
30
+ [DATA_TOOL_WRITE]: 'Change rows in the database',
31
+ [DATA_TOOL_ADMIN]: 'Manage the database'
32
+ };
33
+
34
+ // How many rows data-query returns at most. The engine pages up to 500; a
35
+ // model reading 500 rows answers worse, not better.
36
+ export const DATA_TOOL_MAX_LIMIT = 100;
37
+
38
+ export interface DataToolDefinition {
39
+ name: string;
40
+ title: string;
41
+ description: string;
42
+ inputSchema: Record<string, unknown>;
43
+ annotations: {
44
+ readOnlyHint: boolean;
45
+ destructiveHint: boolean;
46
+ openWorldHint: boolean;
47
+ };
48
+ }
49
+
50
+ const READS = {
51
+ readOnlyHint: true,
52
+ destructiveHint: false,
53
+ openWorldHint: false
54
+ };
55
+ // One annotation per tool: a write may delete, so the tool says it is
56
+ // destructive, and the runner and the MCP confirmation decide per call.
57
+ const CHANGES = {
58
+ readOnlyHint: false,
59
+ destructiveHint: true,
60
+ openWorldHint: false
61
+ };
62
+
63
+ const WHERE_HELP =
64
+ 'Filter object. { "column": value } matches exactly; { "column": { "op": value } } with op one of eq, ne, gt, gte, lt, lte, in, notIn (lists), contains, startsWith (text, case-insensitive), isNull (true/false). Several keys must all match; { "or": [ {...}, {...} ] } matches any. A relation column takes the related row\'s id, or filter on a column of the related table with "relation.column", e.g. { "product.name": "Margherita" }. Datetimes are ISO 8601; include the offset for local times, e.g. "2026-10-19T00:00:00-05:00".';
65
+
66
+ const accessOf = (caller: DataCaller, table: DataTable) =>
67
+ caller.restricted ? table.access : 'full';
68
+
69
+ const visible = (schema: DataSchema, caller: DataCaller) =>
70
+ schema.tables.filter(table => DATA_ACCESS_RANK[accessOf(caller, table)] >= 1);
71
+
72
+ // One column as data-query's description lists it: the type when it isn't
73
+ // plain text, so the model can write filters without guessing names.
74
+ const columnHint = (column: DataColumn) =>
75
+ column.type === 'relation'
76
+ ? `${column.name} (→ ${column.table})`
77
+ : column.type === 'enum'
78
+ ? `${column.name} (${(column.values ?? []).join('|')})`
79
+ : column.type === 'text' || column.type === 'long_text'
80
+ ? column.name
81
+ : `${column.name} (${column.type}${column.min !== null ? `, ≥ ${column.min}` : ''})`;
82
+
83
+ const tableLine = (caller: DataCaller, table: DataTable) => {
84
+ const access = accessOf(caller, table);
85
+ const description = table.description?.trim().replace(/\.$/, '');
86
+ return `- ${table.name}${description ? `: ${description}.` : '.'}${
87
+ caller.restricted && table.customerScoped
88
+ ? " Only this customer's own rows are visible."
89
+ : ''
90
+ }${access === 'read' ? ' Read only.' : access === 'add' ? ' Rows can be added, not changed.' : ''} Columns: ${table.columns
91
+ .map(columnHint)
92
+ .join(', ')}.`;
93
+ };
94
+
95
+ const queryLine = (query: DataQuery) =>
96
+ `- ${query.name}${query.description ? `: ${query.description.replace(/\.$/, '')}.` : '.'}${
97
+ query.params.length
98
+ ? ` Params: ${query.params
99
+ .map(
100
+ p =>
101
+ `${p.name} (${p.type}${p.required === false ? ', optional' : ''})`
102
+ )
103
+ .join(', ')}.`
104
+ : ''
105
+ }`;
106
+
107
+ /** Whether the caller may use the data tools at all: it sees a table. */
108
+ export const hasDataToolTables = (schema: DataSchema, caller: DataCaller) =>
109
+ visible(schema, caller).length > 0;
110
+
111
+ /**
112
+ * data-describe, data-query and, when the caller may add rows somewhere,
113
+ * data-write. Only tables the caller may see are named, so a hidden table
114
+ * doesn't exist for the model. `queries` are the named queries it may run.
115
+ */
116
+ export const dataToolDefinitions = (
117
+ schema: DataSchema,
118
+ caller: DataCaller,
119
+ options: { queries?: DataQuery[] } = {}
120
+ ): DataToolDefinition[] => {
121
+ const tables = visible(schema, caller);
122
+ if (tables.length === 0) return [];
123
+ const queries = options.queries ?? [];
124
+ const timeZone = caller.timeZone || 'UTC';
125
+ const tools: DataToolDefinition[] = [
126
+ {
127
+ name: DATA_TOOL_DESCRIBE,
128
+ title: DATA_TOOL_TITLES[DATA_TOOL_DESCRIBE],
129
+ description:
130
+ "Describe the project's database: its tables, their columns (type, allowed values, which table a relation points to, the owner's description of each), row counts, which columns full-text search covers, and what the assistant may do with each table (read, add, or full). Call it before querying a table whose columns you haven't seen in this conversation.",
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ table: {
135
+ type: 'string',
136
+ description:
137
+ 'One table to describe. Leave out to describe all of them.'
138
+ }
139
+ }
140
+ },
141
+ annotations: READS
142
+ },
143
+ {
144
+ name: DATA_TOOL_QUERY,
145
+ title: DATA_TOOL_TITLES[DATA_TOOL_QUERY],
146
+ description: [
147
+ "Read rows from the project's database: structured records such as products, prices, stock, orders, bookings or customers. Use it for anything with fields and numbers; use the knowledge/resource tools for documents and pages.",
148
+ 'Returns matching rows (relation columns come back as { id, label }), the total that match, and nextCursor when there are more. With "aggregate" it returns counts, sums, averages, minimums or maximums instead of rows, optionally per group: use that for "how many" and "how much" rather than adding rows up yourself.',
149
+ "The database holds only what these tables and columns hold. When a question needs something they don't have (opening hours, parking, calories, reviews, forecasts), look in the knowledge tools if you have them; if nothing has it, say you don't have that information. Never guess it.",
150
+ `Every table also has id, created_at and updated_at. Datetimes come back in the project's time zone (${timeZone}) with their offset; read them as local times.`,
151
+ 'Tables:',
152
+ tables.map(table => tableLine(caller, table)).join('\n'),
153
+ ...(queries.length
154
+ ? [
155
+ 'Saved reports: instead of "table", pass "query" with its name and "params"; they return exactly what the report computes.',
156
+ queries.map(queryLine).join('\n')
157
+ ]
158
+ : [])
159
+ ].join('\n'),
160
+ inputSchema: {
161
+ type: 'object',
162
+ properties: {
163
+ table: { type: 'string', description: 'Table to read.' },
164
+ where: { type: 'object', description: WHERE_HELP },
165
+ search: {
166
+ type: 'string',
167
+ description:
168
+ "Words to look for in the table's searchable text columns (accents and case ignored, ranked by relevance). Use it when the user names something loosely; use where for exact values."
169
+ },
170
+ sort: {
171
+ type: 'array',
172
+ items: { type: 'string' },
173
+ description:
174
+ 'Columns to sort by, "-" first for descending, e.g. ["-created_at"]. Relation paths allowed.'
175
+ },
176
+ limit: {
177
+ type: 'integer',
178
+ description: `Rows to return, 1 to ${DATA_TOOL_MAX_LIMIT}. Default 20.`
179
+ },
180
+ cursor: {
181
+ type: 'string',
182
+ description:
183
+ 'nextCursor from the previous call, to get the next page.'
184
+ },
185
+ include: {
186
+ type: 'array',
187
+ items: { type: 'string' },
188
+ description:
189
+ 'Relation columns to expand into the full related row, e.g. ["customer"].'
190
+ },
191
+ aggregate: {
192
+ type: 'array',
193
+ description:
194
+ 'Compute instead of listing rows. Each item is { "fn": "count" } or { "fn": "sum"|"avg"|"min"|"max", "column": "..." } (relation paths allowed, e.g. "order.total"). Add "times": "<number column>" to aggregate a product, e.g. { "fn": "sum", "column": "stock", "times": "unit_cost" } for stock value.',
195
+ items: {
196
+ type: 'object',
197
+ properties: {
198
+ fn: {
199
+ type: 'string',
200
+ enum: ['count', 'sum', 'avg', 'min', 'max']
201
+ },
202
+ column: { type: 'string' },
203
+ times: { type: 'string' }
204
+ },
205
+ required: ['fn']
206
+ }
207
+ },
208
+ groupBy: {
209
+ type: 'array',
210
+ items: { type: 'string' },
211
+ description:
212
+ 'With aggregate: columns to group by, e.g. ["status"] or ["product"]. Groups come back largest first.'
213
+ },
214
+ ...(queries.length
215
+ ? {
216
+ query: {
217
+ type: 'string',
218
+ enum: queries.map(q => q.name),
219
+ description:
220
+ 'A saved report to run instead of reading a table.'
221
+ },
222
+ params: {
223
+ type: 'object',
224
+ description:
225
+ 'The saved report\'s parameters, e.g. { "from": "2026-10-01" }.'
226
+ }
227
+ }
228
+ : {})
229
+ },
230
+ ...(queries.length ? {} : { required: ['table'] })
231
+ },
232
+ annotations: READS
233
+ }
234
+ ];
235
+ if (
236
+ tables.some(
237
+ table => DATA_ACCESS_RANK[accessOf(caller, table)] >= DATA_ACCESS_RANK.add
238
+ )
239
+ ) {
240
+ tools.push({
241
+ name: DATA_TOOL_WRITE,
242
+ title: DATA_TOOL_TITLES[DATA_TOOL_WRITE],
243
+ description: [
244
+ "Add, change or delete rows in the project's database. All operations in one call succeed together or none is applied, so put everything that belongs together (an order, its lines and the stock they use) in one call.",
245
+ '- insert: { "op": "insert", "table": "...", "values": { column: value } }. A relation column takes the related row\'s id; to point at a row inserted earlier in the same call use { "$ref": n }, n being that operation\'s position (0-based).',
246
+ '- update: { "op": "update", "table": "...", "id": 12, "set": { column: value } }. For numbers, { "decrement": 2 } or { "increment": 2 } changes the value relative to what is stored. Instead of id you can give "where" (same format as data-query).',
247
+ '- delete: { "op": "delete", "table": "...", "id": 12 } (or "where").',
248
+ 'Guards: an update or delete also takes "where" conditions that must hold, e.g. "where": { "stock": { "gte": 2 } }, and "expect", how many rows must change (1 by default when id is given). If a guard fails, nothing in the call is written and the error says why (e.g. how much stock is left).',
249
+ 'Only use ids you got from data-query or from an earlier insert; never guess them.'
250
+ ].join('\n'),
251
+ inputSchema: {
252
+ type: 'object',
253
+ properties: {
254
+ operations: {
255
+ type: 'array',
256
+ items: {
257
+ type: 'object',
258
+ properties: {
259
+ op: { type: 'string', enum: ['insert', 'update', 'delete'] },
260
+ table: { type: 'string' },
261
+ id: { type: 'integer' },
262
+ values: { type: 'object' },
263
+ set: { type: 'object' },
264
+ where: { type: 'object' },
265
+ expect: { type: 'integer' }
266
+ },
267
+ required: ['op', 'table']
268
+ }
269
+ }
270
+ },
271
+ required: ['operations']
272
+ },
273
+ annotations: CHANGES
274
+ });
275
+ }
276
+ return tools;
277
+ };
278
+
279
+ export const DATA_ADMIN_ACTIONS = [
280
+ 'schema',
281
+ 'create_table',
282
+ 'update_table',
283
+ 'drop_table',
284
+ 'add_column',
285
+ 'update_column',
286
+ 'drop_column',
287
+ 'save_query',
288
+ 'drop_query',
289
+ 'import',
290
+ 'export'
291
+ ] as const;
292
+ export type DataAdminAction = (typeof DATA_ADMIN_ACTIONS)[number];
293
+
294
+ /**
295
+ * data-admin: the owner's own MCP client managing the database's structure.
296
+ * One tool, so it costs one slot in the session it exists in; destructive, so
297
+ * every call goes through confirmation and, after outside content, the
298
+ * approval link.
299
+ */
300
+ export const dataAdminToolDefinition = (): DataToolDefinition => ({
301
+ name: DATA_TOOL_ADMIN,
302
+ title: DATA_TOOL_TITLES[DATA_TOOL_ADMIN],
303
+ description: [
304
+ "Manage the project's database structure as its owner: tables, columns, indexes, descriptions, saved reports, imports and exports. To read or change rows use data-query and data-write.",
305
+ '- schema: every table as it would be written again (columns with type, required, unique, filterable, searchable, description, enum values, relation target and minimum), plus the saved reports.',
306
+ '- create_table: "input" is { name, description, displayColumn, access (none|read|add|full: what channel bots may do), customerScoped, columns: [{ name, type, ... }] }. Types: text, long_text, integer, decimal, boolean, date, datetime, enum (with "values"), json, email, url, phone, relation (with "table" and "onDelete": restrict|cascade|set_null), file.',
307
+ '- update_table: "table" and "input" with what changes: name, description, displayColumn, access, customerScoped, history.',
308
+ '- drop_table: "table". It can be undone with a restore from the dashboard.',
309
+ '- add_column: "table" and "input" as one column of create_table.',
310
+ '- update_column: "table", "column" and "input" with what changes: name, type, description, required, unique, filterable (adds an index), searchable, values, min, default. A type change copies the table and refuses if a value doesn\'t fit.',
311
+ '- drop_column: "table" and "column".',
312
+ '- save_query: "input" is { name, description, sql, params: [{ name, type: text|integer|decimal|boolean|date|datetime, description }], assistant }. sql is one SELECT with named parameters (:from); it is checked to only read. assistant: true lets channel bots run it.',
313
+ '- drop_query: "name".',
314
+ '- import: "table" and "resource", the uri of a CSV or Excel file in the project\'s knowledge ("sheet" picks one, the first by default). Headers are matched to columns by name or label, and relation columns by the related row\'s name; rows are added, not replaced. Up to 5 MB.',
315
+ '- export: start a copy of the whole database (format jsonl or csv); download it from the Data page.'
316
+ ].join('\n'),
317
+ inputSchema: {
318
+ type: 'object',
319
+ properties: {
320
+ action: { type: 'string', enum: [...DATA_ADMIN_ACTIONS] },
321
+ table: { type: 'string' },
322
+ column: { type: 'string' },
323
+ name: { type: 'string', description: 'drop_query: the saved report.' },
324
+ resource: {
325
+ type: 'string',
326
+ description: 'import: the CSV or Excel resource uri.'
327
+ },
328
+ sheet: {
329
+ type: 'string',
330
+ description: 'import: the Excel sheet; the first by default.'
331
+ },
332
+ format: { type: 'string', enum: ['jsonl', 'csv'] },
333
+ input: { type: 'object' }
334
+ },
335
+ required: ['action']
336
+ },
337
+ annotations: CHANGES
338
+ });