@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.
Files changed (33) hide show
  1. package/dist/collection/index.cjs +44 -51
  2. package/dist/collection/index.cjs.map +1 -1
  3. package/dist/collection/index.js +1 -8
  4. package/dist/collection/index.js.map +1 -1
  5. package/dist/collection/registry/server/index.cjs +2 -2
  6. package/dist/collection/registry/server/index.js +2 -2
  7. package/dist/collection/server/index.cjs +6 -3
  8. package/dist/collection/server/index.d.ts +1 -0
  9. package/dist/collection/server/index.js +3 -3
  10. package/dist/collection/server/manageTool.d.ts +82 -0
  11. package/dist/collection-watchers/index.cjs +4 -4
  12. package/dist/collection-watchers/index.js +2 -2
  13. package/dist/feeds/index.cjs +4 -4
  14. package/dist/feeds/index.js +2 -2
  15. package/dist/feeds/server/index.cjs +5 -5
  16. package/dist/feeds/server/index.js +3 -3
  17. package/dist/{ingestTypes-DvsJhqMx.js → ingestTypes-DEjpiZGM.js} +2 -2
  18. package/dist/{ingestTypes-DvsJhqMx.js.map → ingestTypes-DEjpiZGM.js.map} +1 -1
  19. package/dist/{ingestTypes-Cq65Bv-N.cjs → ingestTypes-Dh5lniBL.cjs} +3 -3
  20. package/dist/{ingestTypes-Cq65Bv-N.cjs.map → ingestTypes-Dh5lniBL.cjs.map} +1 -1
  21. package/dist/{calendarGrid-CRaMAguM.cjs → promptSafety-DN5V__Ku.cjs} +14 -1
  22. package/dist/promptSafety-DN5V__Ku.cjs.map +1 -0
  23. package/dist/{calendarGrid-B3dtFN-m.js → promptSafety-DyG3EuC7.js} +9 -2
  24. package/dist/promptSafety-DyG3EuC7.js.map +1 -0
  25. package/dist/{server-C3FOqabY.js → server-Bd8l1bhv.js} +397 -11
  26. package/dist/server-Bd8l1bhv.js.map +1 -0
  27. package/dist/{server-Cd6HA_3-.cjs → server-Bh-DPK-H.cjs} +444 -40
  28. package/dist/server-Bh-DPK-H.cjs.map +1 -0
  29. package/package.json +1 -1
  30. package/dist/calendarGrid-B3dtFN-m.js.map +0 -1
  31. package/dist/calendarGrid-CRaMAguM.cjs.map +0 -1
  32. package/dist/server-C3FOqabY.js.map +0 -1
  33. package/dist/server-Cd6HA_3-.cjs.map +0 -1
@@ -1,5 +1,6 @@
1
- import { A as SAFE_RECORD_ID_PATTERN, C as coerceNumeric, E as actionVisible, F as COMPUTED_TYPES, I as FEED_SCHEDULES, L as INGEST_KINDS, M as isSafeRecordId, N as isSafeSlug, P as AGENT_INGEST_KIND, R as embedTargetId, S as backlinkRows, T as rollupValue, b as selectDynamicRecord, h as deriveAll, j as SAFE_SLUG_PATTERN, l as parseIsoDate, u as parseIsoDateTime, v as firstDateField, w as projectBacklinkRow, y as resolveIcon, z as isFieldDrivenEvery } from "./calendarGrid-B3dtFN-m.js";
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 { collectionsRegistriesConfigPath as $, recordFieldProblem as A, readItem as B, toSummary as C, validateCollectionRecords as D, firstMutateParamProblem as E, isRegularFile as F, SCHEMA_FILE as G, resolveCreateItemId as H, listItems as I, itemFilePath as J, isContainedInRoot as K, promptPathsFor as L, buildCollectionActionSeedPrompt as M, deleteItem as N, validateRecordObject as O, generateItemId as P, safeSlugName as Q, readCustomViewHtml as R, toDetail as S, applyMutateAction as T, writeItem as U, readSkillTemplate as V, writeFileAtomic as W, resolveTemplatePath as X, resolveDataDir as Y, safeRecordId as Z, buildWorkspaceOntology as _, computeSuccessor as a, discoverCollections as b, isTriggerDue as c, resolveEvery as d, configureCollectionHost as et, successorId as f, enrichItems as g, computeCollectionIcon as h, advanceTriggerDate as i, setCollectionChangePublisher as it, buildActionSeedPrompt as j, compileRecordZ as k, maybeSpawnSuccessor as l, errorMessage as m, deleteCollection as n, log as nt, daysInMonth as o, ONE_SECOND_MS as p, isContainedInWorkspace as q, deleteCollectionRefusalMessage as r, publishCollectionChange as rt, formatCivil as s, deleteCustomView as t, getWorkspaceRoot as tt, parseCivil as u, schemaRelations as v, CollectionSchemaZ as w, loadCollection as x, acceptParsedSchema as y, readCustomViewI18n as z };
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-C3FOqabY.js.map
2817
+ //# sourceMappingURL=server-Bd8l1bhv.js.map