@mulmoclaude/core 4.1.0 → 4.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.
- package/assets/helps/collection-skills.md +19 -0
- package/assets/helps/error-recovery.md +63 -0
- package/dist/collection/registry/server/index.cjs +1 -1
- package/dist/collection/registry/server/index.js +1 -1
- package/dist/collection/server/index.cjs +3 -1
- package/dist/collection/server/index.js +2 -2
- package/dist/collection/server/manageTool.d.ts +29 -0
- package/dist/collection-watchers/index.cjs +1 -1
- package/dist/collection-watchers/index.js +1 -1
- package/dist/feeds/server/index.cjs +1 -1
- package/dist/feeds/server/index.js +1 -1
- package/dist/google/index.js +2 -2
- package/dist/google/index.js.map +1 -1
- package/dist/{server-dijB4BYv.js → server-L12n8We3.js} +211 -10
- package/dist/{server-dijB4BYv.js.map → server-L12n8We3.js.map} +1 -1
- package/dist/{server-DSrRdPvf.cjs → server-meS6kjBN.cjs} +221 -8
- package/dist/{server-DSrRdPvf.cjs.map → server-meS6kjBN.cjs.map} +1 -1
- package/package.json +1 -1
|
@@ -6,9 +6,10 @@ import { C as actionVisible, O as firstUnknownDefault, _ as uniqueRefTargets, b
|
|
|
6
6
|
import { B as SCHEMA_FILE$1, C as CollectionQueryZ, J as isBackendUnavailable, K as safeSlugName, O as isRegularFile, S as compileJsonlQuery, U as resolveDataDir, V as isContainedInRoot, W as resolveTemplatePath, X as archiveDir, b as queryCsv, c as resolveMutateSet, d as storeFor, f as checkpointSqliteDatabase, ht as stagingSkillDir, i as resolvePrimaryField, it as isPresetSlug$1, j as resolveCreateItemId, m as cacheDir, n as discoverCollections, nt as getWorkspaceRoot, ot as log, r as loadCollection, s as CollectionSchemaZ, u as readOnlyRefusal, y as normalizeCsvValue } from "./discovery-D88x2DmW.js";
|
|
7
7
|
import { ingestStatePath } from "./feeds/paths.js";
|
|
8
8
|
import { mirrorSkillWrite } from "./skill-bridge/index.js";
|
|
9
|
+
import { constants } from "node:fs";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
11
|
-
import { cp, lstat, mkdir, open, readFile, readdir, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
12
|
+
import { cp, lstat, mkdir, open, readFile, readdir, realpath, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
//#region src/collection/server/skillAssets.ts
|
|
14
15
|
/** Read a collection's custom-view HTML, path-safely. `viewFile` is a
|
|
@@ -1556,6 +1557,26 @@ var SCHEMA_FILE = "schema.json";
|
|
|
1556
1557
|
var SCHEMA_DOCS_FILE = "collection-skills.md";
|
|
1557
1558
|
/** Cap the rejected-schema issue list so a deeply-broken schema can't flood the result. */
|
|
1558
1559
|
var MAX_SCHEMA_ISSUES = 20;
|
|
1560
|
+
/** Cap the rows one putItems call may write. `putOneItem` validates and
|
|
1561
|
+
* writes one record at a time, so a large `itemsFile` holds the tool call
|
|
1562
|
+
* open for minutes. Over the cap the call is refused WHOLE — a truncating
|
|
1563
|
+
* write that reported success would leave a half-filled collection nobody
|
|
1564
|
+
* knows is half-filled. */
|
|
1565
|
+
var MAX_PUT_ITEMS = 1e3;
|
|
1566
|
+
/** Refuse an `itemsFile` larger than this, from `stat` and before any read.
|
|
1567
|
+
* The row cap alone cannot bound the work: the file has to be read and parsed
|
|
1568
|
+
* WHOLE before there are rows to count, so a huge blob is paid for in full
|
|
1569
|
+
* first. 8 MiB is far past what 1000 records need and far short of trouble. */
|
|
1570
|
+
var MAX_ITEMS_FILE_BYTES = 8388608;
|
|
1571
|
+
/** `itemsFile` is opened read-only, without following a symlink, and without
|
|
1572
|
+
* blocking on a fifo — see `openContainedItemsFile`.
|
|
1573
|
+
*
|
|
1574
|
+
* `O_NOFOLLOW` and `O_NONBLOCK` are POSIX-only: on Windows they are absent, and
|
|
1575
|
+
* `x | undefined` is `x`, so the flags silently soften to a plain read-only
|
|
1576
|
+
* open. They are hardening where they exist, never the guarantee — the symlink
|
|
1577
|
+
* refusal is an explicit `lstat` (`verifyOpenedItemsFile`) so it holds on every
|
|
1578
|
+
* platform. `?? 0` states that rather than leaving it to coercion. */
|
|
1579
|
+
var OPEN_ITEMS_FILE_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
|
|
1559
1580
|
/** The workspace help-docs dir both hosts seed (`@mulmoclaude/core/workspace-setup`
|
|
1560
1581
|
* syncs the bundled assets here) — the user-editable copy schemaDocs prefers. */
|
|
1561
1582
|
var HELPS_DIR = "config/helps";
|
|
@@ -1756,13 +1777,167 @@ async function handleQueryItems(collection, queryArg, deps) {
|
|
|
1756
1777
|
rows
|
|
1757
1778
|
});
|
|
1758
1779
|
}
|
|
1780
|
+
/** Rewrite a sandbox-mount prefix to the host's workspace root, so the path the
|
|
1781
|
+
* agent wrote to and the file this process reads are the same bytes. Anything
|
|
1782
|
+
* not under the mount is returned untouched — a host with no sandbox hands the
|
|
1783
|
+
* agent real paths already. */
|
|
1784
|
+
function toHostWorkspacePath(absPath, sandboxRoot, workspaceRoot) {
|
|
1785
|
+
if (!sandboxRoot) return absPath;
|
|
1786
|
+
if (absPath === sandboxRoot) return workspaceRoot;
|
|
1787
|
+
const prefix = sandboxRoot.endsWith("/") ? sandboxRoot : `${sandboxRoot}/`;
|
|
1788
|
+
if (!absPath.startsWith(prefix)) return absPath;
|
|
1789
|
+
return path.join(workspaceRoot, ...absPath.slice(prefix.length).split("/"));
|
|
1790
|
+
}
|
|
1791
|
+
/** The host path an `itemsFile` names — translated out of the sandbox, and
|
|
1792
|
+
* required to land INSIDE the workspace.
|
|
1793
|
+
*
|
|
1794
|
+
* Containment is not tidiness. `manageCollection` is always available to a
|
|
1795
|
+
* sandboxed agent, and an unconstrained absolute path would turn this
|
|
1796
|
+
* host-side handler into a read primitive for the whole host filesystem —
|
|
1797
|
+
* point it at any JSON array the server user can open, store the rows, read
|
|
1798
|
+
* them back with `getItems`. The sandbox mounts a few app directories besides
|
|
1799
|
+
* the workspace, but nothing that gives the agent that reach, and the
|
|
1800
|
+
* workspace is where its own generated files land — so confining reads to it
|
|
1801
|
+
* denies the primitive without costing the feature anything.
|
|
1802
|
+
*
|
|
1803
|
+
* This is the CHEAP check, for the ordinary case and a precise message. The
|
|
1804
|
+
* binding one is on the opened descriptor (`verifyOpenedItemsFile`) — a path
|
|
1805
|
+
* checked here and read again later is a path that can change in between. */
|
|
1806
|
+
function resolveItemsFilePath(itemsFile, deps) {
|
|
1807
|
+
const root = resolveBase(deps);
|
|
1808
|
+
const hostPath = toHostWorkspacePath(itemsFile, deps.sandboxWorkspacePath, root);
|
|
1809
|
+
if (!isContainedInRoot(hostPath, root)) return outsideWorkspaceRefusal(defangForPrompt(itemsFile));
|
|
1810
|
+
return { hostPath };
|
|
1811
|
+
}
|
|
1812
|
+
function outsideWorkspaceRefusal(shown) {
|
|
1813
|
+
return `manageCollection: \`itemsFile\` must be inside the workspace — '${shown}' is not, and the host reads this file on your behalf. Write the generated rows under the workspace and pass that path.`;
|
|
1814
|
+
}
|
|
1815
|
+
function symlinkRefusal(shown) {
|
|
1816
|
+
return `manageCollection: \`itemsFile\` '${shown}' is a symbolic link. Pass the real path of a regular file inside the workspace.`;
|
|
1817
|
+
}
|
|
1818
|
+
function openItemsFileRefusal(err, shown) {
|
|
1819
|
+
if (isErrorWithCode(err) && (err.code === "ELOOP" || err.code === "EMLINK")) return symlinkRefusal(shown);
|
|
1820
|
+
return `manageCollection: could not read \`itemsFile\` '${shown}' — ${defangForPrompt(errorMessage(err))}. It must exist inside the workspace and be readable by the host.`;
|
|
1821
|
+
}
|
|
1822
|
+
/** Everything decided about the file, decided about the OPEN DESCRIPTOR rather
|
|
1823
|
+
* than about the path a second time.
|
|
1824
|
+
*
|
|
1825
|
+
* Re-`stat`ing and re-`readFile`ing the pathname would leave a TOCTOU window
|
|
1826
|
+
* the containment check cannot close: the caller is a sandboxed agent with
|
|
1827
|
+
* write access to the workspace, so it can point `rows.json` at an in-workspace
|
|
1828
|
+
* file, call the tool, and swap the symlink to a host file outside the mount
|
|
1829
|
+
* while the first `await` is pending — restoring exactly the read primitive
|
|
1830
|
+
* containment exists to deny. Bound to one descriptor, a swap after the open
|
|
1831
|
+
* changes nothing about the bytes this call goes on to read.
|
|
1832
|
+
*
|
|
1833
|
+
* The `dev`/`ino` comparison is what ties the two together: it proves the
|
|
1834
|
+
* descriptor's inode is the one reachable at a contained path. A hardlink
|
|
1835
|
+
* would satisfy it, but the agent cannot create one across the mount boundary
|
|
1836
|
+
* — only the workspace is mounted, and a hardlink cannot cross filesystems. */
|
|
1837
|
+
async function verifyOpenedItemsFile(handle, hostPath, root, shown) {
|
|
1838
|
+
const opened = await handle.stat();
|
|
1839
|
+
if (!opened.isFile()) return `manageCollection: \`itemsFile\` '${shown}' is not a regular file. It must be a JSON file holding an array of record objects.`;
|
|
1840
|
+
if (opened.size > 8388608) return `manageCollection: \`itemsFile\` '${shown}' is ${opened.size} bytes, over the limit of ${MAX_ITEMS_FILE_BYTES}. Nothing was read; split the rows across several files and call once per file.`;
|
|
1841
|
+
let real;
|
|
1842
|
+
let atPath;
|
|
1843
|
+
let link;
|
|
1844
|
+
try {
|
|
1845
|
+
link = await lstat(hostPath);
|
|
1846
|
+
real = await realpath(hostPath);
|
|
1847
|
+
atPath = await stat(real);
|
|
1848
|
+
} catch (err) {
|
|
1849
|
+
return openItemsFileRefusal(err, shown);
|
|
1850
|
+
}
|
|
1851
|
+
if (link.isSymbolicLink()) return symlinkRefusal(shown);
|
|
1852
|
+
if (!isContainedInRoot(real, root)) return outsideWorkspaceRefusal(shown);
|
|
1853
|
+
if (atPath.ino !== opened.ino || atPath.dev !== opened.dev) return `manageCollection: \`itemsFile\` '${shown}' changed while it was being opened. Nothing was read; write the file, then call putItems.`;
|
|
1854
|
+
return { size: opened.size };
|
|
1855
|
+
}
|
|
1856
|
+
/** Open the file ONCE, then prove that descriptor is the contained file.
|
|
1857
|
+
* `O_NOFOLLOW` refuses a symlink outright rather than resolving it, and
|
|
1858
|
+
* `O_NONBLOCK` keeps a fifo from parking this call on `open` itself — the
|
|
1859
|
+
* descriptor is what `verifyOpenedItemsFile` then judges. */
|
|
1860
|
+
async function openContainedItemsFile(hostPath, root, shown) {
|
|
1861
|
+
let handle;
|
|
1862
|
+
try {
|
|
1863
|
+
handle = await open(hostPath, OPEN_ITEMS_FILE_FLAGS);
|
|
1864
|
+
} catch (err) {
|
|
1865
|
+
return openItemsFileRefusal(err, shown);
|
|
1866
|
+
}
|
|
1867
|
+
const verified = await verifyOpenedItemsFile(handle, hostPath, root, shown);
|
|
1868
|
+
if (typeof verified !== "string") return {
|
|
1869
|
+
handle,
|
|
1870
|
+
size: verified.size
|
|
1871
|
+
};
|
|
1872
|
+
await handle.close();
|
|
1873
|
+
return verified;
|
|
1874
|
+
}
|
|
1875
|
+
/** Read the descriptor into a buffer bounded by the size that was CHECKED,
|
|
1876
|
+
* rather than to EOF.
|
|
1877
|
+
*
|
|
1878
|
+
* `FileHandle.readFile()` reads until end-of-file, which the size check cannot
|
|
1879
|
+
* bound: the agent can hold the same file open and append to it after the
|
|
1880
|
+
* `stat` and before the read, and because appending does not change the inode,
|
|
1881
|
+
* the identity check cannot see it either. A 2-byte file that passed the gate
|
|
1882
|
+
* can hand back gigabytes. Allocating `size + 1` makes the cap hold on the
|
|
1883
|
+
* bytes actually taken, whatever the file does meanwhile — and that one extra
|
|
1884
|
+
* byte is what detects the growth, so a file being written under us is refused
|
|
1885
|
+
* rather than parsed as the truncated half it would otherwise look like. */
|
|
1886
|
+
async function readItemsBytes(handle, size, shown) {
|
|
1887
|
+
const buffer = Buffer.allocUnsafe(size + 1);
|
|
1888
|
+
const { bytesRead } = await handle.read(buffer, 0, size + 1, 0);
|
|
1889
|
+
if (bytesRead > size) return `manageCollection: \`itemsFile\` '${shown}' grew while it was being read. Nothing was written; finish writing the file, then call putItems.`;
|
|
1890
|
+
return { raw: buffer.subarray(0, bytesRead).toString("utf-8") };
|
|
1891
|
+
}
|
|
1892
|
+
function parseItemsJson(raw, shown) {
|
|
1893
|
+
let parsed;
|
|
1894
|
+
try {
|
|
1895
|
+
parsed = JSON.parse(raw);
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
return `manageCollection: \`itemsFile\` '${shown}' could not be read as JSON — ${defangForPrompt(errorMessage(err))}. It must hold a JSON array of record objects.`;
|
|
1898
|
+
}
|
|
1899
|
+
if (!isRecordArray(parsed) || parsed.length === 0) return `manageCollection: \`itemsFile\` '${shown}' must hold a non-empty JSON array of record objects.`;
|
|
1900
|
+
return parsed;
|
|
1901
|
+
}
|
|
1902
|
+
/** Read the rows an `itemsFile` holds. Every failure comes back as tool text,
|
|
1903
|
+
* never a throw: a bad path or a malformed file is something the agent can fix
|
|
1904
|
+
* and retry. The path it passed is what the messages name, even when the bytes
|
|
1905
|
+
* were read from the translated host path — it is the one the agent recognises. */
|
|
1906
|
+
async function readItemsFile(itemsFile, deps) {
|
|
1907
|
+
const shown = defangForPrompt(itemsFile);
|
|
1908
|
+
const resolved = resolveItemsFilePath(itemsFile, deps);
|
|
1909
|
+
if (typeof resolved === "string") return resolved;
|
|
1910
|
+
const opened = await openContainedItemsFile(resolved.hostPath, resolveBase(deps), shown);
|
|
1911
|
+
if (typeof opened === "string") return opened;
|
|
1912
|
+
let read;
|
|
1913
|
+
try {
|
|
1914
|
+
read = await readItemsBytes(opened.handle, opened.size, shown);
|
|
1915
|
+
} catch (err) {
|
|
1916
|
+
return openItemsFileRefusal(err, shown);
|
|
1917
|
+
} finally {
|
|
1918
|
+
await opened.handle.close();
|
|
1919
|
+
}
|
|
1920
|
+
return typeof read === "string" ? read : parseItemsJson(read.raw, shown);
|
|
1921
|
+
}
|
|
1922
|
+
/** The rows this call will write, from whichever source it named. The cap is
|
|
1923
|
+
* checked here — on the resolved rows, so it holds for `items` and `itemsFile`
|
|
1924
|
+
* alike — and BEFORE the first write, so an over-cap call leaves the
|
|
1925
|
+
* collection exactly as it found it instead of half-filled. */
|
|
1926
|
+
async function resolvePutRows(args, deps) {
|
|
1927
|
+
const rows = args.itemsFile === void 0 ? args.items : await readItemsFile(args.itemsFile, deps);
|
|
1928
|
+
if (typeof rows === "string") return rows;
|
|
1929
|
+
if (rows.length > 1e3) return `manageCollection: refused — ${rows.length} rows is over the putItems limit of ${MAX_PUT_ITEMS}. Nothing was written; split them across several calls.`;
|
|
1930
|
+
return rows;
|
|
1931
|
+
}
|
|
1759
1932
|
async function handlePutItems(collection, args, deps) {
|
|
1760
1933
|
const store = storeFor(collection, { workspaceRoot: deps.workspaceRoot });
|
|
1761
1934
|
const { write } = store;
|
|
1762
1935
|
if (!write) return `manageCollection: ${readOnlyRefusal(collection.slug)} (its records are the rows of '${collection.schema.dataSource?.path}'; edit that file to change the data).`;
|
|
1936
|
+
const rows = await resolvePutRows(args, deps);
|
|
1937
|
+
if (typeof rows === "string") return rows;
|
|
1763
1938
|
const written = [];
|
|
1764
1939
|
const rejected = [];
|
|
1765
|
-
for (const record of
|
|
1940
|
+
for (const record of rows) {
|
|
1766
1941
|
const outcome = await putOneItem(collection, store, write, record, args.mode, deps);
|
|
1767
1942
|
if (outcome.written) written.push(outcome.written);
|
|
1768
1943
|
if (outcome.rejected) rejected.push(outcome.rejected);
|
|
@@ -1825,14 +2000,36 @@ function parsePutMode(mode) {
|
|
|
1825
2000
|
function isRecordArray(value) {
|
|
1826
2001
|
return isUnknownArray(value) && value.every(isRecord);
|
|
1827
2002
|
}
|
|
2003
|
+
/** `items` and `itemsFile` are ALTERNATIVES, never a pair: two row sets in one
|
|
2004
|
+
* call has no correct reading — honouring one silently discards the other, and
|
|
2005
|
+
* concatenating them writes rows the caller never asked to write together. So
|
|
2006
|
+
* both present is refused, rather than resolved by precedence.
|
|
2007
|
+
*
|
|
2008
|
+
* A relative `itemsFile` is refused too, for a reason the agent cannot see from
|
|
2009
|
+
* where it stands: this tool runs inside the HOST'S SERVER PROCESS, whose
|
|
2010
|
+
* working directory is not the agent's. A relative path would not reliably
|
|
2011
|
+
* fail — it would resolve against an unrelated directory and either miss or,
|
|
2012
|
+
* worse, read a different file that happens to share the name. */
|
|
2013
|
+
function parseItemsSource(items, itemsFile) {
|
|
2014
|
+
if (items !== void 0 && itemsFile !== void 0) return "manageCollection: pass either `items` or `itemsFile` for putItems, not both — two row sets in one call is ambiguous.";
|
|
2015
|
+
if (itemsFile !== void 0) return parseItemsFile(itemsFile);
|
|
2016
|
+
if (!isRecordArray(items) || items.length === 0) return "manageCollection: putItems needs `items` (a non-empty array of record objects) or `itemsFile` (an absolute path to a JSON file of them — use it for a set a script generated, so the rows never pass through your context).";
|
|
2017
|
+
return { items };
|
|
2018
|
+
}
|
|
2019
|
+
function parseItemsFile(itemsFile) {
|
|
2020
|
+
const file = typeof itemsFile === "string" ? itemsFile.trim() : "";
|
|
2021
|
+
if (!file) return "manageCollection: `itemsFile` must be a non-empty absolute path to a JSON file of record objects.";
|
|
2022
|
+
if (!path.isAbsolute(file)) return `manageCollection: \`itemsFile\` must be an ABSOLUTE path — '${defangForPrompt(file)}' is relative, and this tool runs in the host's server process, whose working directory is not yours. Pass the full path.`;
|
|
2023
|
+
return { itemsFile: file };
|
|
2024
|
+
}
|
|
1828
2025
|
function parsePutItems(args, slug) {
|
|
1829
|
-
const
|
|
1830
|
-
if (
|
|
1831
|
-
const putMode = parsePutMode(mode);
|
|
2026
|
+
const source = parseItemsSource(args.items, args.itemsFile);
|
|
2027
|
+
if (typeof source === "string") return source;
|
|
2028
|
+
const putMode = parsePutMode(args.mode);
|
|
1832
2029
|
if (putMode === null) return "manageCollection: `mode` must be \"upsert\" (default), \"create\", or \"merge\".";
|
|
1833
2030
|
return {
|
|
2031
|
+
...source,
|
|
1834
2032
|
slug,
|
|
1835
|
-
items,
|
|
1836
2033
|
mode: putMode
|
|
1837
2034
|
};
|
|
1838
2035
|
}
|
|
@@ -1958,7 +2155,7 @@ async function handlePutSchema(slug, schemaArg, deps) {
|
|
|
1958
2155
|
written: true
|
|
1959
2156
|
});
|
|
1960
2157
|
}
|
|
1961
|
-
var MANAGE_COLLECTION_PROMPT = "Use `manageCollection` instead of raw Read/Write/Edit when working with a collection's records OR its schema (raw file I/O stays available as the escape hatch). Before authoring or changing a collection's `schema.json`, call `schemaDocs` to load the field/DSL reference — the default reply is the core authoring guide plus a table of contents; fetch advanced sections (actions, bells, calendar/kanban views, dataSource, storage) by passing their heading as `topic` rather than dumping `topic: \"all\"`. Then read with `getSchema` and write with `putSchema` — `putSchema` validates the whole schema before writing and returns actionable errors instead of silently failing discovery's validation. `getItems` is the only way to see computed values — `derived` fields (e.g. a portfolio's value), `toggle` projections, and `embed` records are host-computed and never present in the stored JSON files. On large collections pass `ids` and/or `fields` to keep the result small. For a question that spans collections (\"which clients have unpaid invoices?\"), start with `getOntology`: it lists every collection with its primaryKey, record count, and outbound `ref`/`embed` relations, so you know which collections to join before reading any records. `putItems` validates every row against the schema before writing (required fields, enum values, primaryKey = record id) and returns `{ written, rejected }`; fix each rejected row using its `problem` text and retry just those rows. Never include computed fields in a row you write. To update a few fields of an existing record, use `mode: \"merge\"` with a partial row ({ id, <changed fields> }) — the default upsert replaces the WHOLE record, so a partial upsert would silently erase every optional field it omits. `deleteItems` removes records by id and returns `{ deleted, rejected }`; an id that doesn't exist comes back rejected rather than counted as deleted, so check `rejected` before reporting a deletion as done. Answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` on ANY collection — on a dataSource (CSV) collection it scans the whole file (getItems is row-capped, so aggregates computed from its output can be silently wrong on large files); on a file-backed collection it aggregates the enriched records, so computed fields (derived/rollup/toggle) are queryable columns.";
|
|
2158
|
+
var MANAGE_COLLECTION_PROMPT = "Use `manageCollection` instead of raw Read/Write/Edit when working with a collection's records OR its schema (raw file I/O stays available as the escape hatch). Before authoring or changing a collection's `schema.json`, call `schemaDocs` to load the field/DSL reference — the default reply is the core authoring guide plus a table of contents; fetch advanced sections (actions, bells, calendar/kanban views, dataSource, storage) by passing their heading as `topic` rather than dumping `topic: \"all\"`. Then read with `getSchema` and write with `putSchema` — `putSchema` validates the whole schema before writing and returns actionable errors instead of silently failing discovery's validation. `getItems` is the only way to see computed values — `derived` fields (e.g. a portfolio's value), `toggle` projections, and `embed` records are host-computed and never present in the stored JSON files. On large collections pass `ids` and/or `fields` to keep the result small. For a question that spans collections (\"which clients have unpaid invoices?\"), start with `getOntology`: it lists every collection with its primaryKey, record count, and outbound `ref`/`embed` relations, so you know which collections to join before reading any records. `putItems` validates every row against the schema before writing (required fields, enum values, primaryKey = record id) and returns `{ written, rejected }`; fix each rejected row using its `problem` text and retry just those rows. Never include computed fields in a row you write. When the rows come from a script rather than from you (a generated schedule, an imported set, anything past a few dozen records), write them to a JSON file UNDER THE WORKSPACE and pass its absolute path as `itemsFile` instead of `items` — the host reads the file, so the rows never pass through your context. Do NOT hand-transcribe a generated file into `items`, and never drive the collection by spawning the MCP bridge yourself. To update a few fields of an existing record, use `mode: \"merge\"` with a partial row ({ id, <changed fields> }) — the default upsert replaces the WHOLE record, so a partial upsert would silently erase every optional field it omits. `deleteItems` removes records by id and returns `{ deleted, rejected }`; an id that doesn't exist comes back rejected rather than counted as deleted, so check `rejected` before reporting a deletion as done. Answer aggregation questions (counts, sums, averages, group-bys) with `queryItems` on ANY collection — on a dataSource (CSV) collection it scans the whole file (getItems is row-capped, so aggregates computed from its output can be silently wrong on large files); on a file-backed collection it aggregates the enriched records, so computed fields (derived/rollup/toggle) are queryable columns.";
|
|
1962
2159
|
/** Validate getItems' optional `ids`/`fields` args, then delegate. */
|
|
1963
2160
|
async function dispatchGetItems(collection, args, deps) {
|
|
1964
2161
|
const ids = optionalStringArray(args.ids, "ids");
|
|
@@ -2049,7 +2246,11 @@ var MANAGE_COLLECTION_DEFINITION = {
|
|
|
2049
2246
|
items: {
|
|
2050
2247
|
type: "array",
|
|
2051
2248
|
items: { type: "object" },
|
|
2052
|
-
description: "putItems: the record objects to store. Each must carry the schema's primaryKey value (it doubles as the filename)."
|
|
2249
|
+
description: "putItems: the record objects to store, inline. Each must carry the schema's primaryKey value (it doubles as the filename). For rows a script generated, pass `itemsFile` instead — never both."
|
|
2250
|
+
},
|
|
2251
|
+
itemsFile: {
|
|
2252
|
+
type: "string",
|
|
2253
|
+
description: "putItems: an ABSOLUTE path to a JSON file holding the array of record objects, read by the host — the alternative to `items` for rows a script produced. Use it whenever the data already exists as a file: passing a generated set of several hundred records through `items` means writing every byte of it yourself. The path must be absolute (this tool runs in the host's server process, whose working directory is not yours) and must be INSIDE the workspace — write the generated file under the workspace, not to a system temp dir. The file must hold a non-empty JSON array, and `items` and `itemsFile` are mutually exclusive."
|
|
2053
2254
|
},
|
|
2054
2255
|
mode: {
|
|
2055
2256
|
type: "string",
|
|
@@ -2085,6 +2286,6 @@ function makeManageCollectionTool(deps = {}) {
|
|
|
2085
2286
|
};
|
|
2086
2287
|
}
|
|
2087
2288
|
//#endregion
|
|
2088
|
-
export {
|
|
2289
|
+
export { runCollectionQuery as A, firstMutateParamProblem as C, validateRecordObject as D, validateCollectionRecords as E, promptPathsFor as F, readCustomViewHtml as I, readCustomViewI18n as L, runQueryOverRows as M, buildActionSeedPrompt as N, compileRecordZ as O, buildCollectionActionSeedPrompt as P, readSkillTemplate as R, applyMutateAction as S, STORE_UNREADABLE as T, successorId as _, makeManageCollectionTool as a, buildWorkspaceOntology as b, deleteCollectionRefusalMessage as c, daysInMonth as d, formatCivil as f, resolveEvery as g, parseCivil as h, MAX_UNSELECTIVE_ITEMS as i, enrichItems as j, recordFieldProblem as k, advanceTriggerDate as l, maybeSpawnSuccessor as m, MAX_PUT_ITEMS as n, deleteCustomView as o, isTriggerDue as p, MAX_SCHEMA_ISSUES as r, deleteCollection as s, MAX_ITEMS_FILE_BYTES as t, computeSuccessor as u, ONE_SECOND_MS as v, MAX_RECORD_ISSUES as w, schemaRelations as x, computeCollectionIcon as y };
|
|
2089
2290
|
|
|
2090
|
-
//# sourceMappingURL=server-
|
|
2291
|
+
//# sourceMappingURL=server-L12n8We3.js.map
|