@mulmoclaude/core 0.18.1 → 0.19.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/error-recovery.md +29 -0
- 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,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as isFieldDrivenEvery, C as backlinkRows, D as actionVisible, E as rollupValue, F as AGENT_INGEST_KIND, I as COMPUTED_TYPES, L as FEED_SCHEDULES, M as SAFE_SLUG_PATTERN, N as isSafeRecordId, P as isSafeSlug, R as INGEST_KINDS, T as projectBacklinkRow, b as resolveIcon, d as parseIsoDateTime, g as deriveAll, j as SAFE_RECORD_ID_PATTERN, t as defangForPrompt, u as parseIsoDate, w as coerceNumeric, x as selectDynamicRecord, y as firstDateField, z as embedTargetId } from "./promptSafety-DyG3EuC7.js";
|
|
2
2
|
import { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath } from "./collection/paths.js";
|
|
3
|
+
import { dataSkillDir, mirrorSkillWrite } from "./skill-bridge/index.js";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { promises, realpathSync } from "node:fs";
|
|
5
6
|
import { cp, lstat, mkdir, open, readFile, readdir, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
@@ -60,7 +61,7 @@ function collectionsRegistriesConfigPath() {
|
|
|
60
61
|
const host = requireHost();
|
|
61
62
|
return host.paths.collectionsRegistriesConfig(host.workspaceRoot);
|
|
62
63
|
}
|
|
63
|
-
function isPresetSlug(slug) {
|
|
64
|
+
function isPresetSlug$1(slug) {
|
|
64
65
|
return requireHost().isPresetSlug(slug);
|
|
65
66
|
}
|
|
66
67
|
/** Logger proxy so engine modules can `import { log }` and use it exactly like
|
|
@@ -77,7 +78,7 @@ var log = {
|
|
|
77
78
|
};
|
|
78
79
|
//#endregion
|
|
79
80
|
//#region src/collection/server/paths.ts
|
|
80
|
-
var SCHEMA_FILE = "schema.json";
|
|
81
|
+
var SCHEMA_FILE$1 = "schema.json";
|
|
81
82
|
/** Sanitise a user-supplied slug into a safe directory-name leaf.
|
|
82
83
|
* Returns null for anything that fails the slug whitelist OR isn't a
|
|
83
84
|
* basename (i.e. survives `path.basename` round-trip unchanged).
|
|
@@ -1589,7 +1590,7 @@ function acceptParsedSchema(schema, opts) {
|
|
|
1589
1590
|
async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
|
|
1590
1591
|
const safeName = safeSlugName(slug);
|
|
1591
1592
|
if (safeName === null) return null;
|
|
1592
|
-
const schemaPath = path.join(skillsRoot, safeName, SCHEMA_FILE);
|
|
1593
|
+
const schemaPath = path.join(skillsRoot, safeName, SCHEMA_FILE$1);
|
|
1593
1594
|
let raw;
|
|
1594
1595
|
try {
|
|
1595
1596
|
if (!(await stat(schemaPath)).isFile()) return null;
|
|
@@ -2300,7 +2301,7 @@ async function deleteCollection(collection, opts = {}) {
|
|
|
2300
2301
|
kind: "user-scope",
|
|
2301
2302
|
slug
|
|
2302
2303
|
};
|
|
2303
|
-
if (isPresetSlug(slug)) return {
|
|
2304
|
+
if (isPresetSlug$1(slug)) return {
|
|
2304
2305
|
kind: "preset",
|
|
2305
2306
|
slug
|
|
2306
2307
|
};
|
|
@@ -2366,9 +2367,9 @@ async function canonicalBase(collection, workspaceRoot, safeSlug) {
|
|
|
2366
2367
|
* project collection (no staging mirror) doesn't have an empty staging tree
|
|
2367
2368
|
* materialised by a side effect of the delete. */
|
|
2368
2369
|
async function schemaWriteTargets(collection, workspaceRoot, safeSlug) {
|
|
2369
|
-
const active = path.join(collection.skillDir, SCHEMA_FILE);
|
|
2370
|
+
const active = path.join(collection.skillDir, SCHEMA_FILE$1);
|
|
2370
2371
|
if (collection.source !== "project") return [active];
|
|
2371
|
-
const stagingSchema = path.join(skillsStagingDir(workspaceRoot), safeSlug, SCHEMA_FILE);
|
|
2372
|
+
const stagingSchema = path.join(skillsStagingDir(workspaceRoot), safeSlug, SCHEMA_FILE$1);
|
|
2372
2373
|
const targets = [];
|
|
2373
2374
|
if (await fileExists(stagingSchema)) targets.push(stagingSchema);
|
|
2374
2375
|
targets.push(active);
|
|
@@ -2389,7 +2390,7 @@ async function unlinkIfPresent(target) {
|
|
|
2389
2390
|
* preserved verbatim. */
|
|
2390
2391
|
async function removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug) {
|
|
2391
2392
|
const base = await canonicalBase(collection, workspaceRoot, safeSlug);
|
|
2392
|
-
const canonical = path.join(base, SCHEMA_FILE);
|
|
2393
|
+
const canonical = path.join(base, SCHEMA_FILE$1);
|
|
2393
2394
|
const parsed = JSON.parse(await readFile(canonical, "utf-8"));
|
|
2394
2395
|
if (Array.isArray(parsed.views)) parsed.views = parsed.views.filter((entry) => entry?.id !== viewId);
|
|
2395
2396
|
const serialized = `${JSON.stringify(parsed, null, 2)}\n`;
|
|
@@ -2400,7 +2401,7 @@ async function removeViewFromSchemas(collection, viewId, workspaceRoot, safeSlug
|
|
|
2400
2401
|
* refused (read-only / re-seeded on boot), consistent with `deleteCollection`. */
|
|
2401
2402
|
async function deleteCustomView(collection, viewId, opts = {}) {
|
|
2402
2403
|
if (collection.source === "user") return { kind: "user-scope" };
|
|
2403
|
-
if (isPresetSlug(collection.slug)) return { kind: "preset" };
|
|
2404
|
+
if (isPresetSlug$1(collection.slug)) return { kind: "preset" };
|
|
2404
2405
|
const safeSlug = safeSlugName(collection.slug);
|
|
2405
2406
|
if (safeSlug === null) return {
|
|
2406
2407
|
kind: "unsafe-path",
|
|
@@ -2425,7 +2426,392 @@ async function deleteCustomView(collection, viewId, opts = {}) {
|
|
|
2425
2426
|
viewId
|
|
2426
2427
|
};
|
|
2427
2428
|
}
|
|
2429
|
+
/** True for a launcher-managed preset slug (`mc-<something>`). The sync logic uses
|
|
2430
|
+
* this to bound which active skills it may refresh/prune, so user-authored skills are
|
|
2431
|
+
* never touched. */
|
|
2432
|
+
function isPresetSlug(slug) {
|
|
2433
|
+
return slug.startsWith("mc-") && slug.length > 3;
|
|
2434
|
+
}
|
|
2435
|
+
//#endregion
|
|
2436
|
+
//#region src/collection/server/manageTool.ts
|
|
2437
|
+
/** Refuse an unselective getItems beyond this many records — a silent
|
|
2438
|
+
* truncation would read as "covered everything", and an unbounded dump
|
|
2439
|
+
* of a large collection is a token bomb. `ids` or `fields` lifts it. */
|
|
2440
|
+
var MAX_UNSELECTIVE_ITEMS = 200;
|
|
2441
|
+
/** schema.json basename under a skill dir (canonical staging + active mirror). */
|
|
2442
|
+
var SCHEMA_FILE = "schema.json";
|
|
2443
|
+
/** The collection-authoring reference, served by the `schemaDocs` action. */
|
|
2444
|
+
var SCHEMA_DOCS_FILE = "collection-skills.md";
|
|
2445
|
+
/** Cap the rejected-schema issue list so a deeply-broken schema can't flood the result. */
|
|
2446
|
+
var MAX_SCHEMA_ISSUES = 20;
|
|
2447
|
+
/** The workspace help-docs dir both hosts seed (`@mulmoclaude/core/workspace-setup`
|
|
2448
|
+
* syncs the bundled assets here) — the user-editable copy schemaDocs prefers. */
|
|
2449
|
+
var HELPS_DIR = "config/helps";
|
|
2450
|
+
/** Resolve the workspace root the same way every collections call does:
|
|
2451
|
+
* the injected override (tests) or the configured collection host. */
|
|
2452
|
+
function resolveBase(deps) {
|
|
2453
|
+
return deps.workspaceRoot ?? getWorkspaceRoot();
|
|
2454
|
+
}
|
|
2455
|
+
/** Shared "unknown collection" message — its schema.json is missing or
|
|
2456
|
+
* failed validation, so discovery skipped it. */
|
|
2457
|
+
function unknownCollection(slug) {
|
|
2458
|
+
return `manageCollection: unknown collection '${defangForPrompt(slug)}' — its schema.json is missing or failed validation.`;
|
|
2459
|
+
}
|
|
2460
|
+
function optionalStringArray(value, name) {
|
|
2461
|
+
if (value === void 0) return { ok: true };
|
|
2462
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string" && entry.length > 0)) return {
|
|
2463
|
+
ok: false,
|
|
2464
|
+
error: `manageCollection: \`${name}\` must be an array of non-empty strings when present.`
|
|
2465
|
+
};
|
|
2466
|
+
return {
|
|
2467
|
+
ok: true,
|
|
2468
|
+
value
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
/** Project a record down to the requested fields. The primary key is
|
|
2472
|
+
* always kept so every returned record stays addressable for a
|
|
2473
|
+
* follow-up getItems/putItems. */
|
|
2474
|
+
function projectFields(record, fields, primaryKey) {
|
|
2475
|
+
const keys = fields.includes(primaryKey) ? fields : [primaryKey, ...fields];
|
|
2476
|
+
return Object.fromEntries(keys.filter((key) => key in record).map((key) => [key, record[key]]));
|
|
2477
|
+
}
|
|
2478
|
+
/** The validation warning appended to a getItems result when stored
|
|
2479
|
+
* record files are malformed (they're silently skipped at read time,
|
|
2480
|
+
* so without this they'd just look missing). Issue strings quote
|
|
2481
|
+
* record-controlled text → defanged, mirroring the presentCollection
|
|
2482
|
+
* dispatch. The record VALUES in `items` ride verbatim, like a raw
|
|
2483
|
+
* file Read — only host-composed report strings are defanged. */
|
|
2484
|
+
async function recordIssuesWarning(collection, deps) {
|
|
2485
|
+
if (deps.ablateValidation) return void 0;
|
|
2486
|
+
const issues = await validateCollectionRecords(collection, { workspaceRoot: deps.workspaceRoot });
|
|
2487
|
+
if (issues.length === 0) return void 0;
|
|
2488
|
+
const lines = issues.map((issue) => `- ${defangForPrompt(issue.file)}: ${defangForPrompt(issue.problem)}`).join("\n");
|
|
2489
|
+
return `${issues.length} record file(s) have data problems and are missing from this result. Fix each (Read → correct → Write):\n${lines}`;
|
|
2490
|
+
}
|
|
2491
|
+
async function loadRequestedItems(collection, ids, deps) {
|
|
2492
|
+
if (!ids) return {
|
|
2493
|
+
items: await listItems(collection.dataDir, { workspaceRoot: deps.workspaceRoot }),
|
|
2494
|
+
missing: []
|
|
2495
|
+
};
|
|
2496
|
+
const items = [];
|
|
2497
|
+
const missing = [];
|
|
2498
|
+
for (const recordId of ids) {
|
|
2499
|
+
const item = await readItem(collection.dataDir, recordId, { workspaceRoot: deps.workspaceRoot }).catch(() => null);
|
|
2500
|
+
if (item) items.push(item);
|
|
2501
|
+
else missing.push(recordId);
|
|
2502
|
+
}
|
|
2503
|
+
return {
|
|
2504
|
+
items,
|
|
2505
|
+
missing
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
async function handleGetItems(collection, args, deps) {
|
|
2509
|
+
const { items, missing } = await loadRequestedItems(collection, args.ids, deps);
|
|
2510
|
+
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.`;
|
|
2511
|
+
const enriched = await enrichItems(collection, items, deps);
|
|
2512
|
+
const projected = args.fields ? enriched.map((item) => projectFields(item, args.fields, collection.schema.primaryKey)) : enriched;
|
|
2513
|
+
const warning = !args.ids || missing.length > 0 ? await recordIssuesWarning(collection, deps) : void 0;
|
|
2514
|
+
return JSON.stringify({
|
|
2515
|
+
collection: collection.slug,
|
|
2516
|
+
count: projected.length,
|
|
2517
|
+
items: projected,
|
|
2518
|
+
...missing.length > 0 ? { missing: missing.map((recordId) => defangForPrompt(recordId)) } : {},
|
|
2519
|
+
...warning ? { warning } : {}
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
/** Reject writes that set host-computed keys, with a pointer at the
|
|
2523
|
+
* writable source of truth (the toggle's enum) where one exists. */
|
|
2524
|
+
function computedKeyProblem(record, schema) {
|
|
2525
|
+
for (const key of Object.keys(record)) {
|
|
2526
|
+
const spec = schema.fields[key];
|
|
2527
|
+
if (!spec || !COMPUTED_TYPES.has(spec.type)) continue;
|
|
2528
|
+
if (spec.type === "toggle" && spec.field) return `'${key}' is a toggle projection — write the enum field '${spec.field}' instead`;
|
|
2529
|
+
return `'${key}' is ${{
|
|
2530
|
+
derived: "derived",
|
|
2531
|
+
embed: "an embed",
|
|
2532
|
+
backlinks: "a backlinks view",
|
|
2533
|
+
rollup: "a rollup"
|
|
2534
|
+
}[spec.type] ?? "computed"} — computed by the host, remove it from the record`;
|
|
2535
|
+
}
|
|
2536
|
+
return null;
|
|
2537
|
+
}
|
|
2538
|
+
/** `mode: "merge"` resolves the row against the EXISTING record —
|
|
2539
|
+
* a partial row updates just the fields it carries, instead of a
|
|
2540
|
+
* whole-record upsert silently erasing the optional fields it omits
|
|
2541
|
+
* (an upsert of `{id, status}` would pass validation yet drop
|
|
2542
|
+
* `notes`/`lesson`/…). Merge is a partial UPDATE by definition, so a
|
|
2543
|
+
* missing record is a reject, not an implicit create — a merged-over-
|
|
2544
|
+
* nothing partial record is exactly the data shape this mode exists
|
|
2545
|
+
* to prevent.
|
|
2546
|
+
*
|
|
2547
|
+
* Computed keys found in the STORED record are stripped before the
|
|
2548
|
+
* merge: the caller's own row was already computed-key-rejected, but a
|
|
2549
|
+
* raw-written / legacy record can carry a stale `derived`/`embed`/
|
|
2550
|
+
* `toggle` value, and re-writing it would perpetuate a forged
|
|
2551
|
+
* host-computed value. A merge heals the record instead.
|
|
2552
|
+
*
|
|
2553
|
+
* readItem THROWS on a malformed stored file (only ENOENT is null) —
|
|
2554
|
+
* downgraded to a per-row rejection here, like loadRequestedItems'
|
|
2555
|
+
* `missing`, so one broken file can't abort the whole putItems batch. */
|
|
2556
|
+
async function mergeWithExisting(collection, record, itemId, deps) {
|
|
2557
|
+
let existing;
|
|
2558
|
+
try {
|
|
2559
|
+
existing = await readItem(collection.dataDir, itemId, { workspaceRoot: deps.workspaceRoot });
|
|
2560
|
+
} catch {
|
|
2561
|
+
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"`;
|
|
2562
|
+
}
|
|
2563
|
+
if (!existing) return `'${itemId}' not found — mode "merge" updates an existing record; use "upsert" or "create" to add it`;
|
|
2564
|
+
const stored = Object.entries(existing).filter(([key]) => {
|
|
2565
|
+
const spec = collection.schema.fields[key];
|
|
2566
|
+
return !spec || !COMPUTED_TYPES.has(spec.type);
|
|
2567
|
+
});
|
|
2568
|
+
return {
|
|
2569
|
+
...Object.fromEntries(stored),
|
|
2570
|
+
...record
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
async function putOneItem(collection, record, mode, deps) {
|
|
2574
|
+
const { schema } = collection;
|
|
2575
|
+
const itemId = resolveCreateItemId(schema, record);
|
|
2576
|
+
const reject = (about, problem) => ({ rejected: {
|
|
2577
|
+
id: defangForPrompt(about),
|
|
2578
|
+
problem: defangForPrompt(problem)
|
|
2579
|
+
} });
|
|
2580
|
+
if (itemId === null) return reject("(no id)", `record has no '${schema.primaryKey}' value — set it (it doubles as the filename)`);
|
|
2581
|
+
const computed = computedKeyProblem(record, schema);
|
|
2582
|
+
if (computed) return reject(itemId, computed);
|
|
2583
|
+
let toWrite = record;
|
|
2584
|
+
if (mode === "merge") {
|
|
2585
|
+
const merged = await mergeWithExisting(collection, record, itemId, deps);
|
|
2586
|
+
if (typeof merged === "string") return reject(itemId, merged);
|
|
2587
|
+
toWrite = merged;
|
|
2588
|
+
}
|
|
2589
|
+
if (!deps.ablateValidation) {
|
|
2590
|
+
const invalid = validateRecordObject(toWrite, itemId, schema);
|
|
2591
|
+
if (invalid) return reject(itemId, invalid);
|
|
2592
|
+
}
|
|
2593
|
+
const result = await writeItem(collection.dataDir, itemId, toWrite, {
|
|
2594
|
+
refuseOverwrite: mode === "create",
|
|
2595
|
+
workspaceRoot: deps.workspaceRoot,
|
|
2596
|
+
slug: collection.slug
|
|
2597
|
+
});
|
|
2598
|
+
if (result.kind === "ok") return { written: result.itemId };
|
|
2599
|
+
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)`);
|
|
2600
|
+
if (result.kind === "conflict") return reject(itemId, `'${itemId}' already exists — mode "create" refuses overwrite; use "upsert" to update it`);
|
|
2601
|
+
return reject(itemId, "write refused: the collection's data dir escapes the workspace");
|
|
2602
|
+
}
|
|
2603
|
+
async function handlePutItems(collection, args, deps) {
|
|
2604
|
+
const written = [];
|
|
2605
|
+
const rejected = [];
|
|
2606
|
+
for (const record of args.items) {
|
|
2607
|
+
const outcome = await putOneItem(collection, record, args.mode, deps);
|
|
2608
|
+
if (outcome.written) written.push(outcome.written);
|
|
2609
|
+
if (outcome.rejected) rejected.push(outcome.rejected);
|
|
2610
|
+
}
|
|
2611
|
+
return JSON.stringify({
|
|
2612
|
+
collection: collection.slug,
|
|
2613
|
+
written,
|
|
2614
|
+
rejected
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
function parsePutItems(args, slug) {
|
|
2618
|
+
const { items, mode } = args;
|
|
2619
|
+
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.";
|
|
2620
|
+
if (mode !== void 0 && mode !== "upsert" && mode !== "create" && mode !== "merge") return "manageCollection: `mode` must be \"upsert\" (default), \"create\", or \"merge\".";
|
|
2621
|
+
return {
|
|
2622
|
+
slug,
|
|
2623
|
+
items,
|
|
2624
|
+
mode: mode ?? "upsert"
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
/** The machine-readable workspace ontology: every collection with its
|
|
2628
|
+
* identity, record count, and outbound `ref`/`embed` relations. Slugs
|
|
2629
|
+
* are discovery-sanitized; titles/labels are workspace-authored schema
|
|
2630
|
+
* text and ride verbatim — the same trust class as the record values
|
|
2631
|
+
* getItems returns. */
|
|
2632
|
+
async function handleGetOntology(deps) {
|
|
2633
|
+
const collections = await buildWorkspaceOntology(deps);
|
|
2634
|
+
return JSON.stringify({
|
|
2635
|
+
count: collections.length,
|
|
2636
|
+
collections
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
/** Return the collection-authoring reference (`collection-skills.md`).
|
|
2640
|
+
* Workspace copy first (reflects user edits), bundled asset as the
|
|
2641
|
+
* always-present fallback. Both reads guarded; if neither resolves the
|
|
2642
|
+
* agent still gets an actionable message instead of a thrown call. */
|
|
2643
|
+
async function handleSchemaDocs(deps) {
|
|
2644
|
+
const candidates = [path.join(resolveBase(deps), HELPS_DIR, SCHEMA_DOCS_FILE), ...deps.bundledHelpsDir ? [path.join(deps.bundledHelpsDir(), SCHEMA_DOCS_FILE)] : []];
|
|
2645
|
+
for (const candidate of candidates) try {
|
|
2646
|
+
return await readFile(candidate, "utf-8");
|
|
2647
|
+
} catch {}
|
|
2648
|
+
return `manageCollection: could not read the collection-authoring reference (${SCHEMA_DOCS_FILE}).`;
|
|
2649
|
+
}
|
|
2650
|
+
/** Return the raw schema.json of an existing collection, for editing.
|
|
2651
|
+
* Staging (the canonical writable copy) first, the active mirror as a
|
|
2652
|
+
* fallback for user-scope skills that have no staging copy. Raw text —
|
|
2653
|
+
* not the parsed schema — so the agent edits the true on-disk source. */
|
|
2654
|
+
async function handleGetSchema(slug, deps) {
|
|
2655
|
+
const collection = await loadCollection(slug, deps);
|
|
2656
|
+
if (!collection) return unknownCollection(slug);
|
|
2657
|
+
const candidates = [path.join(dataSkillDir(resolveBase(deps), collection.slug), SCHEMA_FILE), path.join(collection.skillDir, SCHEMA_FILE)];
|
|
2658
|
+
for (const candidate of candidates) try {
|
|
2659
|
+
return await readFile(candidate, "utf-8");
|
|
2660
|
+
} catch {}
|
|
2661
|
+
return `manageCollection: '${defangForPrompt(slug)}' has no readable ${SCHEMA_FILE}.`;
|
|
2662
|
+
}
|
|
2663
|
+
/** Refuse a schema edit the host can't honour: user-scope/feed collections
|
|
2664
|
+
* are read-only, and presets (mc-*) re-seed on restart so an edit is lost. */
|
|
2665
|
+
function schemaEditRefusal(collection, slug) {
|
|
2666
|
+
if (collection.source !== "project") return `manageCollection: '${defangForPrompt(slug)}' is ${collection.source}-scope and read-only here — only project collections (data/skills/) can be edited.`;
|
|
2667
|
+
if (isPresetSlug(slug)) return `manageCollection: '${defangForPrompt(slug)}' is a preset (mc-*) and re-seeds on restart — copy it under a different slug to customise.`;
|
|
2668
|
+
return null;
|
|
2669
|
+
}
|
|
2670
|
+
/** Turn a CollectionSchemaZ failure into a short, actionable list the
|
|
2671
|
+
* agent can fix, pointing back at the field reference. */
|
|
2672
|
+
function formatSchemaIssues(issues) {
|
|
2673
|
+
const shown = issues.slice(0, 20);
|
|
2674
|
+
const lines = shown.map((issue) => `- ${issue.path.map(String).join(".") || "(root)"}: ${defangForPrompt(issue.message)}`).join("\n");
|
|
2675
|
+
const omitted = issues.length - shown.length;
|
|
2676
|
+
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.` : ""}`;
|
|
2677
|
+
}
|
|
2678
|
+
/** Write the validated schema to the canonical staging copy, then mirror
|
|
2679
|
+
* it into the active `.claude/skills/` tree discovery reads — an internal
|
|
2680
|
+
* write doesn't fire the skill-bridge hook, so we mirror explicitly. */
|
|
2681
|
+
async function writeAndMirrorSchema(slug, schema, deps) {
|
|
2682
|
+
const base = resolveBase(deps);
|
|
2683
|
+
await writeFileAtomic(path.join(dataSkillDir(base, slug), SCHEMA_FILE), `${JSON.stringify(schema, null, 2)}\n`);
|
|
2684
|
+
mirrorSkillWrite(base, {
|
|
2685
|
+
slug,
|
|
2686
|
+
relSegments: [SCHEMA_FILE]
|
|
2687
|
+
});
|
|
2688
|
+
try {
|
|
2689
|
+
await deps.refreshAfterWrite?.();
|
|
2690
|
+
} catch {}
|
|
2691
|
+
}
|
|
2692
|
+
/** The post-Zod acceptance gates discovery applies before a parsed schema
|
|
2693
|
+
* becomes a live collection. Mirrors discovery's checks (`loadOneCollection`)
|
|
2694
|
+
* so a write can't pass here yet be silently skipped on the next load.
|
|
2695
|
+
* putSchema only runs for project-scope collections, so the feed-`ingest`
|
|
2696
|
+
* gate doesn't apply. Returns a one-line reason, or null when the schema
|
|
2697
|
+
* would be accepted. */
|
|
2698
|
+
function schemaDiscoveryGate(schema, base) {
|
|
2699
|
+
const primaryField = schema.fields[schema.primaryKey];
|
|
2700
|
+
if (!primaryField) return `primaryKey '${schema.primaryKey}' is not one of the declared fields`;
|
|
2701
|
+
if (primaryField.primary !== true) return `the primaryKey field '${schema.primaryKey}' must be flagged \`primary: true\``;
|
|
2702
|
+
if (resolveDataDir(schema.dataPath, base) === null) return `dataPath '${schema.dataPath}' escapes the workspace`;
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
/** Validate a schema against CollectionSchemaZ and, on success, persist it.
|
|
2706
|
+
* Edit-only: a new collection is created by writing SKILL.md + schema.json
|
|
2707
|
+
* under data/skills/<slug>/ (the normal create flow), not through here. */
|
|
2708
|
+
async function handlePutSchema(slug, schemaArg, deps) {
|
|
2709
|
+
if (!schemaArg || typeof schemaArg !== "object" || Array.isArray(schemaArg)) return "manageCollection: `schema` is required for putSchema — the full collection schema object.";
|
|
2710
|
+
const collection = await loadCollection(slug, deps);
|
|
2711
|
+
if (!collection) return `manageCollection: unknown collection '${defangForPrompt(slug)}' — create it by writing SKILL.md + ${SCHEMA_FILE} under data/skills/${defangForPrompt(slug)}/, then edit it here.`;
|
|
2712
|
+
const refusal = schemaEditRefusal(collection, slug);
|
|
2713
|
+
if (refusal) return refusal;
|
|
2714
|
+
const parsed = CollectionSchemaZ.safeParse(schemaArg);
|
|
2715
|
+
if (!parsed.success) return formatSchemaIssues(parsed.error.issues);
|
|
2716
|
+
const gate = schemaDiscoveryGate(parsed.data, resolveBase(deps));
|
|
2717
|
+
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.`;
|
|
2718
|
+
await writeAndMirrorSchema(collection.slug, parsed.data, deps);
|
|
2719
|
+
return JSON.stringify({
|
|
2720
|
+
collection: collection.slug,
|
|
2721
|
+
written: true
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
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.";
|
|
2725
|
+
/** Validate getItems' optional `ids`/`fields` args, then delegate. */
|
|
2726
|
+
async function dispatchGetItems(collection, args, deps) {
|
|
2727
|
+
const ids = optionalStringArray(args.ids, "ids");
|
|
2728
|
+
if (!ids.ok) return ids.error;
|
|
2729
|
+
const fields = optionalStringArray(args.fields, "fields");
|
|
2730
|
+
if (!fields.ok) return fields.error;
|
|
2731
|
+
return handleGetItems(collection, {
|
|
2732
|
+
slug: collection.slug,
|
|
2733
|
+
ids: ids.value,
|
|
2734
|
+
fields: fields.value
|
|
2735
|
+
}, deps);
|
|
2736
|
+
}
|
|
2737
|
+
async function manageCollectionHandler(deps, args) {
|
|
2738
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
2739
|
+
if (action === "schemaDocs") return handleSchemaDocs(deps);
|
|
2740
|
+
if (action === "getOntology") return handleGetOntology(deps);
|
|
2741
|
+
const slug = typeof args.slug === "string" ? args.slug.trim() : "";
|
|
2742
|
+
if (!slug) return "manageCollection: `slug` is required (the collection's slug).";
|
|
2743
|
+
if (action === "getSchema") return handleGetSchema(slug, deps);
|
|
2744
|
+
if (action === "putSchema") return handlePutSchema(slug, args.schema, deps);
|
|
2745
|
+
if (action !== "getItems" && action !== "putItems") return "manageCollection: `action` must be \"getItems\", \"putItems\", \"getOntology\", \"schemaDocs\", \"getSchema\", or \"putSchema\".";
|
|
2746
|
+
const collection = await loadCollection(slug, deps);
|
|
2747
|
+
if (!collection) return unknownCollection(slug);
|
|
2748
|
+
if (action === "getItems") return dispatchGetItems(collection, args, deps);
|
|
2749
|
+
const parsed = parsePutItems(args, slug);
|
|
2750
|
+
if (typeof parsed === "string") return parsed;
|
|
2751
|
+
return handlePutItems(collection, parsed, deps);
|
|
2752
|
+
}
|
|
2753
|
+
function makeManageCollectionTool(deps = {}) {
|
|
2754
|
+
return {
|
|
2755
|
+
definition: {
|
|
2756
|
+
name: "manageCollection",
|
|
2757
|
+
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.",
|
|
2758
|
+
inputSchema: {
|
|
2759
|
+
type: "object",
|
|
2760
|
+
properties: {
|
|
2761
|
+
action: {
|
|
2762
|
+
type: "string",
|
|
2763
|
+
enum: [
|
|
2764
|
+
"getItems",
|
|
2765
|
+
"putItems",
|
|
2766
|
+
"getOntology",
|
|
2767
|
+
"schemaDocs",
|
|
2768
|
+
"getSchema",
|
|
2769
|
+
"putSchema"
|
|
2770
|
+
],
|
|
2771
|
+
description: "What to do."
|
|
2772
|
+
},
|
|
2773
|
+
slug: {
|
|
2774
|
+
type: "string",
|
|
2775
|
+
description: "The collection's slug (its directory name, e.g. `stock-quotes`). Required for everything except schemaDocs and getOntology."
|
|
2776
|
+
},
|
|
2777
|
+
ids: {
|
|
2778
|
+
type: "array",
|
|
2779
|
+
items: { type: "string" },
|
|
2780
|
+
description: "getItems: only these record ids (primary-key values). Omit for all records."
|
|
2781
|
+
},
|
|
2782
|
+
fields: {
|
|
2783
|
+
type: "array",
|
|
2784
|
+
items: { type: "string" },
|
|
2785
|
+
description: "getItems: only these fields per record (the primary key is always included). Omit for all fields. Use on large collections."
|
|
2786
|
+
},
|
|
2787
|
+
items: {
|
|
2788
|
+
type: "array",
|
|
2789
|
+
items: { type: "object" },
|
|
2790
|
+
description: "putItems: the record objects to store. Each must carry the schema's primaryKey value (it doubles as the filename)."
|
|
2791
|
+
},
|
|
2792
|
+
mode: {
|
|
2793
|
+
type: "string",
|
|
2794
|
+
enum: [
|
|
2795
|
+
"upsert",
|
|
2796
|
+
"create",
|
|
2797
|
+
"merge"
|
|
2798
|
+
],
|
|
2799
|
+
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."
|
|
2800
|
+
},
|
|
2801
|
+
schema: {
|
|
2802
|
+
type: "object",
|
|
2803
|
+
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."
|
|
2804
|
+
}
|
|
2805
|
+
},
|
|
2806
|
+
required: ["action"]
|
|
2807
|
+
}
|
|
2808
|
+
},
|
|
2809
|
+
alwaysActive: true,
|
|
2810
|
+
prompt: MANAGE_COLLECTION_PROMPT,
|
|
2811
|
+
handler: (args) => manageCollectionHandler(deps, args)
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2428
2814
|
//#endregion
|
|
2429
|
-
export {
|
|
2815
|
+
export { resolveTemplatePath as $, validateCollectionRecords as A, promptPathsFor as B, discoverCollections as C, CollectionSchemaZ as D, toSummary as E, buildCollectionActionSeedPrompt as F, resolveCreateItemId as G, readCustomViewI18n as H, deleteItem as I, SCHEMA_FILE$1 as J, writeItem as K, generateItemId as L, compileRecordZ as M, recordFieldProblem as N, applyMutateAction as O, buildActionSeedPrompt as P, resolveDataDir as Q, isRegularFile as R, acceptParsedSchema as S, toDetail as T, readItem as U, readCustomViewHtml as V, readSkillTemplate as W, isContainedInWorkspace as X, isContainedInRoot as Y, itemFilePath as Z, errorMessage as _, deleteCollection as a, log as at, buildWorkspaceOntology as b, computeSuccessor as c, isTriggerDue as d, safeRecordId as et, maybeSpawnSuccessor as f, ONE_SECOND_MS as g, successorId as h, deleteCustomView as i, getWorkspaceRoot as it, validateRecordObject as j, firstMutateParamProblem as k, daysInMonth as l, resolveEvery as m, MAX_UNSELECTIVE_ITEMS as n, collectionsRegistriesConfigPath as nt, deleteCollectionRefusalMessage as o, publishCollectionChange as ot, parseCivil as p, writeFileAtomic as q, makeManageCollectionTool as r, configureCollectionHost as rt, advanceTriggerDate as s, setCollectionChangePublisher as st, MAX_SCHEMA_ISSUES as t, safeSlugName as tt, formatCivil as u, computeCollectionIcon as v, loadCollection as w, schemaRelations as x, enrichItems as y, listItems as z };
|
|
2430
2816
|
|
|
2431
|
-
//# sourceMappingURL=server-
|
|
2817
|
+
//# sourceMappingURL=server-Bd8l1bhv.js.map
|