vectrojs 0.0.1 → 0.1.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +828 -4
  3. package/bin/vectro.mjs +82 -0
  4. package/dist/vectro.wasm +0 -0
  5. package/mcp/server.mjs +288 -0
  6. package/package.json +84 -6
  7. package/src/arrow.mjs +905 -0
  8. package/src/bitpack.mjs +135 -0
  9. package/src/browser.mjs +129 -0
  10. package/src/bytes.mjs +60 -0
  11. package/src/csv-kernel.mjs +205 -0
  12. package/src/csv.mjs +270 -0
  13. package/src/dataset.mjs +1763 -0
  14. package/src/dates.mjs +139 -0
  15. package/src/emitter.mjs +109 -0
  16. package/src/engine.mjs +881 -0
  17. package/src/files-browser.mjs +104 -0
  18. package/src/files-node.mjs +59 -0
  19. package/src/frames.mjs +191 -0
  20. package/src/fuse.mjs +512 -0
  21. package/src/interp.mjs +305 -0
  22. package/src/join.mjs +534 -0
  23. package/src/kernels.mjs +312 -0
  24. package/src/ndjson.mjs +136 -0
  25. package/src/node.mjs +120 -0
  26. package/src/pack.mjs +114 -0
  27. package/src/parallel-sort.mjs +99 -0
  28. package/src/partition-kernels.mjs +388 -0
  29. package/src/partition.mjs +395 -0
  30. package/src/plan.mjs +551 -0
  31. package/src/portable.mjs +21 -0
  32. package/src/rank-kernels.mjs +81 -0
  33. package/src/rows.mjs +91 -0
  34. package/src/serve/catalog.mjs +148 -0
  35. package/src/serve/flight.mjs +270 -0
  36. package/src/serve/formats.mjs +87 -0
  37. package/src/serve/http.mjs +182 -0
  38. package/src/serve/index.mjs +9 -0
  39. package/src/serve/odata.mjs +291 -0
  40. package/src/serve/pg.mjs +588 -0
  41. package/src/serve/pgcatalog.mjs +220 -0
  42. package/src/serve/pgtypes.mjs +199 -0
  43. package/src/sketch.mjs +75 -0
  44. package/src/sort-kernels.mjs +117 -0
  45. package/src/sort.mjs +102 -0
  46. package/src/split-worker-core.mjs +252 -0
  47. package/src/split-worker-node.mjs +8 -0
  48. package/src/split-worker.mjs +7 -0
  49. package/src/split.mjs +741 -0
  50. package/src/sql.mjs +1558 -0
  51. package/src/sqltext.mjs +132 -0
  52. package/src/static-kernels.mjs +164 -0
  53. package/src/stream-worker.mjs +553 -0
  54. package/src/table-kernels.mjs +323 -0
  55. package/src/table.mjs +2247 -0
  56. package/src/textmap.mjs +159 -0
  57. package/src/vectors.mjs +138 -0
  58. package/src/wasm.mjs +254 -0
  59. package/src/window.mjs +160 -0
  60. package/src/worker-browser.mjs +21 -0
  61. package/src/worker-loop.mjs +106 -0
  62. package/src/worker.mjs +22 -0
  63. package/src/zones.mjs +75 -0
