@mulmoclaude/core 3.5.0 → 3.7.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 (56) hide show
  1. package/assets/helps/collection-skills.md +49 -1
  2. package/assets/helps/error-recovery.md +53 -0
  3. package/dist/calendarGrid-CQ8MVSRb.js.map +1 -1
  4. package/dist/calendarGrid-DGILaVxI.cjs.map +1 -1
  5. package/dist/collection/core/schema.d.ts +8 -1
  6. package/dist/collection/core/schemaZ.d.ts +36 -26
  7. package/dist/collection/firestore.cjs +51 -0
  8. package/dist/collection/firestore.cjs.map +1 -0
  9. package/dist/collection/firestore.d.ts +1 -0
  10. package/dist/collection/firestore.js +50 -0
  11. package/dist/collection/firestore.js.map +1 -0
  12. package/dist/collection/registry/server/index.cjs +19 -19
  13. package/dist/collection/registry/server/index.cjs.map +1 -1
  14. package/dist/collection/registry/server/index.js +2 -2
  15. package/dist/collection/server/appManifest.d.ts +53 -0
  16. package/dist/collection/server/delete.d.ts +10 -0
  17. package/dist/collection/server/discoveredCollection.d.ts +10 -0
  18. package/dist/collection/server/discovery.d.ts +1 -0
  19. package/dist/collection/server/firestoreDocs.d.ts +39 -0
  20. package/dist/collection/server/firestoreStore.d.ts +14 -0
  21. package/dist/collection/server/host.d.ts +52 -0
  22. package/dist/collection/server/index.cjs +72 -52
  23. package/dist/collection/server/index.d.ts +8 -1
  24. package/dist/collection/server/index.js +3 -3
  25. package/dist/collection/server/manageTool.d.ts +4 -0
  26. package/dist/collection/server/publish.d.ts +56 -0
  27. package/dist/collection/server/publishChecks.d.ts +29 -0
  28. package/dist/collection/server/publishManifest.d.ts +181 -0
  29. package/dist/collection/server/publishProject.d.ts +86 -0
  30. package/dist/collection/server/validate.d.ts +12 -0
  31. package/dist/collection-watchers/index.cjs +143 -52
  32. package/dist/collection-watchers/index.cjs.map +1 -1
  33. package/dist/collection-watchers/index.js +132 -41
  34. package/dist/collection-watchers/index.js.map +1 -1
  35. package/dist/collection-watchers/reconciler.d.ts +1 -1
  36. package/dist/feeds/server/index.cjs +10 -10
  37. package/dist/feeds/server/index.cjs.map +1 -1
  38. package/dist/feeds/server/index.js +2 -2
  39. package/dist/google/index.cjs +12 -12
  40. package/dist/google/index.cjs.map +1 -1
  41. package/dist/google/index.js +1 -1
  42. package/dist/{server-BiRLLMpW.js → server-B48Jyxcj.js} +1100 -230
  43. package/dist/server-B48Jyxcj.js.map +1 -0
  44. package/dist/{server-5EMj3naj.cjs → server-CWZyg8fn.cjs} +1333 -385
  45. package/dist/server-CWZyg8fn.cjs.map +1 -0
  46. package/dist/{discovery-Ck4AqikY.cjs → store-5_P_NsGa.cjs} +2108 -1755
  47. package/dist/store-5_P_NsGa.cjs.map +1 -0
  48. package/dist/{discovery-DH9wweuj.js → store-_61sO8K8.js} +2333 -2022
  49. package/dist/store-_61sO8K8.js.map +1 -0
  50. package/dist/whisper/index.cjs +1 -1
  51. package/dist/whisper/index.js +1 -1
  52. package/package.json +7 -1
  53. package/dist/discovery-Ck4AqikY.cjs.map +0 -1
  54. package/dist/discovery-DH9wweuj.js.map +0 -1
  55. package/dist/server-5EMj3naj.cjs.map +0 -1
  56. package/dist/server-BiRLLMpW.js.map +0 -1
