@chaprola/mcp-server 1.5.0 → 1.6.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/dist/index.js CHANGED
@@ -164,6 +164,40 @@ server.resource("endpoints", "chaprola://endpoints", { description: "Chaprola AP
164
164
  server.resource("auth", "chaprola://auth", { description: "Chaprola authentication reference — API key model, BAA flow, credential recovery", mimeType: "text/markdown" }, async () => ({
165
165
  contents: [{ uri: "chaprola://auth", mimeType: "text/markdown", text: readRef("auth.md") }],
166
166
  }));
167
+ // --- Modular Resources (Tier 1 + Tier 2) ---
168
+ server.resource("quickstart", "chaprola://quickstart", { description: "Chaprola quickstart — auth, base URL, core workflow, and index of all feature references. READ THIS FIRST.", mimeType: "text/markdown" }, async () => ({
169
+ contents: [{ uri: "chaprola://quickstart", mimeType: "text/markdown", text: readRef("quickstart.md") }],
170
+ }));
171
+ server.resource("ref-import", "chaprola://ref/import", { description: "Import, export, list, download — all data I/O endpoints", mimeType: "text/markdown" }, async () => ({
172
+ contents: [{ uri: "chaprola://ref/import", mimeType: "text/markdown", text: readRef("ref-import.md") }],
173
+ }));
174
+ server.resource("ref-query", "chaprola://ref/query", { description: "Query, sort, index, merge, record CRUD — data operations", mimeType: "text/markdown" }, async () => ({
175
+ contents: [{ uri: "chaprola://ref/query", mimeType: "text/markdown", text: readRef("ref-query.md") }],
176
+ }));
177
+ server.resource("ref-pivot", "chaprola://ref/pivot", { description: "Pivot tables (GROUP BY) — row, column, aggregate functions", mimeType: "text/markdown" }, async () => ({
178
+ contents: [{ uri: "chaprola://ref/pivot", mimeType: "text/markdown", text: readRef("ref-pivot.md") }],
179
+ }));
180
+ server.resource("ref-mercury", "chaprola://ref/mercury", { description: "Mercury weighted scoring — rank records by multiple criteria", mimeType: "text/markdown" }, async () => ({
181
+ contents: [{ uri: "chaprola://ref/mercury", mimeType: "text/markdown", text: readRef("ref-mercury.md") }],
182
+ }));
183
+ server.resource("ref-programs", "chaprola://ref/programs", { description: "Chaprola .CS language — MOVE, LET, GET, PUT, SEEK, IF, GOTO, PRINT, secondary files, params", mimeType: "text/markdown" }, async () => ({
184
+ contents: [{ uri: "chaprola://ref/programs", mimeType: "text/markdown", text: readRef("ref-programs.md") }],
185
+ }));
186
+ server.resource("ref-huldra", "chaprola://ref/huldra", { description: "HULDRA nonlinear optimization — parameter fitting, model catalog", mimeType: "text/markdown" }, async () => ({
187
+ contents: [{ uri: "chaprola://ref/huldra", mimeType: "text/markdown", text: readRef("ref-huldra.md") }],
188
+ }));
189
+ server.resource("ref-deploy", "chaprola://ref/deploy", { description: "Static app deployment to chaprola.org/apps/", mimeType: "text/markdown" }, async () => ({
190
+ contents: [{ uri: "chaprola://ref/deploy", mimeType: "text/markdown", text: readRef("ref-deploy.md") }],
191
+ }));
192
+ server.resource("ref-email", "chaprola://ref/email", { description: "Email system — inbox, read, send, forward, attachments", mimeType: "text/markdown" }, async () => ({
193
+ contents: [{ uri: "chaprola://ref/email", mimeType: "text/markdown", text: readRef("ref-email.md") }],
194
+ }));
195
+ server.resource("ref-gotchas", "chaprola://ref/gotchas", { description: "Common Chaprola mistakes — language, API, and secondary file pitfalls", mimeType: "text/markdown" }, async () => ({
196
+ contents: [{ uri: "chaprola://ref/gotchas", mimeType: "text/markdown", text: readRef("ref-gotchas.md") }],
197
+ }));
198
+ server.resource("ref-auth", "chaprola://ref/auth", { description: "Authentication details — registration, login, BAA, MCP env vars", mimeType: "text/markdown" }, async () => ({
199
+ contents: [{ uri: "chaprola://ref/auth", mimeType: "text/markdown", text: readRef("ref-auth.md") }],
200
+ }));
167
201
  // --- MCP Prompts ---
168
202
  server.prompt("chaprola-guide", "Essential guide for working with Chaprola. Read this before writing any Chaprola source code.", async () => ({
169
203
  messages: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaprola/mcp-server",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "MCP server for Chaprola — agent-first data platform. Gives AI agents 46 tools for structured data storage, record CRUD, querying, schema inspection, web search, URL fetching, scheduled jobs, and execution via plain HTTP.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,30 @@
1
+ # Chaprola Quickstart
2
+
3
+ Base URL: `https://api.chaprola.org`
4
+ Auth: `Authorization: Bearer chp_your_api_key` on all protected endpoints.
5
+ Every request body's `userid` must match the authenticated username.
6
+
7
+ ## Core Workflow
8
+
9
+ ```bash
10
+ POST /import {userid, project, name: "STAFF", data: [{name: "Alice", salary: 95000}, ...]}
11
+ POST /compile {userid, project, name: "REPORT", source: "...", primary_format: "STAFF"}
12
+ POST /run {userid, project, name: "REPORT", primary_file: "STAFF", record: 1}
13
+ ```
14
+
15
+ ## Available References
16
+
17
+ Read only what your app needs:
18
+
19
+ | Resource | When to read |
20
+ |----------|-------------|
21
+ | `chaprola://ref/import` | Importing data, exporting, listing files |
22
+ | `chaprola://ref/query` | Filtering, sorting, indexing, joining data |
23
+ | `chaprola://ref/pivot` | GROUP BY / pivot tables |
24
+ | `chaprola://ref/mercury` | Weighted scoring / ranking |
25
+ | `chaprola://ref/programs` | Writing .CS source code (MOVE, LET, GET, PUT, SEEK, IF, GOTO, PRINT) |
26
+ | `chaprola://ref/huldra` | Nonlinear optimization / parameter fitting |
27
+ | `chaprola://ref/deploy` | Deploying static web apps to chaprola.org/apps/ |
28
+ | `chaprola://ref/email` | Sending/receiving email via @chaprola.org |
29
+ | `chaprola://ref/gotchas` | Common mistakes to avoid |
30
+ | `chaprola://ref/auth` | Registration, login, BAA, credentials |
@@ -0,0 +1,25 @@
1
+ # Authentication
2
+
3
+ ## API Key Model
4
+ All protected requests: `Authorization: Bearer chp_your_api_key`
5
+ Keys: `chp_` + 64 hex chars. Expire after 90 days.
6
+
7
+ ## Registration & Login
8
+ ```bash
9
+ POST /register {"username": "my-agent", "passcode": "16-chars-minimum-passcode"}
10
+ → {"api_key": "chp_..."}
11
+
12
+ POST /login {"username": "my-agent", "passcode": "..."}
13
+ → {"api_key": "chp_..."} # old keys remain valid until expiration
14
+ ```
15
+ - Passcode: 16-128 chars. Username: 3-40 chars, alphanumeric + hyphens/underscores.
16
+ - Rate limits: auth 5 rps, data 20 rps.
17
+
18
+ ## BAA
19
+ Only needed for PHI. Sign once: `POST /baa-text` → human reviews → `POST /sign-baa {userid, signatory_name}`.
20
+
21
+ ## MCP Environment Variables
22
+ | Variable | Description |
23
+ |----------|-------------|
24
+ | `CHAPROLA_USERNAME` | Your registered username |
25
+ | `CHAPROLA_API_KEY` | Your API key (`chp_...`) |
@@ -0,0 +1,24 @@
1
+ # App Deployment
2
+
3
+ Deploy static web apps (HTML/JS/CSS) to `https://chaprola.org/apps/{userid}/{project}/`.
4
+
5
+ ## Flow
6
+ ```bash
7
+ # 1. Get presigned upload URL
8
+ POST /app/deploy {userid, project} → {upload_url, staging_key, max_size_mb: 50}
9
+
10
+ # 2. Upload .zip or .tar.gz to upload_url
11
+ PUT upload_url --data-binary @app.zip
12
+
13
+ # 3. Extract and deploy
14
+ POST /app/deploy/process {userid, project, staging_key} → {url, files_deployed}
15
+ ```
16
+
17
+ ## Single file upload
18
+ ```bash
19
+ POST /app/upload {userid, project, path: "css/app.css"} → {upload_url, content_type}
20
+ PUT upload_url --data-binary @app.css
21
+ ```
22
+
23
+ Max 500 files per app. Blocked extensions: .php, .py, .sh, .exe.
24
+ Content-Type set automatically from extension.
@@ -0,0 +1,20 @@
1
+ # Email (@chaprola.org)
2
+
3
+ Every account gets `{username}@chaprola.org`.
4
+
5
+ ## Endpoints
6
+ - `POST /email/inbox {address, limit?, before?}` → `{emails: [...], total}`
7
+ - `POST /email/read {address, message_id}` → `{email: {from, to, subject, text, html, attachments}}`
8
+ - `POST /email/send {from, to, subject, text, html?}` → `{status, message_id}`
9
+ - `POST /email/delete {address, message_id}` → `{status}`
10
+ - `POST /email/forward {from, message_id, to}` → `{status, resend_message_id}`
11
+
12
+ ## Attachments
13
+ `/email/read` returns presigned download URLs (1hr expiry) for each attachment.
14
+ `/email/forward` re-attaches stored attachments automatically.
15
+
16
+ ## Rate limits
17
+ 20 emails/day per user, 3 emails/minute. Outbound emails are AI content-moderated (blocks spam/PHI).
18
+
19
+ ## Delegates
20
+ Agents can send as another user if listed in their delegate file. E.g., tawni can send `"from": "charles"`.
@@ -0,0 +1,23 @@
1
+ # Gotchas
2
+
3
+ ## Language
4
+ - **No parentheses in LET.** `LET result = price * qty` only. For `price * (qty + bonus)`: use `LET temp = qty + bonus` then `LET result = price * temp`.
5
+ - **IF EQUAL compares literal to location.** To compare two locations, copy both to U buffer first.
6
+ - **MOVE length must match field width.** If `name` is 8 chars wide, `MOVE P.name U.1 20` bleeds into adjacent fields.
7
+ - **DEFINE VARIABLE names must not collide with field names.** If format has `balance`, don't `DEFINE VARIABLE balance R41`.
8
+ - **R-variables are 64-bit floats.** `7 / 2 = 3.5`. Use PUT with `I` format for integer display.
9
+ - **FIND returns 0 on no match.** Always check `IF match EQ 0` before READ.
10
+ - **PRINT 0 clears the U buffer.** No need to manually clear between prints.
11
+ - **Statement numbers are labels, not line numbers.** Only number GOTO/CALL targets.
12
+
13
+ ## API
14
+ - **userid must match authenticated user.** 403 on mismatch.
15
+ - **Login invalidates the old key.** Save the new one immediately.
16
+ - **Async for large datasets.** `/run` with `async: true` for >100K records (API Gateway 30s timeout).
17
+ - **secondary_format is a string**, not an array.
18
+ - **Data files expire.** Default 90 days. Override with `expires_in_days` on import.
19
+ - **API keys expire after 90 days.** Re-login to get a new key.
20
+
21
+ ## Secondary Files
22
+ - **One at a time.** CLOSE before opening another.
23
+ - **CLOSE flushes writes.** Always CLOSE before END if you wrote to the secondary file.
@@ -0,0 +1,78 @@
1
+ # HULDRA — Nonlinear Optimization
2
+
3
+ HULDRA finds optimal parameter values for a mathematical model by minimizing the difference between predictions and observed data.
4
+
5
+ ## R-Variable Interface
6
+
7
+ | Range | Purpose | Who sets it |
8
+ |-------|---------|-------------|
9
+ | R1–R20 | Elements (parameters to optimize) | HULDRA sets before each run |
10
+ | R21–R40 | Objectives (error metrics) | Your program computes these |
11
+ | R41–R50 | Scratch space | Your program's temp variables |
12
+
13
+ ## POST /optimize
14
+ ```json
15
+ {
16
+ "userid": "...", "project": "fit", "program": "SALFIT", "primary_file": "EMP",
17
+ "elements": [
18
+ {"index": 1, "label": "slope", "start": 5000, "min": 0, "max": 20000, "delta": 10},
19
+ {"index": 2, "label": "base", "start": 40000, "min": 0, "max": 100000, "delta": 100}
20
+ ],
21
+ "objectives": [
22
+ {"index": 1, "label": "SSR", "goal": 0.0, "weight": 1.0}
23
+ ],
24
+ "max_iterations": 100,
25
+ "async_exec": true
26
+ }
27
+ ```
28
+
29
+ Async: `POST /optimize/status {userid, project, job_id}`
30
+
31
+ ## Delta Guidance
32
+ - Dollar amounts: `0.01` to `1.0`
33
+ - Rates/percentages: `0.001` to `0.01`
34
+ - Counts: `0.1` to `1.0`
35
+
36
+ ## Performance
37
+ HULDRA runs your program `1 + 2 × N_elements` times per iteration. Lambda timeout: 900s. Sample large datasets (200-500 records) before optimizing.
38
+
39
+ ## Complete Example: Linear Fit
40
+ `salary = R1 × years_exp + R2`
41
+
42
+ ```chaprola
43
+ DEFINE VARIABLE REC R41
44
+ DEFINE VARIABLE YRS R42
45
+ DEFINE VARIABLE SAL R43
46
+ DEFINE VARIABLE PRED R44
47
+ DEFINE VARIABLE RESID R45
48
+ DEFINE VARIABLE SSR R46
49
+
50
+ LET SSR = 0 // MUST initialize to 0
51
+ LET REC = 1
52
+ 100 SEEK REC
53
+ IF EOF GOTO 200
54
+ GET YRS FROM P.years_exp
55
+ GET SAL FROM P.salary
56
+ LET PRED = R1 * YRS
57
+ LET PRED = PRED + R2
58
+ LET RESID = PRED - SAL
59
+ LET RESID = RESID * RESID
60
+ LET SSR = SSR + RESID
61
+ LET REC = REC + 1
62
+ GOTO 100
63
+ 200 LET R21 = SSR
64
+ END
65
+ ```
66
+
67
+ ## Model Catalog
68
+
69
+ | Model | Formula | When to use |
70
+ |-------|---------|-------------|
71
+ | Linear | `y = R1*x + R2` | Proportional relationships |
72
+ | Multi-linear | `y = R1*x1 + R2*x2 + R3` | Multiple factors |
73
+ | Quadratic | `y = R1*x^2 + R2*x + R3` | Accelerating curves |
74
+ | Exponential | `y = R1 * EXP(R2*x)` | Compound growth |
75
+ | Exp. decay | `y = R1 * EXP(-R2*x) + R3` | Decay, cooling |
76
+ | Power law | `y = R1 * POW(x, R2)` | Scaling laws |
77
+ | Logarithmic | `y = R1 * LOG(x) + R2` | Diminishing returns |
78
+ | Logistic | `y = R1 / (1 + EXP(-R2*(x-R3)))` | S-curves, saturation |
@@ -0,0 +1,34 @@
1
+ # Import / Export / Files
2
+
3
+ ## POST /import
4
+ `{userid, project, name, data: [{...}, ...], format?, expires_in_days?, force?}`
5
+ Returns: `{records, fields, record_length, format_file, data_file}`
6
+
7
+ ```bash
8
+ POST /import {userid, project, name: "STAFF", data: [{"name": "Alice", "salary": 95000}]}
9
+ ```
10
+
11
+ Field widths auto-sized from longest value. Default expiry: 90 days. Override with `expires_in_days`.
12
+
13
+ ## Large File Upload (presigned URL)
14
+ ```bash
15
+ POST /import-url {userid, project, name} → {upload_url, staging_key}
16
+ # PUT your JSON to upload_url
17
+ POST /import-process {userid, project, name, staging_key} → same as /import
18
+ ```
19
+
20
+ ## POST /import-download
21
+ `{userid, project, name, url, instructions?, max_rows?}`
22
+ Imports directly from URL. Supports: CSV, TSV, JSON, NDJSON, Parquet, Excel (.xlsx/.xls).
23
+ Optional `instructions` for AI schema inference. Max 1M records.
24
+
25
+ ## POST /export
26
+ `{userid, project, name, format?}` → `{data: [...records]}`
27
+ Optional `format: "fhir"` for FHIR JSON reconstruction.
28
+
29
+ ## POST /list
30
+ `{userid, project, pattern?}` → `{files: [...], total}`
31
+
32
+ ## POST /download
33
+ `{userid, project, file, type}` → `{download_url, expires_in, size_bytes}`
34
+ Type: `data`, `format`, `source`, `proc`, `output`.
@@ -0,0 +1,23 @@
1
+ # Mercury (Weighted Scoring)
2
+
3
+ Add `mercury` to `/query` to score and rank records by weighted criteria.
4
+
5
+ ```json
6
+ POST /query {
7
+ "userid": "...", "project": "...", "file": "CANDIDATES",
8
+ "mercury": {
9
+ "scores": [
10
+ {"field": "experience_years", "weight": 0.4, "direction": "higher_better"},
11
+ {"field": "interview_score", "weight": 0.35, "direction": "higher_better"},
12
+ {"field": "salary_ask", "weight": 0.25, "direction": "lower_better"}
13
+ ]
14
+ },
15
+ "limit": 10
16
+ }
17
+ ```
18
+
19
+ Returns records ranked by composite score (0-100). Each record includes `_mercury_score` and per-field `_mercury_{field}` component scores.
20
+
21
+ Fields are normalized min-max within the dataset. `direction` controls whether higher or lower raw values score better. Weights must sum to 1.0.
22
+
23
+ Combine with `where` to pre-filter before scoring.
@@ -0,0 +1,32 @@
1
+ # Pivot (GROUP BY)
2
+
3
+ Chaprola's pivot IS GROUP BY. Add `pivot` to `/query`.
4
+
5
+ ## Simple aggregation (no cross-tab)
6
+ ```json
7
+ POST /query {
8
+ "userid": "...", "project": "...", "file": "STAFF",
9
+ "pivot": {
10
+ "row": "department",
11
+ "column": "",
12
+ "values": [
13
+ {"field": "department", "function": "count"},
14
+ {"field": "salary", "function": "avg"}
15
+ ]
16
+ }
17
+ }
18
+ ```
19
+ SQL equivalent: `SELECT department, COUNT(*), AVG(salary) FROM staff GROUP BY department`
20
+
21
+ ## Cross-tabulation
22
+ ```json
23
+ "pivot": {
24
+ "row": "department",
25
+ "column": "year",
26
+ "values": [{"field": "revenue", "function": "sum"}],
27
+ "totals": true
28
+ }
29
+ ```
30
+ SQL equivalent: `SELECT department, year, SUM(revenue) FROM sales GROUP BY department, year`
31
+
32
+ Supported functions: `count`, `sum`, `avg`, `min`, `max`, `stddev`.
@@ -0,0 +1,85 @@
1
+ # Chaprola Programs (.CS Source)
2
+
3
+ ## Compile & Run
4
+ ```bash
5
+ POST /compile {userid, project, name: "REPORT", source: "...", primary_format: "STAFF", secondary_format?: "DEPTS"}
6
+ POST /run {userid, project, name: "REPORT", primary_file: "STAFF", record: 1, async?: true, nophi?: true}
7
+ POST /run/status {userid, project, job_id} # poll async jobs
8
+ POST /publish {userid, project, name, primary_file, acl?: "public|authenticated|owner|token"}
9
+ ```
10
+
11
+ ## Memory Regions
12
+
13
+ | Prefix | Description |
14
+ |--------|-------------|
15
+ | `P` | Primary data file (current record) |
16
+ | `S` | Secondary data file (current record) |
17
+ | `U` | User buffer (output scratch) |
18
+ | `X` | System text (date, time, filenames) |
19
+
20
+ ## Language Essentials
21
+
22
+ ```chaprola
23
+ // Loop through records
24
+ DEFINE VARIABLE rec R41
25
+ LET rec = 1
26
+ 100 SEEK rec
27
+ IF EOF GOTO 900
28
+ MOVE P.name U.1 20 // copy field to output buffer
29
+ GET sal FROM P.salary // numeric field → R variable
30
+ PUT sal INTO U.22 10 D 2 // R variable → formatted output
31
+ PRINT 0 // output full U buffer, clear it
32
+ LET rec = rec + 1
33
+ GOTO 100
34
+ 900 END
35
+ ```
36
+
37
+ - `PRINT 0` — output entire U buffer and clear. `PRINT N` — output exactly N chars.
38
+ - `MOVE BLANKS U.1 80` — clear a region. `MOVE "literal" U.1 7` — move literal.
39
+ - `IF EQUAL "text" U.50 4 GOTO 200` — compare literal to memory location.
40
+ - `U.name` — named positions (auto-allocated by compiler): `MOVE P.name U.name 20`
41
+ - `DEFINE VARIABLE counter R41` — alias R-variable. **Use R41-R50** (R1-R40 reserved for HULDRA).
42
+
43
+ ## PUT Format Codes
44
+
45
+ | Code | Description | Example |
46
+ |------|-------------|---------|
47
+ | `D` | Dollar with commas | `$1,234.56` |
48
+ | `F` | Fixed decimal | `1234.56` |
49
+ | `I` | Integer (right-justified) | ` 1234` |
50
+ | `E` | Scientific notation | `1.23E+03` |
51
+
52
+ Syntax: `PUT R41 INTO U.30 10 D 2` — (R-var, location, width, format, decimals)
53
+
54
+ ## Math
55
+
56
+ ```chaprola
57
+ LET R42 = R41 + 1 // one operation per LET
58
+ LET R43 = EXP R41 // also: LOG, SQRT, ABS
59
+ LET R44 = POW R41 R42 // R41^R42
60
+ ```
61
+
62
+ ## Secondary Files (FIND/JOIN)
63
+
64
+ ```chaprola
65
+ OPEN "DEPARTMENTS" 0 // open secondary file
66
+ FIND match FROM S.dept_code 3 USING P.dept_code
67
+ IF match EQ 0 GOTO 200 // 0 = no match
68
+ READ match // load matched record
69
+ MOVE S.dept_name U.30 15
70
+ WRITE match // write back if modified
71
+ CLOSE // flush + close
72
+ ```
73
+
74
+ Compile with: `secondary_format: "DEPARTMENTS"`
75
+
76
+ ## Parameterized Reports (PARAM.name)
77
+ ```chaprola
78
+ MOVE PARAM.deck U.1 20 // string param → U buffer
79
+ LET lvl = PARAM.level // numeric param → R variable
80
+ ```
81
+ Publish, then call: `POST /report?userid=X&project=Y&name=Z&deck=kanji&level=3`
82
+ Discover params: `POST /report/params {userid, project, name}`
83
+
84
+ ## Common Field Widths
85
+ ISO datetime: 20, UUID: 36, email: 50, short ID: 8-12, dollar: 10, phone: 15.
@@ -0,0 +1,40 @@
1
+ # Query / Sort / Index / Merge
2
+
3
+ ## POST /query
4
+ ```json
5
+ {
6
+ "userid": "...", "project": "...", "file": "STAFF",
7
+ "where": [{"field": "salary", "op": "gt", "value": 80000}],
8
+ "where_logic": "and",
9
+ "select": ["name", "salary"],
10
+ "aggregate": [{"field": "salary", "function": "avg"}],
11
+ "order_by": "salary desc",
12
+ "limit": 100,
13
+ "offset": 0
14
+ }
15
+ ```
16
+ Returns: `{records: [...], total, fields}`
17
+
18
+ WHERE operators: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `between`, `contains`, `starts_with`, `ends_with`
19
+
20
+ ## JOIN
21
+ Add `join` to /query:
22
+ ```json
23
+ "join": {"file": "DEPARTMENTS", "on": {"left": "dept_id", "right": "dept_id"}, "type": "inner"}
24
+ ```
25
+ Types: `inner`, `left`, `right`, `full`. Optional `pre_sorted: true` for merge join.
26
+
27
+ ## POST /sort
28
+ `{userid, project, file, sort_by: "salary desc"}`
29
+
30
+ ## POST /index
31
+ `{userid, project, file, key_fields: ["dept_id"], output: "STAFF_BY_DEPT"}`
32
+
33
+ ## POST /merge
34
+ `{userid, project, file_a, file_b, output, key}`
35
+
36
+ ## Record CRUD
37
+ - `POST /insert-record {userid, project, file, record: {field: "value"}}`
38
+ - `POST /update-record {userid, project, file, where: [...], set: {field: "value"}}`
39
+ - `POST /delete-record {userid, project, file, where: [...]}`
40
+ - `POST /consolidate {userid, project, file}` — merge .MRG into .DA