@mulmoclaude/core 0.18.1 → 0.19.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/collection/index.cjs +44 -51
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +1 -8
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/index.cjs +6 -3
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +3 -3
- package/dist/collection/server/manageTool.d.ts +82 -0
- package/dist/collection-watchers/index.cjs +4 -4
- package/dist/collection-watchers/index.js +2 -2
- package/dist/feeds/index.cjs +4 -4
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +5 -5
- package/dist/feeds/server/index.js +3 -3
- package/dist/{ingestTypes-DvsJhqMx.js → ingestTypes-DEjpiZGM.js} +2 -2
- package/dist/{ingestTypes-DvsJhqMx.js.map → ingestTypes-DEjpiZGM.js.map} +1 -1
- package/dist/{ingestTypes-Cq65Bv-N.cjs → ingestTypes-Dh5lniBL.cjs} +3 -3
- package/dist/{ingestTypes-Cq65Bv-N.cjs.map → ingestTypes-Dh5lniBL.cjs.map} +1 -1
- package/dist/{calendarGrid-CRaMAguM.cjs → promptSafety-DN5V__Ku.cjs} +14 -1
- package/dist/promptSafety-DN5V__Ku.cjs.map +1 -0
- package/dist/{calendarGrid-B3dtFN-m.js → promptSafety-DyG3EuC7.js} +9 -2
- package/dist/promptSafety-DyG3EuC7.js.map +1 -0
- package/dist/{server-C3FOqabY.js → server-Bd8l1bhv.js} +397 -11
- package/dist/server-Bd8l1bhv.js.map +1 -0
- package/dist/{server-Cd6HA_3-.cjs → server-Bh-DPK-H.cjs} +444 -40
- package/dist/server-Bh-DPK-H.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/calendarGrid-B3dtFN-m.js.map +0 -1
- package/dist/calendarGrid-CRaMAguM.cjs.map +0 -1
- package/dist/server-C3FOqabY.js.map +0 -1
- package/dist/server-Cd6HA_3-.cjs.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-D6vf50IK.cjs");
|
|
2
|
-
const
|
|
2
|
+
const require_promptSafety = require("./promptSafety-DN5V__Ku.cjs");
|
|
3
3
|
const require_collection_paths = require("./collection/paths.cjs");
|
|
4
|
+
const require_skill_bridge_index = require("./skill-bridge/index.cjs");
|
|
4
5
|
let node_path = require("node:path");
|
|
5
6
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
6
7
|
let node_fs = require("node:fs");
|
|
@@ -62,7 +63,7 @@ function collectionsRegistriesConfigPath() {
|
|
|
62
63
|
const host = requireHost();
|
|
63
64
|
return host.paths.collectionsRegistriesConfig(host.workspaceRoot);
|
|
64
65
|
}
|
|
65
|
-
function isPresetSlug(slug) {
|
|
66
|
+
function isPresetSlug$1(slug) {
|
|
66
67
|
return requireHost().isPresetSlug(slug);
|
|
67
68
|
}
|
|
68
69
|
/** Logger proxy so engine modules can `import { log }` and use it exactly like
|
|
@@ -79,7 +80,7 @@ var log = {
|
|
|
79
80
|
};
|
|
80
81
|
//#endregion
|
|
81
82
|
//#region src/collection/server/paths.ts
|
|
82
|
-
var SCHEMA_FILE = "schema.json";
|
|
83
|
+
var SCHEMA_FILE$1 = "schema.json";
|
|
83
84
|
/** Sanitise a user-supplied slug into a safe directory-name leaf.
|
|
84
85
|
* Returns null for anything that fails the slug whitelist OR isn't a
|
|
85
86
|
* basename (i.e. survives `path.basename` round-trip unchanged).
|
|
@@ -87,7 +88,7 @@ var SCHEMA_FILE = "schema.json";
|
|
|
87
88
|
* `js/path-injection` sanitiser. */
|
|
88
89
|
function safeSlugName(slug) {
|
|
89
90
|
if (typeof slug !== "string") return null;
|
|
90
|
-
if (!
|
|
91
|
+
if (!require_promptSafety.SAFE_SLUG_PATTERN.test(slug)) return null;
|
|
91
92
|
const basename = node_path.default.basename(slug);
|
|
92
93
|
if (basename !== slug) return null;
|
|
93
94
|
return basename;
|
|
@@ -99,7 +100,7 @@ function safeSlugName(slug) {
|
|
|
99
100
|
* `js/path-injection` sanitiser CodeQL recognises on `safeSlugName`. */
|
|
100
101
|
function safeRecordId(recordId) {
|
|
101
102
|
if (typeof recordId !== "string") return null;
|
|
102
|
-
if (!
|
|
103
|
+
if (!require_promptSafety.SAFE_RECORD_ID_PATTERN.test(recordId)) return null;
|
|
103
104
|
if (recordId.includes("..")) return null;
|
|
104
105
|
const basename = node_path.default.basename(recordId);
|
|
105
106
|
if (basename !== recordId) return null;
|
|
@@ -658,10 +659,10 @@ function enforcedProblem(key, spec, value) {
|
|
|
658
659
|
function strictTypeProblem(key, spec, value) {
|
|
659
660
|
switch (spec.type) {
|
|
660
661
|
case "number":
|
|
661
|
-
case "money": return Number.isFinite(
|
|
662
|
+
case "money": return Number.isFinite(require_promptSafety.coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
|
|
662
663
|
case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
|
|
663
|
-
case "date": return
|
|
664
|
-
case "datetime": return
|
|
664
|
+
case "date": return require_promptSafety.parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
|
|
665
|
+
case "datetime": return require_promptSafety.parseIsoDateTime(value) !== null ? null : `'${key}' = '${String(value)}' is not a YYYY-MM-DDTHH:MM datetime (seconds optional, no timezone suffix — the shape the calendar parses)`;
|
|
665
666
|
default: return null;
|
|
666
667
|
}
|
|
667
668
|
}
|
|
@@ -706,7 +707,7 @@ var compiled = /* @__PURE__ */ new WeakMap();
|
|
|
706
707
|
function compileRecordZ(schema, tier) {
|
|
707
708
|
const cached = compiled.get(schema)?.[tier];
|
|
708
709
|
if (cached) return cached;
|
|
709
|
-
const stored = Object.entries(schema.fields).filter(([, spec]) => !
|
|
710
|
+
const stored = Object.entries(schema.fields).filter(([, spec]) => !require_promptSafety.COMPUTED_TYPES.has(spec.type));
|
|
710
711
|
const validator = zod.z.looseObject({}).superRefine((record, ctx) => {
|
|
711
712
|
for (const [key, spec] of stored) {
|
|
712
713
|
const problem = recordFieldProblem(key, spec, record[key], tier);
|
|
@@ -872,14 +873,14 @@ async function applyMutateAction(collection, action, itemId, params, opts = {})
|
|
|
872
873
|
status: "not-found",
|
|
873
874
|
problem: `item '${itemId}' not found`
|
|
874
875
|
};
|
|
875
|
-
if (!
|
|
876
|
+
if (!require_promptSafety.actionVisible(action, existing)) return {
|
|
876
877
|
ok: false,
|
|
877
878
|
status: "require-unmet",
|
|
878
879
|
problem: `action '${action.id}' is not available for item '${itemId}' in its current state`
|
|
879
880
|
};
|
|
880
881
|
const stored = Object.entries(existing).filter(([key]) => {
|
|
881
882
|
const spec = collection.schema.fields[key];
|
|
882
|
-
return !spec || !
|
|
883
|
+
return !spec || !require_promptSafety.COMPUTED_TYPES.has(spec.type);
|
|
883
884
|
});
|
|
884
885
|
const merged = {
|
|
885
886
|
...Object.fromEntries(stored),
|
|
@@ -972,7 +973,7 @@ var RefFieldZ = zod.z.object({
|
|
|
972
973
|
type: zod.z.literal("ref"),
|
|
973
974
|
...fieldBase,
|
|
974
975
|
to: zod.z.string().min(1)
|
|
975
|
-
}).refine((spec) =>
|
|
976
|
+
}).refine((spec) => require_promptSafety.isSafeSlug(spec.to), slugMessage("to"));
|
|
976
977
|
/** A money amount. See `currencyKeys` for the currency-source contract. */
|
|
977
978
|
var MoneyFieldZ = zod.z.object({
|
|
978
979
|
type: zod.z.literal("money"),
|
|
@@ -1007,7 +1008,7 @@ var SubRefFieldZ = zod.z.object({
|
|
|
1007
1008
|
type: zod.z.literal("ref"),
|
|
1008
1009
|
...subFieldBase,
|
|
1009
1010
|
to: zod.z.string().min(1)
|
|
1010
|
-
}).refine((spec) =>
|
|
1011
|
+
}).refine((spec) => require_promptSafety.isSafeSlug(spec.to), slugMessage("to"));
|
|
1011
1012
|
var SubMoneyFieldZ = zod.z.object({
|
|
1012
1013
|
type: zod.z.literal("money"),
|
|
1013
1014
|
...subFieldBase,
|
|
@@ -1070,7 +1071,7 @@ var EmbedFieldZ = zod.z.object({
|
|
|
1070
1071
|
to: zod.z.string().min(1),
|
|
1071
1072
|
id: zod.z.string().trim().min(1).optional(),
|
|
1072
1073
|
idField: zod.z.string().trim().min(1).optional()
|
|
1073
|
-
}).refine((spec) =>
|
|
1074
|
+
}).refine((spec) => require_promptSafety.isSafeSlug(spec.to) && spec.id !== void 0 !== (spec.idField !== void 0), {
|
|
1074
1075
|
message: "fields with type 'embed' must declare a `to` (valid collection slug) and exactly one of `id` (a fixed record's primary key) or `idField` (a sibling field naming the per-record target)",
|
|
1075
1076
|
path: ["id"]
|
|
1076
1077
|
});
|
|
@@ -1091,7 +1092,7 @@ var BacklinksFieldZ = zod.z.object({
|
|
|
1091
1092
|
via: zod.z.string().trim().min(1),
|
|
1092
1093
|
display: zod.z.array(zod.z.string().trim().min(1)).min(1),
|
|
1093
1094
|
filter: WhenZ.optional()
|
|
1094
|
-
}).refine((spec) =>
|
|
1095
|
+
}).refine((spec) => require_promptSafety.isSafeSlug(spec.from), slugMessage("from"));
|
|
1095
1096
|
/** A cross-collection AGGREGATE over a backlink relation (plan step ⑤ of
|
|
1096
1097
|
* plans/done/collection-ontology.md): a computed number — never stored — that
|
|
1097
1098
|
* sums a source column (or counts rows) over the records in `from` whose
|
|
@@ -1112,7 +1113,7 @@ var RollupFieldZ = zod.z.object({
|
|
|
1112
1113
|
op: zod.z.enum(["sum", "count"]),
|
|
1113
1114
|
column: zod.z.string().trim().min(1).optional(),
|
|
1114
1115
|
filter: WhenZ.optional()
|
|
1115
|
-
}).refine((spec) =>
|
|
1116
|
+
}).refine((spec) => require_promptSafety.isSafeSlug(spec.from), slugMessage("from")).refine((spec) => spec.op === "sum" === (spec.column !== void 0), {
|
|
1116
1117
|
message: "a rollup's `column` names the source column to aggregate: required for op \"sum\", meaningless for op \"count\"",
|
|
1117
1118
|
path: ["column"]
|
|
1118
1119
|
});
|
|
@@ -1261,9 +1262,9 @@ var SpawnZ = zod.z.object({
|
|
|
1261
1262
|
* array) only when the response body isn't itself the array; rss/atom yield
|
|
1262
1263
|
* items natively and ignore it — so no kind-specific requirement here. */
|
|
1263
1264
|
var DeclarativeIngestZ = zod.z.object({
|
|
1264
|
-
kind: zod.z.enum(
|
|
1265
|
+
kind: zod.z.enum(require_promptSafety.INGEST_KINDS),
|
|
1265
1266
|
url: zod.z.string().url(),
|
|
1266
|
-
schedule: zod.z.enum(
|
|
1267
|
+
schedule: zod.z.enum(require_promptSafety.FEED_SCHEDULES),
|
|
1267
1268
|
atHour: zod.z.number().int().min(0).max(23).optional(),
|
|
1268
1269
|
itemsAt: zod.z.string().trim().min(1).optional(),
|
|
1269
1270
|
map: zod.z.record(zod.z.string().trim().min(1), zod.z.string().trim().min(1)),
|
|
@@ -1277,8 +1278,8 @@ var DeclarativeIngestZ = zod.z.object({
|
|
|
1277
1278
|
* validated the SAME way an action's template is (safe path under
|
|
1278
1279
|
* `templates/`), so the skill-bridge mirrors it identically. */
|
|
1279
1280
|
var AgentIngestZ = zod.z.object({
|
|
1280
|
-
kind: zod.z.literal(
|
|
1281
|
-
schedule: zod.z.enum(
|
|
1281
|
+
kind: zod.z.literal(require_promptSafety.AGENT_INGEST_KIND),
|
|
1282
|
+
schedule: zod.z.enum(require_promptSafety.FEED_SCHEDULES),
|
|
1282
1283
|
atHour: zod.z.number().int().min(0).max(23).optional(),
|
|
1283
1284
|
role: zod.z.string().trim().min(1),
|
|
1284
1285
|
template: zod.z.string().trim().min(1).refine(require_collection_paths.isSafeActionTemplatePath, "must be a safe path under `templates/` (e.g. `templates/refresh.md`; no `..`, no leading `/`, no backslash)")
|
|
@@ -1424,7 +1425,7 @@ var CollectionSchemaZ = zod.z.object({
|
|
|
1424
1425
|
notifyWhen: WhenZ.optional(),
|
|
1425
1426
|
ingest: IngestZ.optional(),
|
|
1426
1427
|
dynamicIcon: DynamicIconSpecZ.optional()
|
|
1427
|
-
}).refine((schema) => schema.singleton === void 0 ||
|
|
1428
|
+
}).refine((schema) => schema.singleton === void 0 || require_promptSafety.isSafeRecordId(schema.singleton), {
|
|
1428
1429
|
message: "schema `singleton` must be a valid item id (alphanumeric / hyphen / underscore / interior dot, no `..` or path separators)",
|
|
1429
1430
|
path: ["singleton"]
|
|
1430
1431
|
}).refine((schema) => schema.actions === void 0 || new Set(schema.actions.map((action) => action.id)).size === schema.actions.length, {
|
|
@@ -1435,7 +1436,7 @@ var CollectionSchemaZ = zod.z.object({
|
|
|
1435
1436
|
path: ["collectionActions"]
|
|
1436
1437
|
}).refine((schema) => (schema.actions ?? []).every((action) => action.kind !== "mutate" || Object.keys(action.set).every((key) => {
|
|
1437
1438
|
const target = schema.fields[key];
|
|
1438
|
-
return target !== void 0 && !
|
|
1439
|
+
return target !== void 0 && !require_promptSafety.COMPUTED_TYPES.has(target.type) && key !== schema.primaryKey;
|
|
1439
1440
|
})), {
|
|
1440
1441
|
message: "a mutate action's `set` keys must name declared, non-computed fields (and never the primaryKey)",
|
|
1441
1442
|
path: ["actions"]
|
|
@@ -1530,7 +1531,7 @@ var CollectionSchemaZ = zod.z.object({
|
|
|
1530
1531
|
}).refine((schema) => schema.notifyWhen === void 0 || schema.fields[schema.notifyWhen.field] !== void 0, {
|
|
1531
1532
|
message: "schema `notifyWhen.field` must name a top-level field declared in `fields`",
|
|
1532
1533
|
path: ["notifyWhen"]
|
|
1533
|
-
}).refine((schema) => schema.views === void 0 || schema.views.every((view) =>
|
|
1534
|
+
}).refine((schema) => schema.views === void 0 || schema.views.every((view) => require_promptSafety.isSafeSlug(view.id)), {
|
|
1534
1535
|
message: "every `views[].id` must be a valid slug (alphanumeric / hyphen / underscore, no path separators)",
|
|
1535
1536
|
path: ["views"]
|
|
1536
1537
|
}).refine((schema) => schema.views === void 0 || new Set(schema.views.map((view) => view.id)).size === schema.views.length, {
|
|
@@ -1591,7 +1592,7 @@ function acceptParsedSchema(schema, opts) {
|
|
|
1591
1592
|
async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
|
|
1592
1593
|
const safeName = safeSlugName(slug);
|
|
1593
1594
|
if (safeName === null) return null;
|
|
1594
|
-
const schemaPath = node_path.default.join(skillsRoot, safeName, SCHEMA_FILE);
|
|
1595
|
+
const schemaPath = node_path.default.join(skillsRoot, safeName, SCHEMA_FILE$1);
|
|
1595
1596
|
let raw;
|
|
1596
1597
|
try {
|
|
1597
1598
|
if (!(await (0, node_fs_promises.stat)(schemaPath)).isFile()) return null;
|
|
@@ -1824,7 +1825,7 @@ async function loadTarget(slug, opts) {
|
|
|
1824
1825
|
const byId = {};
|
|
1825
1826
|
for (const item of items) {
|
|
1826
1827
|
const itemId = item[target.schema.primaryKey];
|
|
1827
|
-
if (typeof itemId === "string" && itemId.length > 0) byId[itemId] =
|
|
1828
|
+
if (typeof itemId === "string" && itemId.length > 0) byId[itemId] = require_promptSafety.deriveAll(target.schema, item, {});
|
|
1828
1829
|
}
|
|
1829
1830
|
return {
|
|
1830
1831
|
schema: target.schema,
|
|
@@ -1860,7 +1861,7 @@ function toRefRecords(linked) {
|
|
|
1860
1861
|
function projectBacklinks(field, schema, enriched, linked) {
|
|
1861
1862
|
const source = linked[field.from];
|
|
1862
1863
|
if (!source) return [];
|
|
1863
|
-
return
|
|
1864
|
+
return require_promptSafety.backlinkRows(field, String(enriched[schema.primaryKey] ?? ""), Object.values(source.byId)).map((row) => require_promptSafety.projectBacklinkRow(row, field.display, source.schema.primaryKey));
|
|
1864
1865
|
}
|
|
1865
1866
|
/** Project the computed (never-stored) field kinds onto one derived
|
|
1866
1867
|
* record: `toggle` → boolean off its enum, `embed` → the target record
|
|
@@ -1870,7 +1871,7 @@ function projectComputed(schema, enriched, linked) {
|
|
|
1870
1871
|
for (const [key, field] of Object.entries(schema.fields)) {
|
|
1871
1872
|
if (field.type === "toggle" && field.field) enriched[key] = String(enriched[field.field] ?? "") === field.onValue;
|
|
1872
1873
|
if (field.type === "embed" && field.to) {
|
|
1873
|
-
const targetId =
|
|
1874
|
+
const targetId = require_promptSafety.embedTargetId(field, enriched);
|
|
1874
1875
|
enriched[key] = targetId && linked[field.to]?.byId[targetId] || null;
|
|
1875
1876
|
}
|
|
1876
1877
|
if (field.type === "backlinks") enriched[key] = projectBacklinks(field, schema, enriched, linked);
|
|
@@ -1891,7 +1892,7 @@ function projectRollups(schema, record, linked) {
|
|
|
1891
1892
|
if (out === record) out = { ...record };
|
|
1892
1893
|
const source = linked[field.from];
|
|
1893
1894
|
const selfId = String(record[schema.primaryKey] ?? "");
|
|
1894
|
-
out[key] = source ?
|
|
1895
|
+
out[key] = source ? require_promptSafety.rollupValue(field, selfId, Object.values(source.byId)) : null;
|
|
1895
1896
|
}
|
|
1896
1897
|
return out;
|
|
1897
1898
|
}
|
|
@@ -1903,7 +1904,7 @@ async function enrichItems(collection, items, opts = {}) {
|
|
|
1903
1904
|
const { schema } = collection;
|
|
1904
1905
|
const linked = await loadLinkedTargets(schema, opts);
|
|
1905
1906
|
const refRecords = toRefRecords(linked);
|
|
1906
|
-
return items.map((item) => projectComputed(schema,
|
|
1907
|
+
return items.map((item) => projectComputed(schema, require_promptSafety.deriveAll(schema, projectRollups(schema, item, linked), refRecords), linked));
|
|
1907
1908
|
}
|
|
1908
1909
|
//#endregion
|
|
1909
1910
|
//#region src/collection/server/dynamicIcon.ts
|
|
@@ -1939,9 +1940,9 @@ async function computeCollectionIcon(collection, opts = {}) {
|
|
|
1939
1940
|
const source = await loadCollection(spec.source.collection, opts);
|
|
1940
1941
|
if (!source) return spec.fallback ?? schema.icon;
|
|
1941
1942
|
const ordered = sortByPrimaryKey(await listItems(source.dataDir, { workspaceRoot: opts.workspaceRoot }), source.schema.primaryKey);
|
|
1942
|
-
const orderBy = spec.source.orderBy ??
|
|
1943
|
+
const orderBy = spec.source.orderBy ?? require_promptSafety.firstDateField(source.schema);
|
|
1943
1944
|
const recordsById = buildRecordsById(ordered, source.schema.primaryKey);
|
|
1944
|
-
return
|
|
1945
|
+
return require_promptSafety.resolveIcon(require_promptSafety.selectDynamicRecord(ordered, spec.source, orderBy, recordsById), spec, schema.icon, recordsById);
|
|
1945
1946
|
} catch (err) {
|
|
1946
1947
|
log.warn("collections", "dynamic icon compute failed, falling back", {
|
|
1947
1948
|
slug: collection.slug,
|
|
@@ -2075,7 +2076,7 @@ function matchesWhen(when, schema, item) {
|
|
|
2075
2076
|
* Discovery rejects a map that doesn't cover the enum's values, so null
|
|
2076
2077
|
* here means a record that predates a map/enum edit. */
|
|
2077
2078
|
function resolveEvery(every, sourceItem) {
|
|
2078
|
-
if (!
|
|
2079
|
+
if (!require_promptSafety.isFieldDrivenEvery(every)) return every;
|
|
2079
2080
|
const raw = sourceItem[every.fromField];
|
|
2080
2081
|
if (raw === void 0 || raw === null || raw === "") return null;
|
|
2081
2082
|
return every.map[String(raw)] ?? null;
|
|
@@ -2115,7 +2116,7 @@ function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
|
|
|
2115
2116
|
});
|
|
2116
2117
|
return;
|
|
2117
2118
|
}
|
|
2118
|
-
const fromField =
|
|
2119
|
+
const fromField = require_promptSafety.isFieldDrivenEvery(every) ? every.fromField : void 0;
|
|
2119
2120
|
log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
|
|
2120
2121
|
slug,
|
|
2121
2122
|
sourceId,
|
|
@@ -2302,7 +2303,7 @@ async function deleteCollection(collection, opts = {}) {
|
|
|
2302
2303
|
kind: "user-scope",
|
|
2303
2304
|
slug
|
|
2304
2305
|
};
|
|
2305
|
-
if (isPresetSlug(slug)) return {
|
|
2306
|
+
if (isPresetSlug$1(slug)) return {
|
|
2306
2307
|
kind: "preset",
|
|
2307
2308
|
slug
|
|
2308
2309
|
};
|
|
@@ -2368,9 +2369,9 @@ async function canonicalBase(collection, workspaceRoot, safeSlug) {
|
|
|
2368
2369
|
* project collection (no staging mirror) doesn't have an empty staging tree
|
|
2369
2370
|
* materialised by a side effect of the delete. */
|
|
2370
2371
|
async function schemaWriteTargets(collection, workspaceRoot, safeSlug) {
|
|
2371
|
-
const active = node_path.default.join(collection.skillDir, SCHEMA_FILE);
|
|
2372
|
+
const active = node_path.default.join(collection.skillDir, SCHEMA_FILE$1);
|
|
2372
2373
|
if (collection.source !== "project") return [active];
|
|
2373
|
-
const stagingSchema = node_path.default.join(skillsStagingDir(workspaceRoot), safeSlug, SCHEMA_FILE);
|
|
2374
|
+
const stagingSchema = node_path.default.join(skillsStagingDir(workspaceRoot), safeSlug, SCHEMA_FILE$1);
|
|
2374
2375
|
const targets = [];
|
|
2375
2376
|
if (await fileExists(stagingSchema)) targets.push(stagingSchema);
|
|
2376
2377
|
targets.push(active);
|
|
@@ -2391,7 +2392,7 @@ async function unlinkIfPresent(target) {
|
|
|
2391
2392
|
* preserved verbatim. */
|
|
2392
2393
|
async function removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug) {
|
|
2393
2394
|
const base = await canonicalBase(collection, workspaceRoot, safeSlug);
|
|
2394
|
-
const canonical = node_path.default.join(base, SCHEMA_FILE);
|
|
2395
|
+
const canonical = node_path.default.join(base, SCHEMA_FILE$1);
|
|
2395
2396
|
const parsed = JSON.parse(await (0, node_fs_promises.readFile)(canonical, "utf-8"));
|
|
2396
2397
|
if (Array.isArray(parsed.views)) parsed.views = parsed.views.filter((entry) => entry?.id !== viewId);
|
|
2397
2398
|
const serialized = `${JSON.stringify(parsed, null, 2)}\n`;
|
|
@@ -2402,7 +2403,7 @@ async function removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug
|
|
|
2402
2403
|
* refused (read-only / re-seeded on boot), consistent with `deleteCollection`. */
|
|
2403
2404
|
async function deleteCustomView(collection, viewId, opts = {}) {
|
|
2404
2405
|
if (collection.source === "user") return { kind: "user-scope" };
|
|
2405
|
-
if (isPresetSlug(collection.slug)) return { kind: "preset" };
|
|
2406
|
+
if (isPresetSlug$1(collection.slug)) return { kind: "preset" };
|
|
2406
2407
|
const safeSlug = safeSlugName(collection.slug);
|
|
2407
2408
|
if (safeSlug === null) return {
|
|
2408
2409
|
kind: "unsafe-path",
|
|
@@ -2427,6 +2428,391 @@ async function deleteCustomView(collection, viewId, opts = {}) {
|
|
|
2427
2428
|
viewId
|
|
2428
2429
|
};
|
|
2429
2430
|
}
|
|
2431
|
+
/** True for a launcher-managed preset slug (`mc-<something>`). The sync logic uses
|
|
2432
|
+
* this to bound which active skills it may refresh/prune, so user-authored skills are
|
|
2433
|
+
* never touched. */
|
|
2434
|
+
function isPresetSlug(slug) {
|
|
2435
|
+
return slug.startsWith("mc-") && slug.length > 3;
|
|
2436
|
+
}
|
|
2437
|
+
//#endregion
|
|
2438
|
+
//#region src/collection/server/manageTool.ts
|
|
2439
|
+
/** Refuse an unselective getItems beyond this many records — a silent
|
|
2440
|
+
* truncation would read as "covered everything", and an unbounded dump
|
|
2441
|
+
* of a large collection is a token bomb. `ids` or `fields` lifts it. */
|
|
2442
|
+
var MAX_UNSELECTIVE_ITEMS = 200;
|
|
2443
|
+
/** schema.json basename under a skill dir (canonical staging + active mirror). */
|
|
2444
|
+
var SCHEMA_FILE = "schema.json";
|
|
2445
|
+
/** The collection-authoring reference, served by the `schemaDocs` action. */
|
|
2446
|
+
var SCHEMA_DOCS_FILE = "collection-skills.md";
|
|
2447
|
+
/** Cap the rejected-schema issue list so a deeply-broken schema can't flood the result. */
|
|
2448
|
+
var MAX_SCHEMA_ISSUES = 20;
|
|
2449
|
+
/** The workspace help-docs dir both hosts seed (`@mulmoclaude/core/workspace-setup`
|
|
2450
|
+
* syncs the bundled assets here) — the user-editable copy schemaDocs prefers. */
|
|
2451
|
+
var HELPS_DIR = "config/helps";
|
|
2452
|
+
/** Resolve the workspace root the same way every collections call does:
|
|
2453
|
+
* the injected override (tests) or the configured collection host. */
|
|
2454
|
+
function resolveBase(deps) {
|
|
2455
|
+
return deps.workspaceRoot ?? getWorkspaceRoot();
|
|
2456
|
+
}
|
|
2457
|
+
/** Shared "unknown collection" message — its schema.json is missing or
|
|
2458
|
+
* failed validation, so discovery skipped it. */
|
|
2459
|
+
function unknownCollection(slug) {
|
|
2460
|
+
return `manageCollection: unknown collection '${require_promptSafety.defangForPrompt(slug)}' — its schema.json is missing or failed validation.`;
|
|
2461
|
+
}
|
|
2462
|
+
function optionalStringArray(value, name) {
|
|
2463
|
+
if (value === void 0) return { ok: true };
|
|
2464
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string" && entry.length > 0)) return {
|
|
2465
|
+
ok: false,
|
|
2466
|
+
error: `manageCollection: \`${name}\` must be an array of non-empty strings when present.`
|
|
2467
|
+
};
|
|
2468
|
+
return {
|
|
2469
|
+
ok: true,
|
|
2470
|
+
value
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
/** Project a record down to the requested fields. The primary key is
|
|
2474
|
+
* always kept so every returned record stays addressable for a
|
|
2475
|
+
* follow-up getItems/putItems. */
|
|
2476
|
+
function projectFields(record, fields, primaryKey) {
|
|
2477
|
+
const keys = fields.includes(primaryKey) ? fields : [primaryKey, ...fields];
|
|
2478
|
+
return Object.fromEntries(keys.filter((key) => key in record).map((key) => [key, record[key]]));
|
|
2479
|
+
}
|
|
2480
|
+
/** The validation warning appended to a getItems result when stored
|
|
2481
|
+
* record files are malformed (they're silently skipped at read time,
|
|
2482
|
+
* so without this they'd just look missing). Issue strings quote
|
|
2483
|
+
* record-controlled text → defanged, mirroring the presentCollection
|
|
2484
|
+
* dispatch. The record VALUES in `items` ride verbatim, like a raw
|
|
2485
|
+
* file Read — only host-composed report strings are defanged. */
|
|
2486
|
+
async function recordIssuesWarning(collection, deps) {
|
|
2487
|
+
if (deps.ablateValidation) return void 0;
|
|
2488
|
+
const issues = await validateCollectionRecords(collection, { workspaceRoot: deps.workspaceRoot });
|
|
2489
|
+
if (issues.length === 0) return void 0;
|
|
2490
|
+
const lines = issues.map((issue) => `- ${require_promptSafety.defangForPrompt(issue.file)}: ${require_promptSafety.defangForPrompt(issue.problem)}`).join("\n");
|
|
2491
|
+
return `${issues.length} record file(s) have data problems and are missing from this result. Fix each (Read → correct → Write):\n${lines}`;
|
|
2492
|
+
}
|
|
2493
|
+
async function loadRequestedItems(collection, ids, deps) {
|
|
2494
|
+
if (!ids) return {
|
|
2495
|
+
items: await listItems(collection.dataDir, { workspaceRoot: deps.workspaceRoot }),
|
|
2496
|
+
missing: []
|
|
2497
|
+
};
|
|
2498
|
+
const items = [];
|
|
2499
|
+
const missing = [];
|
|
2500
|
+
for (const recordId of ids) {
|
|
2501
|
+
const item = await readItem(collection.dataDir, recordId, { workspaceRoot: deps.workspaceRoot }).catch(() => null);
|
|
2502
|
+
if (item) items.push(item);
|
|
2503
|
+
else missing.push(recordId);
|
|
2504
|
+
}
|
|
2505
|
+
return {
|
|
2506
|
+
items,
|
|
2507
|
+
missing
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
async function handleGetItems(collection, args, deps) {
|
|
2511
|
+
const { items, missing } = await loadRequestedItems(collection, args.ids, deps);
|
|
2512
|
+
if (!args.ids && !args.fields && items.length > 200) return `manageCollection: refused — '${collection.slug}' has ${items.length} records, over the unselective limit of 200. Pass \`ids\` for specific records or \`fields\` to project only the columns you need.`;
|
|
2513
|
+
const enriched = await enrichItems(collection, items, deps);
|
|
2514
|
+
const projected = args.fields ? enriched.map((item) => projectFields(item, args.fields, collection.schema.primaryKey)) : enriched;
|
|
2515
|
+
const warning = !args.ids || missing.length > 0 ? await recordIssuesWarning(collection, deps) : void 0;
|
|
2516
|
+
return JSON.stringify({
|
|
2517
|
+
collection: collection.slug,
|
|
2518
|
+
count: projected.length,
|
|
2519
|
+
items: projected,
|
|
2520
|
+
...missing.length > 0 ? { missing: missing.map((recordId) => require_promptSafety.defangForPrompt(recordId)) } : {},
|
|
2521
|
+
...warning ? { warning } : {}
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
/** Reject writes that set host-computed keys, with a pointer at the
|
|
2525
|
+
* writable source of truth (the toggle's enum) where one exists. */
|
|
2526
|
+
function computedKeyProblem(record, schema) {
|
|
2527
|
+
for (const key of Object.keys(record)) {
|
|
2528
|
+
const spec = schema.fields[key];
|
|
2529
|
+
if (!spec || !require_promptSafety.COMPUTED_TYPES.has(spec.type)) continue;
|
|
2530
|
+
if (spec.type === "toggle" && spec.field) return `'${key}' is a toggle projection — write the enum field '${spec.field}' instead`;
|
|
2531
|
+
return `'${key}' is ${{
|
|
2532
|
+
derived: "derived",
|
|
2533
|
+
embed: "an embed",
|
|
2534
|
+
backlinks: "a backlinks view",
|
|
2535
|
+
rollup: "a rollup"
|
|
2536
|
+
}[spec.type] ?? "computed"} — computed by the host, remove it from the record`;
|
|
2537
|
+
}
|
|
2538
|
+
return null;
|
|
2539
|
+
}
|
|
2540
|
+
/** `mode: "merge"` resolves the row against the EXISTING record —
|
|
2541
|
+
* a partial row updates just the fields it carries, instead of a
|
|
2542
|
+
* whole-record upsert silently erasing the optional fields it omits
|
|
2543
|
+
* (an upsert of `{id, status}` would pass validation yet drop
|
|
2544
|
+
* `notes`/`lesson`/…). Merge is a partial UPDATE by definition, so a
|
|
2545
|
+
* missing record is a reject, not an implicit create — a merged-over-
|
|
2546
|
+
* nothing partial record is exactly the data shape this mode exists
|
|
2547
|
+
* to prevent.
|
|
2548
|
+
*
|
|
2549
|
+
* Computed keys found in the STORED record are stripped before the
|
|
2550
|
+
* merge: the caller's own row was already computed-key-rejected, but a
|
|
2551
|
+
* raw-written / legacy record can carry a stale `derived`/`embed`/
|
|
2552
|
+
* `toggle` value, and re-writing it would perpetuate a forged
|
|
2553
|
+
* host-computed value. A merge heals the record instead.
|
|
2554
|
+
*
|
|
2555
|
+
* readItem THROWS on a malformed stored file (only ENOENT is null) —
|
|
2556
|
+
* downgraded to a per-row rejection here, like loadRequestedItems'
|
|
2557
|
+
* `missing`, so one broken file can't abort the whole putItems batch. */
|
|
2558
|
+
async function mergeWithExisting(collection, record, itemId, deps) {
|
|
2559
|
+
let existing;
|
|
2560
|
+
try {
|
|
2561
|
+
existing = await readItem(collection.dataDir, itemId, { workspaceRoot: deps.workspaceRoot });
|
|
2562
|
+
} catch {
|
|
2563
|
+
return `'${itemId}' has a malformed stored file — mode "merge" needs to read it; fix the file (Read → correct → Write) or replace it whole with "upsert"`;
|
|
2564
|
+
}
|
|
2565
|
+
if (!existing) return `'${itemId}' not found — mode "merge" updates an existing record; use "upsert" or "create" to add it`;
|
|
2566
|
+
const stored = Object.entries(existing).filter(([key]) => {
|
|
2567
|
+
const spec = collection.schema.fields[key];
|
|
2568
|
+
return !spec || !require_promptSafety.COMPUTED_TYPES.has(spec.type);
|
|
2569
|
+
});
|
|
2570
|
+
return {
|
|
2571
|
+
...Object.fromEntries(stored),
|
|
2572
|
+
...record
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
async function putOneItem(collection, record, mode, deps) {
|
|
2576
|
+
const { schema } = collection;
|
|
2577
|
+
const itemId = resolveCreateItemId(schema, record);
|
|
2578
|
+
const reject = (about, problem) => ({ rejected: {
|
|
2579
|
+
id: require_promptSafety.defangForPrompt(about),
|
|
2580
|
+
problem: require_promptSafety.defangForPrompt(problem)
|
|
2581
|
+
} });
|
|
2582
|
+
if (itemId === null) return reject("(no id)", `record has no '${schema.primaryKey}' value — set it (it doubles as the filename)`);
|
|
2583
|
+
const computed = computedKeyProblem(record, schema);
|
|
2584
|
+
if (computed) return reject(itemId, computed);
|
|
2585
|
+
let toWrite = record;
|
|
2586
|
+
if (mode === "merge") {
|
|
2587
|
+
const merged = await mergeWithExisting(collection, record, itemId, deps);
|
|
2588
|
+
if (typeof merged === "string") return reject(itemId, merged);
|
|
2589
|
+
toWrite = merged;
|
|
2590
|
+
}
|
|
2591
|
+
if (!deps.ablateValidation) {
|
|
2592
|
+
const invalid = validateRecordObject(toWrite, itemId, schema);
|
|
2593
|
+
if (invalid) return reject(itemId, invalid);
|
|
2594
|
+
}
|
|
2595
|
+
const result = await writeItem(collection.dataDir, itemId, toWrite, {
|
|
2596
|
+
refuseOverwrite: mode === "create",
|
|
2597
|
+
workspaceRoot: deps.workspaceRoot,
|
|
2598
|
+
slug: collection.slug
|
|
2599
|
+
});
|
|
2600
|
+
if (result.kind === "ok") return { written: result.itemId };
|
|
2601
|
+
if (result.kind === "invalid-id") return reject(itemId, `'${itemId}' is not a valid record id (letters/digits at the ends; -, _, or . inside; no '..' or path characters)`);
|
|
2602
|
+
if (result.kind === "conflict") return reject(itemId, `'${itemId}' already exists — mode "create" refuses overwrite; use "upsert" to update it`);
|
|
2603
|
+
return reject(itemId, "write refused: the collection's data dir escapes the workspace");
|
|
2604
|
+
}
|
|
2605
|
+
async function handlePutItems(collection, args, deps) {
|
|
2606
|
+
const written = [];
|
|
2607
|
+
const rejected = [];
|
|
2608
|
+
for (const record of args.items) {
|
|
2609
|
+
const outcome = await putOneItem(collection, record, args.mode, deps);
|
|
2610
|
+
if (outcome.written) written.push(outcome.written);
|
|
2611
|
+
if (outcome.rejected) rejected.push(outcome.rejected);
|
|
2612
|
+
}
|
|
2613
|
+
return JSON.stringify({
|
|
2614
|
+
collection: collection.slug,
|
|
2615
|
+
written,
|
|
2616
|
+
rejected
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
function parsePutItems(args, slug) {
|
|
2620
|
+
const { items, mode } = args;
|
|
2621
|
+
if (!(Array.isArray(items) && items.length > 0 && items.every((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)))) return "manageCollection: `items` is required for putItems — a non-empty array of record objects.";
|
|
2622
|
+
if (mode !== void 0 && mode !== "upsert" && mode !== "create" && mode !== "merge") return "manageCollection: `mode` must be \"upsert\" (default), \"create\", or \"merge\".";
|
|
2623
|
+
return {
|
|
2624
|
+
slug,
|
|
2625
|
+
items,
|
|
2626
|
+
mode: mode ?? "upsert"
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
/** The machine-readable workspace ontology: every collection with its
|
|
2630
|
+
* identity, record count, and outbound `ref`/`embed` relations. Slugs
|
|
2631
|
+
* are discovery-sanitized; titles/labels are workspace-authored schema
|
|
2632
|
+
* text and ride verbatim — the same trust class as the record values
|
|
2633
|
+
* getItems returns. */
|
|
2634
|
+
async function handleGetOntology(deps) {
|
|
2635
|
+
const collections = await buildWorkspaceOntology(deps);
|
|
2636
|
+
return JSON.stringify({
|
|
2637
|
+
count: collections.length,
|
|
2638
|
+
collections
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
/** Return the collection-authoring reference (`collection-skills.md`).
|
|
2642
|
+
* Workspace copy first (reflects user edits), bundled asset as the
|
|
2643
|
+
* always-present fallback. Both reads guarded; if neither resolves the
|
|
2644
|
+
* agent still gets an actionable message instead of a thrown call. */
|
|
2645
|
+
async function handleSchemaDocs(deps) {
|
|
2646
|
+
const candidates = [node_path.default.join(resolveBase(deps), HELPS_DIR, SCHEMA_DOCS_FILE), ...deps.bundledHelpsDir ? [node_path.default.join(deps.bundledHelpsDir(), SCHEMA_DOCS_FILE)] : []];
|
|
2647
|
+
for (const candidate of candidates) try {
|
|
2648
|
+
return await (0, node_fs_promises.readFile)(candidate, "utf-8");
|
|
2649
|
+
} catch {}
|
|
2650
|
+
return `manageCollection: could not read the collection-authoring reference (${SCHEMA_DOCS_FILE}).`;
|
|
2651
|
+
}
|
|
2652
|
+
/** Return the raw schema.json of an existing collection, for editing.
|
|
2653
|
+
* Staging (the canonical writable copy) first, the active mirror as a
|
|
2654
|
+
* fallback for user-scope skills that have no staging copy. Raw text —
|
|
2655
|
+
* not the parsed schema — so the agent edits the true on-disk source. */
|
|
2656
|
+
async function handleGetSchema(slug, deps) {
|
|
2657
|
+
const collection = await loadCollection(slug, deps);
|
|
2658
|
+
if (!collection) return unknownCollection(slug);
|
|
2659
|
+
const candidates = [node_path.default.join(require_skill_bridge_index.dataSkillDir(resolveBase(deps), collection.slug), SCHEMA_FILE), node_path.default.join(collection.skillDir, SCHEMA_FILE)];
|
|
2660
|
+
for (const candidate of candidates) try {
|
|
2661
|
+
return await (0, node_fs_promises.readFile)(candidate, "utf-8");
|
|
2662
|
+
} catch {}
|
|
2663
|
+
return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' has no readable ${SCHEMA_FILE}.`;
|
|
2664
|
+
}
|
|
2665
|
+
/** Refuse a schema edit the host can't honour: user-scope/feed collections
|
|
2666
|
+
* are read-only, and presets (mc-*) re-seed on restart so an edit is lost. */
|
|
2667
|
+
function schemaEditRefusal(collection, slug) {
|
|
2668
|
+
if (collection.source !== "project") return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' is ${collection.source}-scope and read-only here — only project collections (data/skills/) can be edited.`;
|
|
2669
|
+
if (isPresetSlug(slug)) return `manageCollection: '${require_promptSafety.defangForPrompt(slug)}' is a preset (mc-*) and re-seeds on restart — copy it under a different slug to customise.`;
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2672
|
+
/** Turn a CollectionSchemaZ failure into a short, actionable list the
|
|
2673
|
+
* agent can fix, pointing back at the field reference. */
|
|
2674
|
+
function formatSchemaIssues(issues) {
|
|
2675
|
+
const shown = issues.slice(0, 20);
|
|
2676
|
+
const lines = shown.map((issue) => `- ${issue.path.map(String).join(".") || "(root)"}: ${require_promptSafety.defangForPrompt(issue.message)}`).join("\n");
|
|
2677
|
+
const omitted = issues.length - shown.length;
|
|
2678
|
+
return `manageCollection: schema rejected — fix and retry (call schemaDocs for the field reference):\n${lines}${omitted > 0 ? `\n- …and ${omitted} more issue(s); fix these first and retry.` : ""}`;
|
|
2679
|
+
}
|
|
2680
|
+
/** Write the validated schema to the canonical staging copy, then mirror
|
|
2681
|
+
* it into the active `.claude/skills/` tree discovery reads — an internal
|
|
2682
|
+
* write doesn't fire the skill-bridge hook, so we mirror explicitly. */
|
|
2683
|
+
async function writeAndMirrorSchema(slug, schema, deps) {
|
|
2684
|
+
const base = resolveBase(deps);
|
|
2685
|
+
await writeFileAtomic(node_path.default.join(require_skill_bridge_index.dataSkillDir(base, slug), SCHEMA_FILE), `${JSON.stringify(schema, null, 2)}\n`);
|
|
2686
|
+
require_skill_bridge_index.mirrorSkillWrite(base, {
|
|
2687
|
+
slug,
|
|
2688
|
+
relSegments: [SCHEMA_FILE]
|
|
2689
|
+
});
|
|
2690
|
+
try {
|
|
2691
|
+
await deps.refreshAfterWrite?.();
|
|
2692
|
+
} catch {}
|
|
2693
|
+
}
|
|
2694
|
+
/** The post-Zod acceptance gates discovery applies before a parsed schema
|
|
2695
|
+
* becomes a live collection. Mirrors discovery's checks (`loadOneCollection`)
|
|
2696
|
+
* so a write can't pass here yet be silently skipped on the next load.
|
|
2697
|
+
* putSchema only runs for project-scope collections, so the feed-`ingest`
|
|
2698
|
+
* gate doesn't apply. Returns a one-line reason, or null when the schema
|
|
2699
|
+
* would be accepted. */
|
|
2700
|
+
function schemaDiscoveryGate(schema, base) {
|
|
2701
|
+
const primaryField = schema.fields[schema.primaryKey];
|
|
2702
|
+
if (!primaryField) return `primaryKey '${schema.primaryKey}' is not one of the declared fields`;
|
|
2703
|
+
if (primaryField.primary !== true) return `the primaryKey field '${schema.primaryKey}' must be flagged \`primary: true\``;
|
|
2704
|
+
if (resolveDataDir(schema.dataPath, base) === null) return `dataPath '${schema.dataPath}' escapes the workspace`;
|
|
2705
|
+
return null;
|
|
2706
|
+
}
|
|
2707
|
+
/** Validate a schema against CollectionSchemaZ and, on success, persist it.
|
|
2708
|
+
* Edit-only: a new collection is created by writing SKILL.md + schema.json
|
|
2709
|
+
* under data/skills/<slug>/ (the normal create flow), not through here. */
|
|
2710
|
+
async function handlePutSchema(slug, schemaArg, deps) {
|
|
2711
|
+
if (!schemaArg || typeof schemaArg !== "object" || Array.isArray(schemaArg)) return "manageCollection: `schema` is required for putSchema — the full collection schema object.";
|
|
2712
|
+
const collection = await loadCollection(slug, deps);
|
|
2713
|
+
if (!collection) return `manageCollection: unknown collection '${require_promptSafety.defangForPrompt(slug)}' — create it by writing SKILL.md + ${SCHEMA_FILE} under data/skills/${require_promptSafety.defangForPrompt(slug)}/, then edit it here.`;
|
|
2714
|
+
const refusal = schemaEditRefusal(collection, slug);
|
|
2715
|
+
if (refusal) return refusal;
|
|
2716
|
+
const parsed = CollectionSchemaZ.safeParse(schemaArg);
|
|
2717
|
+
if (!parsed.success) return formatSchemaIssues(parsed.error.issues);
|
|
2718
|
+
const gate = schemaDiscoveryGate(parsed.data, resolveBase(deps));
|
|
2719
|
+
if (gate) return `manageCollection: schema rejected — ${gate} (call schemaDocs for the field reference). It passes basic validation but discovery would skip it, hiding the collection.`;
|
|
2720
|
+
await writeAndMirrorSchema(collection.slug, parsed.data, deps);
|
|
2721
|
+
return JSON.stringify({
|
|
2722
|
+
collection: collection.slug,
|
|
2723
|
+
written: true
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
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, 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.";
|
|
2727
|
+
/** Validate getItems' optional `ids`/`fields` args, then delegate. */
|
|
2728
|
+
async function dispatchGetItems(collection, args, deps) {
|
|
2729
|
+
const ids = optionalStringArray(args.ids, "ids");
|
|
2730
|
+
if (!ids.ok) return ids.error;
|
|
2731
|
+
const fields = optionalStringArray(args.fields, "fields");
|
|
2732
|
+
if (!fields.ok) return fields.error;
|
|
2733
|
+
return handleGetItems(collection, {
|
|
2734
|
+
slug: collection.slug,
|
|
2735
|
+
ids: ids.value,
|
|
2736
|
+
fields: fields.value
|
|
2737
|
+
}, deps);
|
|
2738
|
+
}
|
|
2739
|
+
async function manageCollectionHandler(deps, args) {
|
|
2740
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
2741
|
+
if (action === "schemaDocs") return handleSchemaDocs(deps);
|
|
2742
|
+
if (action === "getOntology") return handleGetOntology(deps);
|
|
2743
|
+
const slug = typeof args.slug === "string" ? args.slug.trim() : "";
|
|
2744
|
+
if (!slug) return "manageCollection: `slug` is required (the collection's slug).";
|
|
2745
|
+
if (action === "getSchema") return handleGetSchema(slug, deps);
|
|
2746
|
+
if (action === "putSchema") return handlePutSchema(slug, args.schema, deps);
|
|
2747
|
+
if (action !== "getItems" && action !== "putItems") return "manageCollection: `action` must be \"getItems\", \"putItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", or \"putSchema\".";
|
|
2748
|
+
const collection = await loadCollection(slug, deps);
|
|
2749
|
+
if (!collection) return unknownCollection(slug);
|
|
2750
|
+
if (action === "getItems") return dispatchGetItems(collection, args, deps);
|
|
2751
|
+
const parsed = parsePutItems(args, slug);
|
|
2752
|
+
if (typeof parsed === "string") return parsed;
|
|
2753
|
+
return handlePutItems(collection, parsed, deps);
|
|
2754
|
+
}
|
|
2755
|
+
function makeManageCollectionTool(deps = {}) {
|
|
2756
|
+
return {
|
|
2757
|
+
definition: {
|
|
2758
|
+
name: "manageCollection",
|
|
2759
|
+
description: "Read and write a schema-driven collection through the host — both its records and its structure. getItems returns records WITH computed values (derived formulas, toggles, embeds) the stored JSON files don't contain; putItems validates each row against the schema before writing. getOntology maps the whole workspace: every collection with its record count and outbound ref/embed relations — call it first for cross-collection questions. schemaDocs returns the collection-authoring reference; getSchema/putSchema read and validate-then-write the collection's schema.json. Prefer it over raw file I/O on collections.",
|
|
2760
|
+
inputSchema: {
|
|
2761
|
+
type: "object",
|
|
2762
|
+
properties: {
|
|
2763
|
+
action: {
|
|
2764
|
+
type: "string",
|
|
2765
|
+
enum: [
|
|
2766
|
+
"getItems",
|
|
2767
|
+
"putItems",
|
|
2768
|
+
"getOntology",
|
|
2769
|
+
"schemaDocs",
|
|
2770
|
+
"getSchema",
|
|
2771
|
+
"putSchema"
|
|
2772
|
+
],
|
|
2773
|
+
description: "What to do."
|
|
2774
|
+
},
|
|
2775
|
+
slug: {
|
|
2776
|
+
type: "string",
|
|
2777
|
+
description: "The collection's slug (its directory name, e.g. `stock-quotes`). Required for everything except schemaDocs and getOntology."
|
|
2778
|
+
},
|
|
2779
|
+
ids: {
|
|
2780
|
+
type: "array",
|
|
2781
|
+
items: { type: "string" },
|
|
2782
|
+
description: "getItems: only these record ids (primary-key values). Omit for all records."
|
|
2783
|
+
},
|
|
2784
|
+
fields: {
|
|
2785
|
+
type: "array",
|
|
2786
|
+
items: { type: "string" },
|
|
2787
|
+
description: "getItems: only these fields per record (the primary key is always included). Omit for all fields. Use on large collections."
|
|
2788
|
+
},
|
|
2789
|
+
items: {
|
|
2790
|
+
type: "array",
|
|
2791
|
+
items: { type: "object" },
|
|
2792
|
+
description: "putItems: the record objects to store. Each must carry the schema's primaryKey value (it doubles as the filename)."
|
|
2793
|
+
},
|
|
2794
|
+
mode: {
|
|
2795
|
+
type: "string",
|
|
2796
|
+
enum: [
|
|
2797
|
+
"upsert",
|
|
2798
|
+
"create",
|
|
2799
|
+
"merge"
|
|
2800
|
+
],
|
|
2801
|
+
description: "putItems: \"upsert\" (default) replaces existing records WHOLE; \"create\" rejects rows whose id already exists; \"merge\" updates only the fields a row carries, keeping the rest of the existing record (rejects unknown ids). Use \"merge\" when changing a few fields."
|
|
2802
|
+
},
|
|
2803
|
+
schema: {
|
|
2804
|
+
type: "object",
|
|
2805
|
+
description: "putSchema: the full collection schema object (same shape as schema.json — title, icon, dataPath, primaryKey, fields, …). Call getSchema first for the current one, and schemaDocs for the field DSL."
|
|
2806
|
+
}
|
|
2807
|
+
},
|
|
2808
|
+
required: ["action"]
|
|
2809
|
+
}
|
|
2810
|
+
},
|
|
2811
|
+
alwaysActive: true,
|
|
2812
|
+
prompt: MANAGE_COLLECTION_PROMPT,
|
|
2813
|
+
handler: (args) => manageCollectionHandler(deps, args)
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2430
2816
|
//#endregion
|
|
2431
2817
|
Object.defineProperty(exports, "CollectionSchemaZ", {
|
|
2432
2818
|
enumerable: true,
|
|
@@ -2434,6 +2820,18 @@ Object.defineProperty(exports, "CollectionSchemaZ", {
|
|
|
2434
2820
|
return CollectionSchemaZ;
|
|
2435
2821
|
}
|
|
2436
2822
|
});
|
|
2823
|
+
Object.defineProperty(exports, "MAX_SCHEMA_ISSUES", {
|
|
2824
|
+
enumerable: true,
|
|
2825
|
+
get: function() {
|
|
2826
|
+
return MAX_SCHEMA_ISSUES;
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
Object.defineProperty(exports, "MAX_UNSELECTIVE_ITEMS", {
|
|
2830
|
+
enumerable: true,
|
|
2831
|
+
get: function() {
|
|
2832
|
+
return MAX_UNSELECTIVE_ITEMS;
|
|
2833
|
+
}
|
|
2834
|
+
});
|
|
2437
2835
|
Object.defineProperty(exports, "ONE_SECOND_MS", {
|
|
2438
2836
|
enumerable: true,
|
|
2439
2837
|
get: function() {
|
|
@@ -2443,7 +2841,7 @@ Object.defineProperty(exports, "ONE_SECOND_MS", {
|
|
|
2443
2841
|
Object.defineProperty(exports, "SCHEMA_FILE", {
|
|
2444
2842
|
enumerable: true,
|
|
2445
2843
|
get: function() {
|
|
2446
|
-
return SCHEMA_FILE;
|
|
2844
|
+
return SCHEMA_FILE$1;
|
|
2447
2845
|
}
|
|
2448
2846
|
});
|
|
2449
2847
|
Object.defineProperty(exports, "acceptParsedSchema", {
|
|
@@ -2632,6 +3030,12 @@ Object.defineProperty(exports, "log", {
|
|
|
2632
3030
|
return log;
|
|
2633
3031
|
}
|
|
2634
3032
|
});
|
|
3033
|
+
Object.defineProperty(exports, "makeManageCollectionTool", {
|
|
3034
|
+
enumerable: true,
|
|
3035
|
+
get: function() {
|
|
3036
|
+
return makeManageCollectionTool;
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
2635
3039
|
Object.defineProperty(exports, "maybeSpawnSuccessor", {
|
|
2636
3040
|
enumerable: true,
|
|
2637
3041
|
get: function() {
|
|
@@ -2777,4 +3181,4 @@ Object.defineProperty(exports, "writeItem", {
|
|
|
2777
3181
|
}
|
|
2778
3182
|
});
|
|
2779
3183
|
|
|
2780
|
-
//# sourceMappingURL=server-
|
|
3184
|
+
//# sourceMappingURL=server-Bh-DPK-H.cjs.map
|