@@ -0,0 +1,50 @@
1
+ import { collection, doc, getDoc, getDocs, orderBy, query, runTransaction, setDoc } from "firebase/firestore";
2
+ //#region src/collection/server/firestoreDocs.ts
3
+ /** The real implementation over the modular SDK.
4
+ *
5
+ * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not
6
+ * nested under a `data` key. This is not a matter of taste: the deployed
7
+ * security rules read `request.resource.data.<field>` and
8
+ * `resource.data[submit.emailField]` — a wrapper would put every field one
9
+ * level down, so the required-field checks, the status state machine and the
10
+ * "your own row" predicate would all read absent values and fail closed. A
11
+ * shared record's shape is part of the authorization contract.
12
+ *
13
+ * `orderBy("__name__")` orders by DOCUMENT ID. Ordering by a record field
14
+ * would silently EXCLUDE documents missing that field (the trap
15
+ * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a
16
+ * partial one; the document id is always present and gives the stable order
17
+ * the store contract requires. */
18
+ function createFirestoreDocs(database) {
19
+ return {
20
+ list: async (collectionPath) => {
21
+ return (await getDocs(query(collection(database, collectionPath), orderBy("__name__")))).docs.map((entry) => ({
22
+ id: entry.id,
23
+ data: entry.data()
24
+ }));
25
+ },
26
+ get: async (collectionPath, docId) => {
27
+ const snapshot = await getDoc(doc(database, collectionPath, docId));
28
+ return snapshot.exists() ? snapshot.data() : null;
29
+ },
30
+ set: async (collectionPath, docId, data) => {
31
+ await setDoc(doc(database, collectionPath, docId), data);
32
+ },
33
+ create: (collectionPath, docId, data) => runTransaction(database, async (transaction) => {
34
+ const ref = doc(database, collectionPath, docId);
35
+ if ((await transaction.get(ref)).exists()) return false;
36
+ transaction.set(ref, data);
37
+ return true;
38
+ }),
39
+ delete: (collectionPath, docId) => runTransaction(database, async (transaction) => {
40
+ const ref = doc(database, collectionPath, docId);
41
+ if (!(await transaction.get(ref)).exists()) return false;
42
+ transaction.delete(ref);
43
+ return true;
44
+ })
45
+ };
46
+ }
47
+ //#endregion
48
+ export { createFirestoreDocs };
49
+
50
+ //# sourceMappingURL=firestore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firestore.js","names":[],"sources":["../../src/collection/server/firestoreDocs.ts"],"sourcesContent":["// The seam between the firestore store and the Firestore SDK.\n//\n// The modular SDK is function-based (`getDocs(query(collection(db, …)))`), so a\n// store that imported those functions directly could not be tested without a\n// real backend — a fake `db` would still be handed to the real functions.\n// Narrowing to this interface makes the backend swappable: core ships\n// `createFirestoreDocs` over the real SDK, and the tests inject an in-memory\n// fake that satisfies the same shape. That is what lets this repository's tests\n// run with no API key and no network.\n//\n// Deliberately minimal and id-keyed: no query builder, no field ordering, no\n// cursors. Everything the store needs is \"the documents of one collection path,\n// ordered by document id\" — see the store header for why field ordering is\n// avoided entirely.\n//\n// The collection path is an ARGUMENT, not something this module composes. It\n// owns the SDK calls; `firestoreStore` owns where the documents live.\n\nimport {\n collection as firestoreCollection,\n doc,\n getDoc,\n getDocs,\n orderBy,\n query as firestoreQuery,\n runTransaction,\n setDoc,\n type Firestore,\n} from \"firebase/firestore\";\n\n/** One stored record document. `data` is the document's own fields — the\n * record itself, not a wrapper around it; see `set` below. The store\n * validates its shape, because a document written by hand could be\n * anything. */\nexport interface FirestoreDoc {\n id: string;\n data: unknown;\n}\n\nexport interface FirestoreDocs {\n /** Every document under `collectionPath`, ordered by document id. */\n list: (collectionPath: string) => Promise<FirestoreDoc[]>;\n /** One document's fields, or null when it doesn't exist. */\n get: (collectionPath: string, docId: string) => Promise<unknown | null>;\n /** Create or replace. */\n set: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<void>;\n /** Create only. Returns false when the id already exists — atomic, so two\n * concurrent creates can't both observe \"missing\". */\n create: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<boolean>;\n /** Delete. Returns false when the id didn't exist, so a caller can tell a\n * real delete from a typo'd id. */\n delete: (collectionPath: string, docId: string) => Promise<boolean>;\n}\n\n/** The real implementation over the modular SDK.\n *\n * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not\n * nested under a `data` key. This is not a matter of taste: the deployed\n * security rules read `request.resource.data.<field>` and\n * `resource.data[submit.emailField]` — a wrapper would put every field one\n * level down, so the required-field checks, the status state machine and the\n * \"your own row\" predicate would all read absent values and fail closed. A\n * shared record's shape is part of the authorization contract.\n *\n * `orderBy(\"__name__\")` orders by DOCUMENT ID. Ordering by a record field\n * would silently EXCLUDE documents missing that field (the trap\n * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a\n * partial one; the document id is always present and gives the stable order\n * the store contract requires. */\nexport function createFirestoreDocs(database: Firestore): FirestoreDocs {\n return {\n list: async (collectionPath) => {\n const snapshot = await getDocs(firestoreQuery(firestoreCollection(database, collectionPath), orderBy(\"__name__\")));\n return snapshot.docs.map((entry) => ({ id: entry.id, data: entry.data() }));\n },\n get: async (collectionPath, docId) => {\n const snapshot = await getDoc(doc(database, collectionPath, docId));\n return snapshot.exists() ? snapshot.data() : null;\n },\n set: async (collectionPath, docId, data) => {\n await setDoc(doc(database, collectionPath, docId), data);\n },\n create: (collectionPath, docId, data) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (existing.exists()) return false;\n transaction.set(ref, data);\n return true;\n }),\n // Firestore's deleteDoc succeeds on a missing document, so an existence\n // check is what makes \"deleted\" distinguishable from \"there was nothing\".\n // It runs INSIDE the transaction (like `create`): a plain get-then-delete\n // would report `true` for a document a concurrent client had already\n // removed, i.e. claim a delete this call never performed.\n delete: (collectionPath, docId) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (!existing.exists()) return false;\n transaction.delete(ref);\n return true;\n }),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqEA,SAAgB,oBAAoB,UAAoC;CACtE,OAAO;EACL,MAAM,OAAO,mBAAmB;GAE9B,QAAO,MADgB,QAAQ,MAAe,WAAoB,UAAU,cAAc,GAAG,QAAQ,UAAU,CAAC,CAAC,EAAA,CACjG,KAAK,KAAK,WAAW;IAAE,IAAI,MAAM;IAAI,MAAM,MAAM,KAAK;GAAE,EAAE;EAC5E;EACA,KAAK,OAAO,gBAAgB,UAAU;GACpC,MAAM,WAAW,MAAM,OAAO,IAAI,UAAU,gBAAgB,KAAK,CAAC;GAClE,OAAO,SAAS,OAAO,IAAI,SAAS,KAAK,IAAI;EAC/C;EACA,KAAK,OAAO,gBAAgB,OAAO,SAAS;GAC1C,MAAM,OAAO,IAAI,UAAU,gBAAgB,KAAK,GAAG,IAAI;EACzD;EACA,SAAS,gBAAgB,OAAO,SAC9B,eAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,MAAM,IAAI,UAAU,gBAAgB,KAAK;GAE/C,KAAI,MADmB,YAAY,IAAI,GAAG,EAAA,CAC7B,OAAO,GAAG,OAAO;GAC9B,YAAY,IAAI,KAAK,IAAI;GACzB,OAAO;EACT,CAAC;EAMH,SAAS,gBAAgB,UACvB,eAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,MAAM,IAAI,UAAU,gBAAgB,KAAK;GAE/C,IAAI,EAAC,MADkB,YAAY,IAAI,GAAG,EAAA,CAC5B,OAAO,GAAG,OAAO;GAC/B,YAAY,OAAO,GAAG;GACtB,OAAO;EACT,CAAC;CACL;AACF"}
@@ -4,9 +4,9 @@ const require_dist = require("../../../dist-CsgSfWwR.cjs");
4
4
  const require_root = require("../../../root-rPH6FGDT.cjs");