package/bin/vectro.mjs ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // vectro serve --root ~/data [--port 8787] [--pg 5433] ...
3
+ // Serves the CSV, TSV, JSON-lines, Arrow and .vectro files in the folder as
4
+ // SQL tables over HTTP and / or the PostgreSQL protocol (src/serve/).
5
+ import { serve, servePg, serveFlight, openTables } from '../src/serve/index.mjs';
6
+
7
+ const [command, ...rest] = process.argv.slice(2);
8
+ const HELP = `usage: vectro serve --root <folder> [options]
9
+
10
+ Every CSV, TSV, JSON-lines, Arrow / Feather and .vectro file in the folder
11
+ becomes a table named after its file.
12
+
13
+ HTTP (Excel, Power BI Web, Google Sheets, pandas):
14
+ --port 8787 the HTTP port (--no-http: none)
15
+ --token <secret> requests need "Authorization: Bearer <secret>"
16
+ --cors allow pages from other sites
17
+ PostgreSQL (Power BI, Tableau, Metabase, Grafana, psql):
18
+ --pg 5433 also speak the PostgreSQL protocol on this port
19
+ --user <name> --password <secret> (or VECTRO_PG_PASSWORD; needed with --host)
20
+ --auth scram|md5|password how the password is checked (scram)
21
+ --tls-cert <file> --tls-key <file> encrypted connections
22
+ --database <name> the database name reported (vectro)
23
+ --log-queries print every statement clients send
24
+ Arrow Flight SQL (pandas, Polars, DuckDB through ADBC):
25
+ --flight 31337 also serve Flight SQL on this port (grpc://host:31337)
26
+ (--user / --password and --tls-cert / --tls-key as above)
27
+ All:
28
+ --host 127.0.0.1 the address to listen on (0.0.0.0: every network)
29
+ --quiet
30
+
31
+ curl 'http://127.0.0.1:8787/sql?q=SELECT region, COUNT(*) FROM orders GROUP BY 1&format=csv'
32
+ psql -h 127.0.0.1 -p 5433 -U vectro vectro`;
33
+
34
+ function option(name, fallback) {
35
+ const i = rest.indexOf(`--${name}`);
36
+ if (i < 0) return fallback;
37
+ const v = rest[i + 1];
38
+ if (v === undefined || v.startsWith('--')) { console.error(`vectro: --${name} needs a value\n\n${HELP}`); process.exit(2); }
39
+ return v;
40
+ }
41
+
42
+ if (command !== 'serve') {
43
+ console.log(HELP);
44
+ process.exit(command === undefined || command === 'help' || command === '--help' ? 0 : 2);
45
+ }
46
+ const quiet = rest.includes('--quiet');
47
+ const log = quiet ? () => {} : (msg) => process.stderr.write(`[vectro] ${msg}\n`);
48
+ try {
49
+ const host = option('host', '127.0.0.1');
50
+ const catalog = await openTables({ root: option('root', process.cwd()), log });
51
+ const running = [];
52
+ if (!rest.includes('--no-http')) {
53
+ const s = await serve({ catalog, port: Number(option('port', process.env.PORT ?? 8787)), host, token: option('token', process.env.VECTRO_TOKEN ?? null), cors: rest.includes('--cors'), log });
54
+ running.push(s);
55
+ log(`HTTP at ${s.url}: try ${s.url}/sql?q=${encodeURIComponent(`SELECT * FROM ${[...catalog.entries.keys()][0] ?? 'mytable'} LIMIT 5`)}`);
56
+ }
57
+ const pgPort = option('pg', null);
58
+ if (pgPort !== null) {
59
+ const certFile = option('tls-cert', null), keyFile = option('tls-key', null);
60
+ const p = await servePg({
61
+ catalog, port: Number(pgPort), host, user: option('user', null), password: option('password', process.env.VECTRO_PG_PASSWORD ?? null),
62
+ auth: option('auth', 'scram'), tls: certFile && keyFile ? { certFile, keyFile } : null, database: option('database', 'vectro'),
63
+ log, logQueries: rest.includes('--log-queries'),
64
+ });
65
+ running.push(p);
66
+ log(`PostgreSQL at ${p.host}:${p.port} (database ${option('database', 'vectro')}${certFile ? ', TLS' : ''}): psql -h ${p.host} -p ${p.port} -U ${option('user', 'vectro')} ${option('database', 'vectro')}`);
67
+ }
68
+ const flightPort = option('flight', null);
69
+ if (flightPort !== null) {
70
+ const certFile = option('tls-cert', null), keyFile = option('tls-key', null);
71
+ const f = await serveFlight({ catalog, port: Number(flightPort), host, user: option('user', null), password: option('password', process.env.VECTRO_PG_PASSWORD ?? null), tls: certFile && keyFile ? { certFile, keyFile } : null, database: option('database', 'vectro'), log, logQueries: rest.includes('--log-queries') });
72
+ running.push(f);
73
+ log(`Flight SQL at ${f.url}`);
74
+ }
75
+ log(`${catalog.entries.size} table(s): ${[...catalog.entries].map(([n, e]) => `${n} (${e.table.length.toLocaleString()} rows)`).join(', ') || 'none'}`);
76
+ const stop = () => Promise.all(running.map((s) => s.close())).then(() => catalog.close()).then(() => process.exit(0));
77
+ process.on('SIGINT', stop);
78
+ process.on('SIGTERM', stop);
79
+ } catch (err) {
80
+ console.error(`vectro: ${err.message.replace(/^vectro:\s*/, '')}`);
81
+ process.exit(1);
82
+ }
Binary file
package/mcp/server.mjs ADDED
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ // Vectro MCP server: lets an AI assistant (Claude, ChatGPT, any MCP client)
3
+ // load data files and ask analytics questions, answered by Vectro on all
4
+ // cores. Speaks the Model Context Protocol over stdio (JSON-RPC 2.0, one
5
+ // message per line), with no dependencies.
6
+ //
7
+ // node mcp/server.mjs --root ~/data (files outside --root are refused)
8
+ //
9
+ // Safety: the assistant only reads files under --root, and its expressions
10
+ // are Vectro's small math language (no code runs, no eval).
11
+ import { realpathSync } from 'node:fs';
12
+ import { resolve, extname, basename } from 'node:path';
13
+ import { createInterface } from 'node:readline';
14
+ import { Engine, sql } from '../src/node.mjs';
15
+ import { LoadError, safePath as safePathIn, loadFile, columnsOf, kindOf } from '../src/serve/catalog.mjs';
16
+
17
+ const VERSION = '0.1.1';
18
+ const PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05'];
19
+ const MAX_ROWS = 500, DEFAULT_ROWS = 50;
20
+
21
+ const args = process.argv.slice(2);
22
+ const rootArg = args.includes('--root') ? args[args.indexOf('--root') + 1] : process.cwd();
23
+ const ROOT = realpathSync(resolve(rootArg));
24
+ const engine = new Engine();
25
+ const tables = new Map(); // name -> { table, source, chain: tables to free when dropped (add_column builds on the previous one) }
26
+ const log = (...a) => process.stderr.write(`[vectro-mcp] ${a.join(' ')}\n`);
27
+
28
+ // ---- tools --------------------------------------------------------------------
29
+ const TOOLS = [
30
+ {
31
+ name: 'load_file',
32
+ description: 'Load a CSV, TSV, JSON-lines (one flat JSON object per line) or Arrow file (.arrow, .feather, .ipc) from the data folder into a named table, or open a Vectro dataset (.vectro: a table on disk, any size; queries read it in chunks). Returns its columns, their kinds and a few sample rows. Text columns become categories (filterable, groupable).',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ path: { type: 'string', description: 'File path, relative to the data folder' },
37
+ name: { type: 'string', description: 'Table name to use in later calls (default: the file name without extension)' },
38
+ types: { type: 'object', description: 'Optional column kinds: { column: "number" | "category" | "string" } (string = free text, returned but not filtered)', additionalProperties: { type: 'string' } },
39
+ },
40
+ required: ['path'],
41
+ },
42
+ },
43
+ {
44
+ name: 'list_tables',
45
+ description: 'List the loaded tables with their row counts and columns.',
46
+ inputSchema: { type: 'object', properties: {} },
47
+ },
48
+ {
49
+ name: 'describe_table',
50
+ description: 'Column statistics: for numbers min, max, mean and missing count; for categories the number of distinct labels and the most common ones.',
51
+ inputSchema: { type: 'object', properties: { table: { type: 'string' } }, required: ['table'] },
52
+ },
53
+ {
54
+ name: 'query',
55
+ description: [
56
+ 'Answer a question about a table. All parts are optional except table.',
57
+ 'Expressions use column names, numbers, + - * /, comparisons (== != < <= > >=), && || !, cond ? a : b, and abs sqrt floor ceil min max. Text is compared with quotes: region == \'EU\'. Category text functions: startsWith(col, \'x\'), endsWith(col, \'x\'), contains(col, \'x\'), in(col, \'a\', \'b\'), like(col, \'EU-%\') (% any run, _ one character). Date columns compare with ISO text (ts >= \'2024-03-01\') and have year(ts), month(ts), day(ts), hour(ts), weekday(ts) (1 = Monday), dayStart(ts), weekStart(ts), monthStart(ts). Missing values: isMissing(x), coalesce(x, 0); sum, mean, min, max and count(x) skip them.',
58
+ 'filter: which rows, e.g. "region == \'EU\' && price > 100".',
59
+ 'aggregates: { name: "sum(expr)" | "mean(expr)" | "min(expr)" | "max(expr)" | "count()" | "count(condition)" | "countDistinct(expr)" }.',
60
+ 'group_by: a column (any text or number column) or a list of columns, to aggregate per group.',
61
+ 'sort: a column, aggregate name or expression ("-" first for largest first), or a list of them.',
62
+ 'limit: rows to return (default 50, at most 500).',
63
+ 'Without aggregates it returns matching rows; with aggregates and no group_by, one row of totals.',
64
+ ].join(' '),
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ table: { type: 'string' },
69
+ filter: { type: 'string' },
70
+ group_by: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
71
+ aggregates: { type: 'object', additionalProperties: { type: 'string' } },
72
+ sort: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
73
+ limit: { type: 'integer', minimum: 1, maximum: MAX_ROWS },
74
+ },
75
+ required: ['table'],
76
+ },
77
+ },
78
+ {
79
+ name: 'sql',
80
+ description: [
81
+ 'Run one SQL SELECT over the loaded tables (by their names).',
82
+ 'SELECT [DISTINCT] ... FROM t [[LEFT | RIGHT | FULL] JOIN u ON t.x = u.y | USING (x)] WHERE ... GROUP BY ... HAVING ... ORDER BY ... [ASC|DESC] LIMIT n [OFFSET m]; WITH (CTEs), subqueries (IN, EXISTS, correlated), UNION / INTERSECT / EXCEPT, window functions (ROW_NUMBER, RANK, SUM(...) OVER (...)), MEDIAN and PERCENTILE_CONT.',
83
+ 'Aggregates: COUNT(*), COUNT(x), COUNT(DISTINCT x), SUM, AVG, MIN, MAX, APPROX_COUNT_DISTINCT. Also BETWEEN, IN, LIKE, IS NULL, CASE WHEN, COALESCE, ROUND, LEAST, GREATEST, CAST, DATE \'2024-01-01\', INTERVAL \'7 days\', EXTRACT(YEAR FROM ts), DATE_TRUNC(\'month\', ts), YEAR(ts), MONTH(ts).',
84
+ 'Text: compare, LIKE / ILIKE, IN, ||, UPPER, LOWER, TRIM, SUBSTRING, REPLACE, LENGTH, SPLIT_PART, STRFTIME and more. Missing values are NULL (missing text is \'\').',
85
+ 'At most 500 rows come back (add LIMIT); the answer says how many rows the query has.',
86
+ ].join(' '),
87
+ inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
88
+ },
89
+ {
90
+ name: 'search_similar',
91
+ description: 'For tables loaded from JSON lines that have an embedding field (an array of numbers): the k rows whose embeddings are most similar (cosine) to a given row\'s, or to a given vector. Optional filter on the other columns.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ table: { type: 'string' },
96
+ like: { type: 'object', description: 'The row to compare with, found by a column value, e.g. { "column": "id", "value": 42 }', properties: { column: { type: 'string' }, value: {} }, required: ['column', 'value'] },
97
+ vector: { type: 'array', items: { type: 'number' }, description: 'Or an embedding to compare with (same length as the stored ones)' },
98
+ k: { type: 'integer', minimum: 1, maximum: MAX_ROWS },
99
+ filter: { type: 'string', description: 'Which rows may be returned, e.g. "year >= 2020"' },
100
+ },
101
+ required: ['table'],
102
+ },
103
+ },
104
+ {
105
+ name: 'add_column',
106
+ description: 'Add a computed column to a table, e.g. name "revenue", expression "price * qty * (1 - discount)".',
107
+ inputSchema: { type: 'object', properties: { table: { type: 'string' }, name: { type: 'string' }, expression: { type: 'string' } }, required: ['table', 'name', 'expression'] },
108
+ },
109
+ {
110
+ name: 'drop_table',
111
+ description: 'Unload a table and free its memory.',
112
+ inputSchema: { type: 'object', properties: { table: { type: 'string' } }, required: ['table'] },
113
+ },
114
+ ];
115
+
116
+ class ToolError extends Error {}
117
+
118
+ function release(name) {
119
+ const entry = tables.get(name);
120
+ if (!entry) return;
121
+ for (const t of entry.chain.reverse()) { if (t.isDataset) t.close(); else t.dispose(); }
122
+ entry.index?.dispose();
123
+ tables.delete(name);
124
+ }
125
+
126
+ const safePath = (path) => safePathIn(ROOT, path);
127
+
128
+ function getTable(name) {
129
+ const entry = tables.get(name);
130
+ if (!entry) throw new ToolError(`no table "${name}" (loaded: ${[...tables.keys()].join(', ') || 'none'}; use load_file)`);
131
+ return entry;
132
+ }
133
+
134
+ const clean = (rows) => rows.map((r) => Object.fromEntries(Object.entries(r).map(([k, v]) => [k, v instanceof Date ? v.toISOString() : typeof v === 'number' && !Number.isFinite(v) ? null : v])));
135
+ const handlers = {
136
+ sql({ query }) {
137
+ const all = Object.fromEntries([...tables].map(([n, e]) => [n, e.table]));
138
+ const t = sql(query, all).collect();
139
+ try {
140
+ const rows = clean(t.toRows(MAX_ROWS));
141
+ return { rows_in_answer: t.length, returned: rows.length, rows };
142
+ } finally { t.dispose(); }
143
+ },
144
+ async load_file({ path, name, types }) {
145
+ const real = safePath(path);
146
+ const tname = name || basename(real, extname(real)).replace(/[^A-Za-z0-9_]/g, '_');
147
+ const { table, skipped = 0, index = null, embedding = null, onDisk } = await loadFile(engine, real, { types });
148
+ release(tname);
149
+ tables.set(tname, { table, source: path, chain: [table], index });
150
+ if (onDisk) return { table: tname, rows: table.length, on_disk: true, columns: columnsOf(table), sample: clean(table.toRows(5)) };
151
+ return { table: tname, rows: table.length, columns: columnsOf(table), sample: clean(table.toRows(5)), ...(embedding ? { embedding } : {}), ...(skipped ? { skipped_lines: skipped } : {}) };
152
+ },
153
+ list_tables() {
154
+ return [...tables].map(([name, { table, source }]) => ({ table: name, source, rows: table.length, columns: columnsOf(table) }));
155
+ },
156
+ describe_table({ table: name }) {
157
+ const { table } = getTable(name);
158
+ const out = {};
159
+ for (const [col, c] of table.cols) {
160
+ if (c.strings) { out[col] = { kind: 'text' }; continue; }
161
+ if (c.dict) {
162
+ const grouped = table.groupBy(col).agg({ rows: 'count()' });
163
+ const g = table.isDataset ? grouped.collect() : grouped;
164
+ const top = clean(g.top(10, ['-rows', col]).toRows());
165
+ out[col] = { kind: 'category', distinct: g.length, most_common: top };
166
+ g.dispose();
167
+ continue;
168
+ }
169
+ const a = table.filter(`${col} == ${col}`).agg({ n: 'count()', min: `min(${col})`, max: `max(${col})`, mean: `mean(${col})` });
170
+ if (c.date) { // ISO text; filter with ts >= '2024-01-01', year(ts), month(ts)...
171
+ const iso = (v) => (Number.isFinite(v) ? new Date(v).toISOString() : null);
172
+ out[col] = { kind: 'date', min: iso(a.min), max: iso(a.max), missing: table.length - a.n };
173
+ continue;
174
+ }
175
+ out[col] = { kind: 'number', ...clean([a])[0], missing: table.length - a.n };
176
+ }
177
+ return { table: name, rows: table.length, columns: out };
178
+ },
179
+ query({ table: name, filter, group_by: groupBy, aggregates, sort, limit = DEFAULT_ROWS }) {
180
+ const entry = getTable(name);
181
+ const lim = Math.max(1, Math.min(MAX_ROWS, Math.floor(limit)));
182
+ const ds = entry.table.isDataset; // (a dataset: a lazy query, read in chunks)
183
+ const view = ds ? (filter ? entry.table.filter(filter) : entry.table.lazy()) : filter ? entry.table.filter(filter) : entry.table;
184
+ const hasAgg = aggregates && Object.keys(aggregates).length;
185
+ if (groupBy) {
186
+ if (!hasAgg) throw new ToolError('group_by needs aggregates, e.g. { "orders": "count()" }');
187
+ const grouped = view.groupBy(groupBy).agg(aggregates);
188
+ const g = ds ? grouped.collect() : grouped;
189
+ try {
190
+ const groups = g.length;
191
+ const t = sort ? g.top(lim, sort) : null;
192
+ const rows = clean((t ?? g).toRows(lim));
193
+ t?.dispose();
194
+ return { groups, returned: rows.length, rows };
195
+ } finally { g.dispose(); }
196
+ }
197
+ if (hasAgg) return { rows: clean([view.agg(aggregates)]) };
198
+ if (ds) return (() => { const rows = clean((sort ? view.top(lim, sort) : view.limit(lim)).toRows()); return { matching_rows: filter ? view.count() : entry.table.length, returned: rows.length, rows }; })();
199
+ const matching = filter ? view.count() : view.length;
200
+ const t = sort ? view.top(lim, sort) : null;
201
+ const rows = clean((t ?? view).toRows(lim));
202
+ t?.dispose();
203
+ return { matching_rows: matching, returned: rows.length, rows };
204
+ },
205
+ async search_similar({ table: name, like, vector, k = 10, filter }) {
206
+ const entry = getTable(name);
207
+ if (!entry.index) throw new ToolError(`table "${name}" has no embeddings (load a JSON-lines file with an array-of-numbers field)`);
208
+ let q = vector;
209
+ if (!q) {
210
+ if (!like?.column) throw new ToolError('give like: { column, value } or vector');
211
+ const value = typeof like.value === 'string' ? `'${like.value.replaceAll("'", '')}'` : Number(like.value);
212
+ const dims = Array.from({ length: entry.index.dim }, (_, j) => `v${j}`);
213
+ const [row] = entry.index.table.filter(`${like.column} == ${value}`).select(...dims).toRows(1);
214
+ if (!row) throw new ToolError(`no row where ${like.column} is ${JSON.stringify(like.value)}`);
215
+ q = dims.map((d) => row[d]);
216
+ }
217
+ const hits = await entry.index.searchAsync(q, { k: Math.max(1, Math.min(MAX_ROWS, Math.floor(k))), filter });
218
+ return { returned: hits.length, rows: clean(hits) };
219
+ },
220
+ add_column({ table: name, name: col, expression }) {
221
+ const entry = getTable(name);
222
+ if (entry.table.isDataset) throw new ToolError(`"${name}" is a dataset on disk; use the expression directly in query or sql instead of adding a column`);
223
+ const derived = entry.table.derive(col, expression); // shares the old columns
224
+ entry.table = derived;
225
+ entry.chain.push(derived);
226
+ return { table: name, columns: columnsOf(derived), sample: clean(derived.toRows(3)) };
227
+ },
228
+ drop_table({ table: name }) {
229
+ getTable(name);
230
+ release(name);
231
+ return { dropped: name };
232
+ },
233
+ };
234
+
235
+ // ---- JSON-RPC over stdio ----------------------------------------------------------
236
+ const send = (msg) => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...msg })}\n`);
237
+
238
+ async function handle(msg) {
239
+ const { id, method, params } = msg;
240
+ const reply = (result) => id !== undefined && send({ id, result });
241
+ const error = (code, message) => id !== undefined && send({ id, error: { code, message } });
242
+ switch (method) {
243
+ case 'initialize': {
244
+ const asked = params?.protocolVersion;
245
+ return reply({
246
+ protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
247
+ capabilities: { tools: { listChanged: false } },
248
+ serverInfo: { name: 'vectro', title: 'Vectro analytics', version: VERSION },
249
+ instructions: `Load CSV, JSON-lines, Arrow or .vectro files from ${ROOT} with load_file, then answer questions with sql (or query: filter, group_by, aggregates, sort, limit). Use describe_table to see columns and common values first.`,
250
+ });
251
+ }
252
+ case 'notifications/initialized':
253
+ case 'notifications/cancelled':
254
+ return undefined;
255
+ case 'ping':
256
+ return reply({});
257
+ case 'tools/list':
258
+ return reply({ tools: TOOLS });
259
+ case 'tools/call': {
260
+ const fn = handlers[params?.name];
261
+ if (!fn) return error(-32602, `unknown tool: ${params?.name}`);
262
+ try {
263
+ const t0 = performance.now();
264
+ const result = await fn(params.arguments ?? {});
265
+ const ms = Math.round((performance.now() - t0) * 100) / 100;
266
+ const body = Array.isArray(result) ? { tables: result, ms } : { ...result, ms };
267
+ return reply({ content: [{ type: 'text', text: JSON.stringify(body) }], structuredContent: body, isError: false });
268
+ } catch (err) {
269
+ // tool errors go back to the model, so it can fix its call
270
+ return reply({ content: [{ type: 'text', text: err instanceof ToolError || err instanceof LoadError || /^vectro:/.test(err.message) ? err.message : `error: ${err.message}` }], isError: true });
271
+ }
272
+ }
273
+ default:
274
+ if (method?.startsWith('notifications/')) return undefined;
275
+ return error(-32601, `method not found: ${method}`);
276
+ }
277
+ }
278
+
279
+ let queue = Promise.resolve();
280
+ const rl = createInterface({ input: process.stdin });
281
+ rl.on('line', (line) => {
282
+ if (!line.trim()) return;
283
+ let msg;
284
+ try { msg = JSON.parse(line); } catch { return send({ id: null, error: { code: -32700, message: 'parse error' } }); }
285
+ for (const m of Array.isArray(msg) ? msg : [msg]) queue = queue.then(() => handle(m)); // one call at a time, in order
286
+ });
287
+ rl.on('close', () => queue.then(() => engine.close()).then(() => process.exit(0)));
288
+ log(`ready; data folder ${ROOT}`);
package/package.json CHANGED
@@ -1,15 +1,93 @@
1
1
  {
2
2
  "name": "vectrojs",
3
- "version": "0.0.1",
4
- "description": "Multi-core analytics engine for JavaScript (Node, browsers, edge). Coming soon.",
3
+ "version": "0.1.1",
4
+ "description": "Multi-core analytics engine for JavaScript: columnar tables, SQL and fused SIMD WebAssembly kernels for Node, browsers and edge runtimes, with no native code. Serves tables to Power BI, Tableau, Metabase, Grafana, pandas and Polars.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Hamza Masood",
8
+ "homepage": "https://vectrojs.dev",
9
+ "bugs": {
10
+ "url": "https://github.com/vectrojs/vectro/issues",
11
+ "email": "support@vectrojs.dev"
12
+ },
5
13
  "repository": {
6
14
  "type": "git",
7
15
  "url": "git+https://github.com/vectrojs/vectro.git"
8
16
  },
9
- "homepage": "https://github.com/vectrojs",
10
- "license": "MIT",
17
+ "keywords": [
18
+ "analytics",
19
+ "query-engine",
20
+ "embedded-database",
21
+ "javascript",
22
+ "nodejs",
23
+ "sql",
24
+ "dataframe",
25
+ "columnar",
26
+ "olap",
27
+ "webassembly",
28
+ "wasm",
29
+ "simd",
30
+ "multithreading",
31
+ "worker-threads",
32
+ "csv",
33
+ "arrow",
34
+ "flight-sql",
35
+ "postgres-wire-protocol",
36
+ "odata",
37
+ "power-bi",
38
+ "tableau",
39
+ "duckdb-alternative",
40
+ "browser"
41
+ ],
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "main": "src/node.mjs",
46
+ "exports": {
47
+ ".": "./src/node.mjs",
48
+ "./portable": "./src/portable.mjs",
49
+ "./browser": "./src/browser.mjs",
50
+ "./serve": "./src/serve/index.mjs",
51
+ "./dist/vectro.wasm": "./dist/vectro.wasm"
52
+ },
11
53
  "files": [
54
+ "src",
55
+ "bin",
56
+ "dist",
57
+ "mcp",
12
58
  "README.md",
13
- "LICENSE"
14
- ]
59
+ "LICENSE",
60
+ "CHANGELOG.md"
61
+ ],
62
+ "scripts": {
63
+ "build": "node scripts/build-wasm.mjs",
64
+ "prepublishOnly": "npm run build && npm test",
65
+ "test": "node test/test-ops.mjs && node test/test-fusion.mjs && node test/test-tables.mjs && node test/test-csv.mjs && node test/test-async.mjs && node test/test-split.mjs && node test/test-balance.mjs && node test/test-stream.mjs && node test/test-ndjson.mjs && node test/test-window.mjs && node test/test-websocket.mjs && node test/test-frames.mjs && node test/test-emitter.mjs && node test/test-arrow.mjs && node test/test-groupby.mjs && node test/test-partition.mjs && node test/test-join.mjs && node test/test-distinct.mjs && node test/test-distinct-wide.mjs && node test/test-types.mjs && node test/test-plan.mjs && node test/test-sql.mjs && node test/test-missing.mjs && node test/test-fast-paths.mjs && node test/test-pack.mjs && node test/test-dataset.mjs && node test/test-topk-dataset.mjs && node test/test-lookup-join.mjs && node test/test-sort.mjs && node test/test-zones.mjs && node test/test-textfns.mjs && node test/test-vectors.mjs && node test/test-mcp.mjs && node test/test-serve.mjs && node test/test-pg.mjs && node test/test-odata.mjs && node test/test-flight.mjs && node test/test-connectors.mjs && node test/test-check-cases.mjs",
66
+ "test:single": "VECTRO_MODE=single sh -c 'node test/test-ops.mjs && node test/test-fusion.mjs && node test/test-tables.mjs && node test/test-csv.mjs'",
67
+ "test:interp": "VECTRO_MODE=interp sh -c 'node test/test-ops.mjs && node test/test-fusion.mjs && node test/test-tables.mjs && node test/test-csv.mjs'",
68
+ "test:edge": "npm run build && VECTRO_MODE=edge sh -c 'node --disallow-code-generation-from-strings test/test-ops.mjs && node --disallow-code-generation-from-strings test/test-fusion.mjs && node --disallow-code-generation-from-strings test/test-tables.mjs && node --disallow-code-generation-from-strings test/test-csv.mjs'",
69
+ "test:browser-sim": "VECTRO_MODE=browser-sim sh -c 'node test/test-ops.mjs && node test/test-fusion.mjs && node test/test-tables.mjs && node test/test-csv.mjs && node test/test-async.mjs'",
70
+ "test:all": "npm test && npm run test:single && npm run test:interp && npm run test:edge && npm run test:browser-sim && npm run test:compressed && npm run test:poison",
71
+ "test:compressed": "VECTRO_COMPRESS=1 npm test",
72
+ "test:poison": "VECTRO_POISON=1 VECTRO_COMPRESS=1 npm test",
73
+ "check:expected": "node scripts/make-check-expected.mjs",
74
+ "bench": "node --max-old-space-size=8192 bench/bench.mjs",
75
+ "bench:async": "node --max-old-space-size=8192 bench/bench-async.mjs",
76
+ "bench:skew": "node --max-old-space-size=8192 bench/bench-skew.mjs",
77
+ "bench:split-csv": "node --max-old-space-size=8192 bench/bench-split-csv.mjs",
78
+ "serve:browser": "node examples/browser-threads/serve.mjs 8766",
79
+ "serve:split": "node examples/browser-threads/serve.mjs 8767 --no-isolation",
80
+ "serve:phone": "node examples/browser-threads/serve.mjs 8772 --no-isolation --lan",
81
+ "bench:stream": "node bench/bench-stream.mjs",
82
+ "bench:sort": "node --max-old-space-size=8192 bench/bench-sort.mjs",
83
+ "bench:vectors": "node --max-old-space-size=8192 bench/bench-vectors.mjs",
84
+ "mcp": "node mcp/server.mjs",
85
+ "bench:pack": "node --max-old-space-size=8192 bench/bench-pack.mjs",
86
+ "bench:plan": "node --max-old-space-size=8192 bench/bench-plan.mjs",
87
+ "bench:zones": "node --max-old-space-size=8192 bench/bench-zones.mjs"
88
+ },
89
+ "bin": {
90
+ "vectro": "bin/vectro.mjs",
91
+ "vectro-mcp": "mcp/server.mjs"
92
+ }
15
93
  }