@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.
- package/assets/helps/collection-skills.md +49 -1
- package/assets/helps/error-recovery.md +53 -0
- package/dist/calendarGrid-CQ8MVSRb.js.map +1 -1
- package/dist/calendarGrid-DGILaVxI.cjs.map +1 -1
- package/dist/collection/core/schema.d.ts +8 -1
- package/dist/collection/core/schemaZ.d.ts +36 -26
- package/dist/collection/firestore.cjs +51 -0
- package/dist/collection/firestore.cjs.map +1 -0
- package/dist/collection/firestore.d.ts +1 -0
- package/dist/collection/firestore.js +50 -0
- package/dist/collection/firestore.js.map +1 -0
- package/dist/collection/registry/server/index.cjs +19 -19
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/appManifest.d.ts +53 -0
- package/dist/collection/server/delete.d.ts +10 -0
- package/dist/collection/server/discoveredCollection.d.ts +10 -0
- package/dist/collection/server/discovery.d.ts +1 -0
- package/dist/collection/server/firestoreDocs.d.ts +39 -0
- package/dist/collection/server/firestoreStore.d.ts +14 -0
- package/dist/collection/server/host.d.ts +52 -0
- package/dist/collection/server/index.cjs +72 -52
- package/dist/collection/server/index.d.ts +8 -1
- package/dist/collection/server/index.js +3 -3
- package/dist/collection/server/manageTool.d.ts +4 -0
- package/dist/collection/server/publish.d.ts +56 -0
- package/dist/collection/server/publishChecks.d.ts +29 -0
- package/dist/collection/server/publishManifest.d.ts +181 -0
- package/dist/collection/server/publishProject.d.ts +86 -0
- package/dist/collection/server/validate.d.ts +12 -0
- package/dist/collection-watchers/index.cjs +143 -52
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +132 -41
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/reconciler.d.ts +1 -1
- package/dist/feeds/server/index.cjs +10 -10
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/index.cjs +12 -12
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +1 -1
- package/dist/{server-BiRLLMpW.js → server-B48Jyxcj.js} +1100 -230
- package/dist/server-B48Jyxcj.js.map +1 -0
- package/dist/{server-5EMj3naj.cjs → server-CWZyg8fn.cjs} +1333 -385
- package/dist/server-CWZyg8fn.cjs.map +1 -0
- package/dist/{discovery-Ck4AqikY.cjs → store-5_P_NsGa.cjs} +2108 -1755
- package/dist/store-5_P_NsGa.cjs.map +1 -0
- package/dist/{discovery-DH9wweuj.js → store-_61sO8K8.js} +2333 -2022
- package/dist/store-_61sO8K8.js.map +1 -0
- package/dist/whisper/index.cjs +1 -1
- package/dist/whisper/index.js +1 -1
- package/package.json +7 -1
- package/dist/discovery-Ck4AqikY.cjs.map +0 -1
- package/dist/discovery-DH9wweuj.js.map +0 -1
- package/dist/server-5EMj3naj.cjs.map +0 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
544
|
+
const staging = require_store.skillsStagingDir(workspaceRoot);
|
|
545
545
|
return staging === null ? {
|
|
546
|
-
skillsDir:
|
|
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 =
|
|
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 =
|
|
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("/") ||
|
|
622
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1022
|
+
const collection = await require_store.loadCollection(slug);
|
|
1023
1023
|
if (!collection) return {
|
|
1024
1024
|
ok: false,
|
|
1025
1025
|
status: STATUS_NOT_FOUND,
|