5
5
  const require_relPath = require("../../../relPath-CTAjGdCL.cjs");
6
6
  const require_utils_fetch = require("../../../utils/fetch.cjs");
7
- const require_discovery = require("../../../discovery-Ck4AqikY.cjs");
7
+ const require_store = require("../../../store-5_P_NsGa.cjs");
8
+ const require_server = require("../../../server-CWZyg8fn.cjs");
8
9
  const require_templatePath = require("../../../templatePath-27vUZowm.cjs");
9
- const require_server = require("../../../server-5EMj3naj.cjs");
10
10
  const require_skill_bridge_index = require("../../../skill-bridge/index.cjs");
11
11
  const require_types = require("../../../types-CNqkLT4M.cjs");
12
12
  let node_fs = require("node:fs");
@@ -49,7 +49,7 @@ function parseEntry(value, index) {
49
49
  * workspace file and delegates here. */
50
50
  function parseRegistriesConfig(raw) {
51
51
  if (!Array.isArray(raw)) {
52
- require_discovery.log.warn("collections-registry", "registries config is not an array — ignoring");
52
+ require_store.log.warn("collections-registry", "registries config is not an array — ignoring");
53
53
  return [];
54
54
  }
55
55
  const seen = /* @__PURE__ */ new Set();
@@ -57,11 +57,11 @@ function parseRegistriesConfig(raw) {
57
57
  for (let i = 0; i < raw.length; i++) {
58
58
  const parsed = parseEntry(raw[i], i);
59
59
  if (typeof parsed === "string") {
60
- require_discovery.log.warn("collections-registry", "registry config entry rejected", { reason: parsed });
60
+ require_store.log.warn("collections-registry", "registry config entry rejected", { reason: parsed });
61
61
  continue;
62
62
  }
63
63
  if (seen.has(parsed.name)) {
64
- require_discovery.log.warn("collections-registry", "registry config duplicate name dropped", { name: parsed.name });
64
+ require_store.log.warn("collections-registry", "registry config duplicate name dropped", { name: parsed.name });
65
65
  continue;
66
66
  }
67
67
  seen.add(parsed.name);
@@ -82,13 +82,13 @@ function readConfigTextOrNull(filePath) {
82
82
  * it is serving. Missing file ⇒ empty list. Malformed JSON or rejected entries
83
83
  * log but never throw. */
84
84
  function loadRegistriesConfig(opts = {}) {
85
- const text = readConfigTextOrNull(require_discovery.collectionsRegistriesConfigPath(opts.workspaceRoot ?? require_discovery.getWorkspaceRoot()));
85
+ const text = readConfigTextOrNull(require_store.collectionsRegistriesConfigPath(opts.workspaceRoot ?? require_store.getWorkspaceRoot()));
86
86
  if (text === null) return [];
87
87
  let parsed;
88
88
  try {
89
89
  parsed = JSON.parse(text);
90
90
  } catch (err) {
91
- require_discovery.log.warn("collections-registry", "registries config is not valid JSON — ignoring", { error: err instanceof Error ? err.message : String(err) });
91
+ require_store.log.warn("collections-registry", "registries config is not valid JSON — ignoring", { error: err instanceof Error ? err.message : String(err) });
92
92
  return [];
93
93
  }
94
94
  return parseRegistriesConfig(parsed);
@@ -130,7 +130,7 @@ async function loadFromNetwork(descriptor) {
130
130
  headers: { accept: "application/json" }
131
131
  });
132
132
  } catch (err) {
133
- require_discovery.log.warn("collections-registry", "index fetch failed", {
133
+ require_store.log.warn("collections-registry", "index fetch failed", {
134
134
  registry: descriptor.name,
135
135
  url: descriptor.indexUrl,
136
136
  error: require_dist.errorMessage(err)
@@ -149,7 +149,7 @@ async function loadFromNetwork(descriptor) {
149
149
  const json = await res.json().catch(() => null);
150
150
  const parsed = require_types.parseRegistryIndex(json, descriptor.name);
151
151
  if (!parsed.ok) {
152
- require_discovery.log.warn("collections-registry", "index invalid", {
152
+ require_store.log.warn("collections-registry", "index invalid", {
153
153
  registry: descriptor.name,
154
154
  url: descriptor.indexUrl,
155
155
  error: parsed.error
@@ -541,9 +541,9 @@ async function findMatchingInstall(skillsDir, registry, entry) {
541
541
  * tree that reads and deletes now deliberately ignore. Update would not find
542
542
  * the provenance, and delete would leave the install behind. */
543
543
  function importInstall(workspaceRoot) {
544
- const staging = require_discovery.skillsStagingDir(workspaceRoot);
544
+ const staging = require_store.skillsStagingDir(workspaceRoot);
545
545
  return staging === null ? {
546
- skillsDir: require_discovery.projectSkillsDir(workspaceRoot),
546
+ skillsDir: require_store.projectSkillsDir(workspaceRoot),
547
547
  mirrors: false
548
548
  } : {
549
549
  skillsDir: staging,
@@ -580,14 +580,14 @@ function validateAndNormalize(bundle, localSlug, workspaceRoot) {
580
580
  } catch {
581
581
  return { error: "schema.json is not valid JSON" };
582
582
  }
583
- const parsed = require_discovery.CollectionSchemaZ.safeParse(parsedJson);
583
+ const parsed = require_store.CollectionSchemaZ.safeParse(parsedJson);
584
584
  if (!parsed.success) return { error: `schema.json failed validation: ${parsed.error.issues[0]?.message ?? "invalid"}` };
585
585
  if (parsed.data.dataSource !== void 0) return { error: "schema.json declares `dataSource` — external-data collections are machine-local and cannot be imported from a registry" };
586
586
  const schema = {
587
587
  ...parsed.data,
588
588
  dataPath: normalizedDataPath(localSlug)
589
589
  };
590
- const acceptance = require_discovery.acceptParsedSchema(schema, {
590
+ const acceptance = require_store.acceptParsedSchema(schema, {
591
591
  source: "project",
592
592
  workspaceRoot,
593
593
  slug: localSlug
@@ -618,8 +618,8 @@ async function materializeSeed(dataDir, bundle) {
618
618
  let written = 0;
619
619
  for (const [rel, content] of seedEntries) {
620
620
  const fileName = rel.slice(11);
621
- if (fileName.includes("/") || require_discovery.safeRecordId(fileName.replace(/\.json$/, "")) === null) {
622
- require_discovery.log.warn("collections-registry", "skipped unsafe seed record", { rel });
621
+ if (fileName.includes("/") || require_store.safeRecordId(fileName.replace(/\.json$/, "")) === null) {
622
+ require_store.log.warn("collections-registry", "skipped unsafe seed record", { rel });
623
623
  continue;
624
624
  }
625
625
  await (0, node_fs_promises.writeFile)(node_path.default.join(dataDir, fileName), content, "utf-8");
@@ -730,7 +730,7 @@ async function writeImportedCollection(params) {
730
730
  try {
731
731
  if (importInstall(workspaceRoot).mirrors) await mirrorToClaudeSkills(workspaceRoot, localSlug, bundle);
732
732
  } catch (err) {
733
- require_discovery.log.warn("collections-registry", "mirror to .claude/skills/ failed (data/skills write succeeded; prior mirror left intact)", {
733
+ require_store.log.warn("collections-registry", "mirror to .claude/skills/ failed (data/skills write succeeded; prior mirror left intact)", {
734
734
  localSlug,
735
735
  error: require_dist.errorMessage(err)
736
736
  });
@@ -778,7 +778,7 @@ async function performImport(author, slug, workspaceRoot, registry = null) {
778
778
  nowIso: (/* @__PURE__ */ new Date()).toISOString()
779
779
  });
780
780
  } catch (err) {
781
- require_discovery.log.warn("collections-registry", "import write failed", {
781
+ require_store.log.warn("collections-registry", "import write failed", {
782
782
  author,
783
783
  slug,
784
784
  registry: entry.registryName,
@@ -923,7 +923,7 @@ async function writeCollectionExport(params) {
923
923
  ...seed.count > 0 ? { dataConsent: true } : {}
924
924
  };
925
925
  await (0, node_fs_promises.writeFile)(node_path.default.join(outDir, "meta.json"), `${JSON.stringify(metaOut, null, 2)}\n`, "utf-8");
926
- require_discovery.log.info("collections-registry", "exported collection", {
926
+ require_store.log.info("collections-registry", "exported collection", {
927
927
  slug: meta.slug,
928
928
  author: meta.author,
929
929
  seedCount: seed.count
@@ -1019,7 +1019,7 @@ async function readJsonObject(file) {
1019
1019
  }
1020
1020
  }
1021
1021
  async function performExport(slug, opts, workspaceRoot) {
1022
- const collection = await require_discovery.loadCollection(slug);
1022
+ const collection = await require_store.loadCollection(slug);
1023
1023
  if (!collection) return {
1024
1024
  ok: false,
1025
1025
  status: STATUS_NOT_FOUND,