@mulmoclaude/core 4.5.0 → 4.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.
@@ -31,27 +31,78 @@ import { z } from "zod";
31
31
  async function readCustomViewHtml(collection, viewFile, opts = {}) {
32
32
  return readSourceAwareFile(collection, viewFile, opts);
33
33
  }
34
- /** Internal helper: read a file using the same source-aware base fallback as
35
- * `readCustomViewHtml`. Used by both `readCustomViewHtml` and
36
- * `readCustomViewI18n` so the two stay in lockstep. */
37
- async function readSourceAwareFile(collection, relPath, opts) {
38
- const safeSlug = safeSlugName(collection.slug);
39
- if (safeSlug === null) return null;
40
- const workspaceRoot = opts.workspaceRoot ?? getWorkspaceRoot();
41
- const staging = collection.source === "project" ? stagingSkillDir(workspaceRoot, safeSlug) : null;
42
- const bases = staging === null ? [collection.skillDir] : [staging, collection.skillDir];
34
+ /** `<staging>/<slug>` when THAT slug is actually staged there, else null.
35
+ *
36
+ * `stagingSkillDir` only says whether the ROOT has a staging tree, and then
37
+ * joins the slug — so on its own it claims every collection in a staged
38
+ * workspace is staged. That is wrong for the two layouts that coexist there:
39
+ * a collection IMPORTED via the discover panel, and one committed directly
40
+ * under `.claude/skills/`, neither of which has a staging copy. A stale
41
+ * `data/skills/<slug>/views/x.html` left beside them then SHADOWED the real
42
+ * view on every read (#3031).
43
+ *
44
+ * The evidence is the slug's own `schema.json`, because that is what the
45
+ * authoring layout actually produces — `putSchema` writes it, and the delete
46
+ * path below has always keyed on it. It must be a REGULAR FILE — `lstat`, not
47
+ * `stat` — so a symlink standing in for the schema is not evidence.
48
+ *
49
+ * Exported so the DELETE path uses this exact function rather than a second
50
+ * copy of the expression: "reads and deletes agree" is a claim `views.ts` has
51
+ * made in its comments since #1836, and sharing the predicate is what makes
52
+ * it structurally true instead of a convention two files have to remember. */
53
+ async function stagedSkillDir(workspaceRoot, safeSlug) {
54
+ const staging = stagingSkillDir(workspaceRoot, safeSlug);
55
+ if (staging === null) return null;
56
+ return await isStagedSchema(path.join(staging, "schema.json")) ? staging : null;
57
+ }
58
+ /** Is the staged `schema.json` there, as a regular file?
59
+ *
60
+ * NOT `isRegularFile` from `io.ts`, which answers `false` for every `lstat`
61
+ * failure. Here "no schema" and "the schema could not be inspected" must not
62
+ * collapse into one answer: the second would quietly downgrade a staged slug
63
+ * to unstaged and serve the OTHER base's copy — the same masking the read loop
64
+ * below refuses, and for the same reason (Sourcery review on #3032). Only the
65
+ * missing-here codes mean "not staged"; anything else propagates. */
66
+ async function isStagedSchema(target) {
67
+ try {
68
+ return (await lstat(target)).isFile();
69
+ } catch (err) {
70
+ if (!isMissingHere(err)) throw err;
71
+ return false;
72
+ }
73
+ }
74
+ /** The two codes that mean "not at this path" rather than "this path failed".
75
+ * A permission denial / disk error must propagate — silently treating it as
76
+ * absent masks a real failure as a stale-from-another-base success or a
77
+ * misleading 404 (CodeRabbit review on #1836). */
78
+ function isMissingHere(err) {
79
+ return isErrorWithCode(err) && (err.code === "ENOENT" || err.code === "ENOTDIR");
80
+ }
81
+ /** Read `relPath` from the first base that holds it, or null when none does.
82
+ * Bases are tried in order and each is containment-checked on its own, so the
83
+ * fallback never widens what a single base would have allowed. */
84
+ async function readFromBases(bases, relPath) {
43
85
  for (const base of bases) {
44
86
  const resolved = resolveTemplatePath(base, relPath);
45
87
  if (resolved === null) continue;
46
88
  try {
47
89
  return await readFile(resolved, "utf-8");
48
90
  } catch (err) {
49
- if (!isErrorWithCode(err)) throw err;
50
- if (err.code !== "ENOENT" && err.code !== "ENOTDIR") throw err;
91
+ if (!isMissingHere(err)) throw err;
51
92
  }
52
93
  }
53
94
  return null;
54
95
  }
96
+ /** Internal helper: read a file using the same source-aware base fallback as
97
+ * `readCustomViewHtml`. Used by both `readCustomViewHtml` and
98
+ * `readCustomViewI18n` so the two stay in lockstep. */
99
+ async function readSourceAwareFile(collection, relPath, opts) {
100
+ const safeSlug = safeSlugName(collection.slug);
101
+ if (safeSlug === null) return null;
102
+ const workspaceRoot = opts.workspaceRoot ?? getWorkspaceRoot();
103
+ const staging = collection.source === "project" ? await stagedSkillDir(workspaceRoot, safeSlug) : null;
104
+ return readFromBases(staging === null ? [collection.skillDir] : [staging, collection.skillDir], relPath);
105
+ }
55
106
  var I18N_FALLBACK_LOCALE = "en";
56
107
  var EMPTY_I18N = {
57
108
  locale: "",
@@ -1265,27 +1316,20 @@ async function deleteCollection(collection, opts = {}) {
1265
1316
  }
1266
1317
  //#endregion
1267
1318
  //#region src/collection/server/views.ts
1268
- async function fileExists(target) {
1269
- try {
1270
- await stat(target);
1271
- return true;
1272
- } catch (err) {
1273
- if (!isErrorWithCode(err)) throw err;
1274
- if (err.code === "ENOENT" || err.code === "ENOTDIR") return false;
1275
- throw err;
1276
- }
1277
- }
1278
1319
  /** The authoritative base dir for a collection's schema.json + view HTML.
1279
1320
  * For a project collection, prefer the staging tree when its schema.json is
1280
1321
  * present (authoring layout); otherwise fall back to the active skill dir
1281
1322
  * (imported layout — staging never materialised). For feed / user, it's
1282
- * always the discovered skillDir. Matches `readCustomViewHtml` so reads and
1283
- * deletes agree on both layouts. */
1323
+ * always the discovered skillDir.
1324
+ *
1325
+ * Matches `readCustomViewHtml` so reads and deletes agree on both layouts —
1326
+ * through the SAME function now (`stagedSkillDir`), not through two copies of
1327
+ * one expression. The read side had drifted off it and was preferring staging
1328
+ * per root rather than per slug (#3031), which is precisely the drift a shared
1329
+ * predicate makes unrepresentable. */
1284
1330
  async function canonicalBase(collection, workspaceRoot, safeSlug) {
1285
1331
  if (collection.source !== "project") return collection.skillDir;
1286
- const staging = stagingSkillDir(workspaceRoot, safeSlug);
1287
- if (staging !== null && await fileExists(path.join(staging, "schema.json"))) return staging;
1288
- return collection.skillDir;
1332
+ return await stagedSkillDir(workspaceRoot, safeSlug) ?? collection.skillDir;
1289
1333
  }
1290
1334
  /** Every on-disk schema.json that must reflect the removal. The active
1291
1335
  * `<skillDir>/schema.json` is the discovery anchor and is always present.
@@ -1295,9 +1339,9 @@ async function canonicalBase(collection, workspaceRoot, safeSlug) {
1295
1339
  async function schemaWriteTargets(collection, workspaceRoot, safeSlug) {
1296
1340
  const active = path.join(collection.skillDir, SCHEMA_FILE$1);
1297
1341
  if (collection.source !== "project") return [active];
1298
- const staging = stagingSkillDir(workspaceRoot, safeSlug);
1342
+ const staging = await stagedSkillDir(workspaceRoot, safeSlug);
1299
1343
  const targets = [];
1300
- if (staging !== null && await fileExists(path.join(staging, "schema.json"))) targets.push(path.join(staging, SCHEMA_FILE$1));
1344
+ if (staging !== null) targets.push(path.join(staging, SCHEMA_FILE$1));
1301
1345
  targets.push(active);
1302
1346
  return targets;
1303
1347
  }
@@ -2385,6 +2429,6 @@ function makeManageCollectionTool(deps = {}) {
2385
2429
  };
2386
2430
  }
2387
2431
  //#endregion
2388
- export { recordFieldProblem as A, applyMutateAction as C, validateCollectionRecords as D, STORE_UNREADABLE as E, buildCollectionActionSeedPrompt as F, promptPathsFor as I, readCustomViewHtml as L, enrichItems as M, runQueryOverRows as N, validateRecordObject as O, buildActionSeedPrompt as P, readCustomViewI18n as R, schemaRelations as S, MAX_RECORD_ISSUES as T, resolveEvery as _, MAX_UNSELECTIVE_ITEMS as a, computeCollectionIcon as b, deleteCollection as c, computeSuccessor as d, daysInMonth as f, parseCivil as g, maybeSpawnSuccessor as h, MAX_SCHEMA_ISSUES as i, runCollectionQuery as j, compileRecordZ as k, deleteCollectionRefusalMessage as l, isTriggerDue as m, MAX_PUT_ITEMS as n, makeManageCollectionTool as o, formatCivil as p, MAX_PUT_LINT as r, deleteCustomView as s, MAX_ITEMS_FILE_BYTES as t, advanceTriggerDate as u, successorId as v, firstMutateParamProblem as w, buildWorkspaceOntology as x, ONE_SECOND_MS as y, readSkillTemplate as z };
2432
+ export { recordFieldProblem as A, stagedSkillDir as B, applyMutateAction as C, validateCollectionRecords as D, STORE_UNREADABLE as E, buildCollectionActionSeedPrompt as F, promptPathsFor as I, readCustomViewHtml as L, enrichItems as M, runQueryOverRows as N, validateRecordObject as O, buildActionSeedPrompt as P, readCustomViewI18n as R, schemaRelations as S, MAX_RECORD_ISSUES as T, resolveEvery as _, MAX_UNSELECTIVE_ITEMS as a, computeCollectionIcon as b, deleteCollection as c, computeSuccessor as d, daysInMonth as f, parseCivil as g, maybeSpawnSuccessor as h, MAX_SCHEMA_ISSUES as i, runCollectionQuery as j, compileRecordZ as k, deleteCollectionRefusalMessage as l, isTriggerDue as m, MAX_PUT_ITEMS as n, makeManageCollectionTool as o, formatCivil as p, MAX_PUT_LINT as r, deleteCustomView as s, MAX_ITEMS_FILE_BYTES as t, advanceTriggerDate as u, successorId as v, firstMutateParamProblem as w, buildWorkspaceOntology as x, ONE_SECOND_MS as y, readSkillTemplate as z };
2389
2433
 
2390
- //# sourceMappingURL=server-BENtFplH.js.map
2434
+ //# sourceMappingURL=server-DyYwfNB6.js.map