@mulmoclaude/core 0.2.9 → 0.2.11

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 (113) hide show
  1. package/dist/collection/core/actionVisible.d.ts +34 -0
  2. package/dist/collection/core/calendarGrid.d.ts +120 -0
  3. package/dist/collection/core/deriveAll.d.ts +38 -0
  4. package/dist/collection/core/derivedFormula.d.ts +18 -0
  5. package/dist/collection/core/draft.d.ts +18 -0
  6. package/dist/collection/core/enumColors.d.ts +33 -0
  7. package/dist/collection/core/errorMessage.d.ts +4 -0
  8. package/dist/collection/core/itemLabel.d.ts +12 -0
  9. package/dist/collection/core/presentCollection.d.ts +13 -0
  10. package/dist/collection/core/promptSafety.d.ts +1 -0
  11. package/dist/collection/core/schema.d.ts +403 -0
  12. package/dist/collection/core/shortHexId.d.ts +8 -0
  13. package/dist/collection/core/sortItems.d.ts +29 -0
  14. package/dist/collection/core/uiTypes.d.ts +106 -0
  15. package/dist/collection/index.cjs +794 -0
  16. package/dist/collection/index.cjs.map +1 -0
  17. package/dist/collection/index.d.ts +14 -0
  18. package/dist/collection/paths.cjs +56 -0
  19. package/dist/collection/paths.cjs.map +1 -0
  20. package/dist/collection/paths.js +12 -1
  21. package/dist/collection/paths.js.map +1 -1
  22. package/dist/collection/server/atomic.d.ts +1 -0
  23. package/dist/collection/server/delete.d.ts +38 -0
  24. package/dist/collection/server/derive.d.ts +8 -0
  25. package/dist/collection/server/discoveredCollection.d.ts +18 -0
  26. package/dist/collection/server/discovery.d.ts +240 -0
  27. package/dist/collection/server/host.d.ts +77 -0
  28. package/dist/collection/server/index.cjs +53 -0
  29. package/dist/collection/server/index.js +3 -3
  30. package/dist/collection/server/io.d.ts +147 -0
  31. package/dist/collection/server/paths.d.ts +52 -0
  32. package/dist/collection/server/spawn.d.ts +55 -0
  33. package/dist/collection/server/templatePath.d.ts +9 -0
  34. package/dist/collection/server/util.d.ts +3 -0
  35. package/dist/collection/server/validate.d.ts +19 -0
  36. package/dist/collection/server/views.d.ts +20 -0
  37. package/dist/collection-watchers/config.d.ts +49 -0
  38. package/dist/collection-watchers/index.cjs +553 -0
  39. package/dist/collection-watchers/index.cjs.map +1 -0
  40. package/dist/collection-watchers/index.d.ts +3 -0
  41. package/dist/collection-watchers/index.js +1 -1
  42. package/dist/collection-watchers/reconciler.d.ts +33 -0
  43. package/dist/collection-watchers/watcher.d.ts +34 -0
  44. package/dist/deriveAll-VRWrs3SF.cjs +413 -0
  45. package/dist/deriveAll-VRWrs3SF.cjs.map +1 -0
  46. package/dist/deriveAll-vzIhhKBK.js.map +1 -1
  47. package/dist/feeds/index.cjs +8 -0
  48. package/dist/feeds/index.d.ts +1 -0
  49. package/dist/feeds/ingestTypes.d.ts +57 -0
  50. package/dist/feeds/paths.cjs +48 -0
  51. package/dist/feeds/paths.cjs.map +1 -0
  52. package/dist/feeds/paths.d.ts +22 -0
  53. package/dist/feeds/server/fetch/httpClient.d.ts +8 -0
  54. package/dist/feeds/server/fetch/rssParser.d.ts +12 -0
  55. package/dist/feeds/server/index.cjs +839 -0
  56. package/dist/feeds/server/index.cjs.map +1 -0
  57. package/dist/feeds/server/index.js +5 -6
  58. package/dist/feeds/server/index.js.map +1 -1
  59. package/dist/feeds/server/pathResolver.d.ts +6 -0
  60. package/dist/feeds/server/projectItem.d.ts +8 -0
  61. package/dist/feeds/server/refreshResult.d.ts +17 -0
  62. package/dist/feeds/server/retrievers/httpJson.d.ts +3 -0
  63. package/dist/feeds/server/retrievers/index.d.ts +12 -0
  64. package/dist/feeds/server/retrievers/registerAll.d.ts +0 -0
  65. package/dist/feeds/server/retrievers/rss.d.ts +3 -0
  66. package/dist/feeds/server/scheduledRefresh.d.ts +1 -1
  67. package/dist/file-change/index.cjs +72 -0
  68. package/dist/file-change/index.cjs.map +1 -0
  69. package/dist/file-change/index.d.ts +43 -0
  70. package/dist/ingestTypes-DDOVhOYC.cjs +32 -0
  71. package/dist/ingestTypes-DDOVhOYC.cjs.map +1 -0
  72. package/dist/notifier/engine.d.ts +72 -0
  73. package/dist/notifier/index.cjs +21 -0
  74. package/dist/notifier/index.d.ts +3 -0
  75. package/dist/notifier/store.d.ts +18 -0
  76. package/dist/notifier/types.d.ts +118 -0
  77. package/dist/notifier/validate.d.ts +17 -0
  78. package/dist/notifier-bS8IEeLA.cjs +577 -0
  79. package/dist/notifier-bS8IEeLA.cjs.map +1 -0
  80. package/dist/rolldown-runtime-D6vf50IK.cjs +28 -0
  81. package/dist/scheduler/adapter.d.ts +48 -0
  82. package/dist/scheduler/index.cjs +352 -0
  83. package/dist/scheduler/index.cjs.map +1 -0
  84. package/dist/scheduler/index.d.ts +2 -0
  85. package/dist/scheduler/task-manager.d.ts +51 -0
  86. package/dist/{server-q4WPXtbU.js → server-D2QZuMS2.js} +54 -4
  87. package/dist/server-D2QZuMS2.js.map +1 -0
  88. package/dist/server-DAyIQ5rT.cjs +2049 -0
  89. package/dist/server-DAyIQ5rT.cjs.map +1 -0
  90. package/dist/skill-bridge/index.cjs +88 -0
  91. package/dist/skill-bridge/index.cjs.map +1 -0
  92. package/dist/skill-bridge/index.d.ts +30 -0
  93. package/dist/whisper/client.cjs +241 -0
  94. package/dist/whisper/client.cjs.map +1 -0
  95. package/dist/whisper/client.d.ts +35 -0
  96. package/dist/whisper/ffmpeg.d.ts +6 -0
  97. package/dist/whisper/index.cjs +433 -0
  98. package/dist/whisper/index.cjs.map +1 -0
  99. package/dist/whisper/index.d.ts +5 -0
  100. package/dist/whisper/internal.d.ts +11 -0
  101. package/dist/whisper/models.d.ts +49 -0
  102. package/dist/whisper/sidecar.d.ts +8 -0
  103. package/dist/whisper/whisper.d.ts +28 -0
  104. package/dist/workspace-setup/assets.d.ts +10 -0
  105. package/dist/workspace-setup/index.d.ts +3 -0
  106. package/dist/workspace-setup/index.js +560 -0
  107. package/dist/workspace-setup/index.js.map +1 -0
  108. package/dist/workspace-setup/slug.d.ts +6 -0
  109. package/dist/workspace-setup/slug.js +13 -0
  110. package/dist/workspace-setup/slug.js.map +1 -0
  111. package/dist/workspace-setup/sync.d.ts +94 -0
  112. package/package.json +1 -1
  113. package/dist/server-q4WPXtbU.js.map +0 -1
@@ -0,0 +1,77 @@
1
+ /** Logger shape the engine logs through — matches the host `Logger`
2
+ * (prefix, message, optional structured data). */
3
+ export interface CollectionLogger {
4
+ error: (prefix: string, message: string, data?: Record<string, unknown>) => void;
5
+ warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;
6
+ info: (prefix: string, message: string, data?: Record<string, unknown>) => void;
7
+ debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;
8
+ }
9
+ export interface CollectionHost {
10
+ /** Absolute path to the host workspace root (e.g. `~/mulmoclaude`). The
11
+ * default root for every path/containment check that isn't given an
12
+ * explicit override. */
13
+ workspaceRoot: string;
14
+ /** Host logger; the engine logs under the `"collections"` prefix. */
15
+ log: CollectionLogger;
16
+ /** Host workspace layout — supplied as the host's own path helpers so the
17
+ * package owns no layout literals and works against a test/alt root. */
18
+ paths: {
19
+ /** Absolute user-scope skills dir (host-specific, e.g. `~/.claude/skills`). */
20
+ userSkillsDir: string;
21
+ /** Absolute project-scope skills dir for a workspace (`<root>/.claude/skills`). */
22
+ projectSkillsDir: (workspaceRoot: string) => string;
23
+ /** Absolute feeds-registry root for a workspace (`<root>/data/feeds`). */
24
+ feedsRoot: (workspaceRoot: string) => string;
25
+ /** Absolute project-skills *staging* dir for a workspace (`<root>/data/skills`). */
26
+ skillsStagingDir: (workspaceRoot: string) => string;
27
+ /** Workspace-relative archive dir (a removed collection's files move here). */
28
+ archiveDir: string;
29
+ };
30
+ /** True for a preset-skill slug (host-owned naming convention). */
31
+ isPresetSlug: (slug: string) => boolean;
32
+ }
33
+ /** A collection's records changed on disk. Carries the `slug` so the host can
34
+ * publish on a per-collection channel; `ids` lists the affected record ids
35
+ * when known (a consumer may ignore them and refetch the whole collection),
36
+ * and `op` is advisory. Deliberately carries NO record bodies — this is a
37
+ * "refetch" ping, not a data feed, so it stays cheap and leaks nothing when a
38
+ * host relays it into an opaque-origin custom-view iframe. */
39
+ export interface CollectionChangePayload {
40
+ slug: string;
41
+ ids?: string[];
42
+ op?: "upsert" | "delete";
43
+ }
44
+ type CollectionChangePublisher = (payload: CollectionChangePayload) => void;
45
+ /** Wire the engine to a host. Call once at server startup, before any
46
+ * collection storage operation. Re-binding to a *different* host throws —
47
+ * silently redirecting later filesystem operations to another workspace
48
+ * would be a bug, not a feature. Re-calling with the same host is a no-op. */
49
+ export declare function configureCollectionHost(host: CollectionHost): void;
50
+ /** Wire a publisher that broadcasts record-change events; the host bridges it
51
+ * to its pubsub. Kept SEPARATE from `configureCollectionHost` because the
52
+ * host's pubsub instance isn't ready at host-binding time (the binding is set
53
+ * at the top of server startup, the pubsub later). Optional: left unset, every
54
+ * write is silent — the default for tests and for a host that doesn't want
55
+ * live view updates. Pass `null` to detach (test teardown). */
56
+ export declare function setCollectionChangePublisher(publish: CollectionChangePublisher | null): void;
57
+ /** Broadcast a record-change event if a publisher is wired (no-op otherwise).
58
+ * Called from the write path (`writeItem`/`deleteItem`). The wired publisher is
59
+ * expected to be fire-and-forget (it wraps its own pubsub call in try/catch),
60
+ * so this stays a thin pass-through and never throws into the write. */
61
+ export declare function publishCollectionChange(payload: CollectionChangePayload): void;
62
+ /** The configured workspace root. Throws if the host never configured one. */
63
+ export declare function getWorkspaceRoot(): string;
64
+ export declare function userSkillsDir(): string;
65
+ export declare function projectSkillsDir(workspaceRoot: string): string;
66
+ export declare function feedsRoot(workspaceRoot: string): string;
67
+ export declare function skillsStagingDir(workspaceRoot: string): string;
68
+ export declare function archiveDir(): string;
69
+ export declare function isPresetSlug(slug: string): boolean;
70
+ /** Logger proxy so engine modules can `import { log }` and use it exactly like
71
+ * the host logger — each call forwards to the live host binding. Logging is
72
+ * non-critical, so calls before the host configures a binding (e.g. unit tests
73
+ * that exercise pure logic) are dropped rather than throwing — unlike
74
+ * `getWorkspaceRoot()`, which fails loudly because the engine cannot operate
75
+ * without a workspace root. */
76
+ export declare const log: CollectionLogger;
77
+ export {};
@@ -0,0 +1,53 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_server = require("../../server-DAyIQ5rT.cjs");
3
+ const require_collection_paths = require("../paths.cjs");
4
+ exports.COMPUTED_TYPES = require_server.COMPUTED_TYPES;
5
+ exports.CollectionSchemaZ = require_server.CollectionSchemaZ;
6
+ exports.SCHEMA_FILE = require_server.SCHEMA_FILE;
7
+ exports.acceptParsedSchema = require_server.acceptParsedSchema;
8
+ exports.advanceTriggerDate = require_server.advanceTriggerDate;
9
+ exports.buildActionSeedPrompt = require_server.buildActionSeedPrompt;
10
+ exports.buildCollectionActionSeedPrompt = require_server.buildCollectionActionSeedPrompt;
11
+ exports.computeSuccessor = require_server.computeSuccessor;
12
+ exports.configureCollectionHost = require_server.configureCollectionHost;
13
+ exports.daysInMonth = require_server.daysInMonth;
14
+ exports.deleteCollection = require_server.deleteCollection;
15
+ exports.deleteCollectionRefusalMessage = require_server.deleteCollectionRefusalMessage;
16
+ exports.deleteCustomView = require_server.deleteCustomView;
17
+ exports.deleteItem = require_server.deleteItem;
18
+ exports.discoverCollections = require_server.discoverCollections;
19
+ exports.enrichItems = require_server.enrichItems;
20
+ exports.formatCivil = require_server.formatCivil;
21
+ exports.generateItemId = require_server.generateItemId;
22
+ exports.getWorkspaceRoot = require_server.getWorkspaceRoot;
23
+ exports.isContainedInRoot = require_server.isContainedInRoot;
24
+ exports.isContainedInWorkspace = require_server.isContainedInWorkspace;
25
+ exports.isSafeActionTemplatePath = require_collection_paths.isSafeActionTemplatePath;
26
+ exports.isSafeCustomViewI18nPath = require_collection_paths.isSafeCustomViewI18nPath;
27
+ exports.isSafeCustomViewPath = require_collection_paths.isSafeCustomViewPath;
28
+ exports.isSafeTemplatePath = require_collection_paths.isSafeTemplatePath;
29
+ exports.isTriggerDue = require_server.isTriggerDue;
30
+ exports.itemFilePath = require_server.itemFilePath;
31
+ exports.listItems = require_server.listItems;
32
+ exports.loadCollection = require_server.loadCollection;
33
+ exports.log = require_server.log;
34
+ exports.maybeSpawnSuccessor = require_server.maybeSpawnSuccessor;
35
+ exports.parseCivil = require_server.parseCivil;
36
+ exports.publishCollectionChange = require_server.publishCollectionChange;
37
+ exports.readCustomViewHtml = require_server.readCustomViewHtml;
38
+ exports.readCustomViewI18n = require_server.readCustomViewI18n;
39
+ exports.readItem = require_server.readItem;
40
+ exports.readSkillTemplate = require_server.readSkillTemplate;
41
+ exports.resolveCreateItemId = require_server.resolveCreateItemId;
42
+ exports.resolveDataDir = require_server.resolveDataDir;
43
+ exports.resolveEvery = require_server.resolveEvery;
44
+ exports.resolveTemplatePath = require_server.resolveTemplatePath;
45
+ exports.safeRecordId = require_server.safeRecordId;
46
+ exports.safeSlugName = require_server.safeSlugName;
47
+ exports.setCollectionChangePublisher = require_server.setCollectionChangePublisher;
48
+ exports.successorId = require_server.successorId;
49
+ exports.toDetail = require_server.toDetail;
50
+ exports.toSummary = require_server.toSummary;
51
+ exports.validateCollectionRecords = require_server.validateCollectionRecords;
52
+ exports.validateRecordObject = require_server.validateRecordObject;
53
+ exports.writeItem = require_server.writeItem;
@@ -1,3 +1,3 @@
1
- import { A as readSkillTemplate, B as safeSlugName, C as buildActionSeedPrompt, D as listItems, E as generateItemId, F as isContainedInWorkspace, G as setCollectionChangePublisher, H as getWorkspaceRoot, I as itemFilePath, L as resolveDataDir, M as writeItem, N as SCHEMA_FILE, O as readCustomViewHtml, P as isContainedInRoot, R as resolveTemplatePath, S as validateRecordObject, T as deleteItem, U as log, V as configureCollectionHost, W as publishCollectionChange, _ as loadCollection, a as computeSuccessor, b as COMPUTED_TYPES, c as isTriggerDue, d as resolveEvery, f as successorId, g as discoverCollections, h as acceptParsedSchema, i as advanceTriggerDate, j as resolveCreateItemId, k as readItem, l as maybeSpawnSuccessor, m as CollectionSchemaZ, n as deleteCollection, o as daysInMonth, p as enrichItems, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as toDetail, w as buildCollectionActionSeedPrompt, x as validateCollectionRecords, y as toSummary, z as safeRecordId } from "../../server-q4WPXtbU.js";
2
- import { isSafeActionTemplatePath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
3
- export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, buildActionSeedPrompt, buildCollectionActionSeedPrompt, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isSafeActionTemplatePath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, publishCollectionChange, readCustomViewHtml, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
1
+ import { A as readItem, B as safeRecordId, C as buildActionSeedPrompt, D as listItems, E as generateItemId, F as isContainedInRoot, G as publishCollectionChange, H as configureCollectionHost, I as isContainedInWorkspace, K as setCollectionChangePublisher, L as itemFilePath, M as resolveCreateItemId, N as writeItem, O as readCustomViewHtml, P as SCHEMA_FILE, R as resolveDataDir, S as validateRecordObject, T as deleteItem, U as getWorkspaceRoot, V as safeSlugName, W as log, _ as loadCollection, a as computeSuccessor, b as COMPUTED_TYPES, c as isTriggerDue, d as resolveEvery, f as successorId, g as discoverCollections, h as acceptParsedSchema, i as advanceTriggerDate, j as readSkillTemplate, k as readCustomViewI18n, l as maybeSpawnSuccessor, m as CollectionSchemaZ, n as deleteCollection, o as daysInMonth, p as enrichItems, r as deleteCollectionRefusalMessage, s as formatCivil, t as deleteCustomView, u as parseCivil, v as toDetail, w as buildCollectionActionSeedPrompt, x as validateCollectionRecords, y as toSummary, z as resolveTemplatePath } from "../../server-D2QZuMS2.js";
2
+ import { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
3
+ export { COMPUTED_TYPES, CollectionSchemaZ, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, buildActionSeedPrompt, buildCollectionActionSeedPrompt, computeSuccessor, configureCollectionHost, daysInMonth, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, enrichItems, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, maybeSpawnSuccessor, parseCivil, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readSkillTemplate, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, setCollectionChangePublisher, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
@@ -0,0 +1,147 @@
1
+ import { CollectionItem, CollectionSchema } from '../core/schema';
2
+ import { LoadedCollection } from './discoveredCollection';
3
+ export interface IoOptions {
4
+ /** Override the workspace root for containment checks. Default:
5
+ * the live `workspacePath`. Tests point this at a `mkdtempSync`
6
+ * tree so the realpath-based escape detection can be exercised
7
+ * without touching `~/mulmoclaude/`. Same pattern as
8
+ * `server/workspace/skills/catalog.ts#CatalogOptions`. */
9
+ workspaceRoot?: string;
10
+ /** Collection slug this write/delete belongs to. When provided, a
11
+ * successful write/delete publishes a record-change event (see
12
+ * `publishCollectionChange`) so live views refetch. `writeItem` has no
13
+ * slug of its own (it's keyed by `dataDir`), so callers thread it through;
14
+ * omitting it just means no event is published (internal / test writes). */
15
+ slug?: string;
16
+ }
17
+ /** Read every record under `dataDir`. Returns [] if the dir doesn't
18
+ * exist yet (legitimate first-use state). Malformed JSON files and
19
+ * symlinked records are skipped (the latter is a file-disclosure
20
+ * defense — see `isRegularFile`). Re-validates the realpath
21
+ * containment to defend against a symlinked data dir appearing
22
+ * between discovery and use. */
23
+ export declare function listItems(dataDir: string, opts?: IoOptions): Promise<CollectionItem[]>;
24
+ /** Read one record by id. Returns null when the file is missing,
25
+ * when the resolved path escapes the workspace via a symlink, or
26
+ * when the record file itself is a symlink (file-disclosure
27
+ * defense — see `isRegularFile`). */
28
+ export declare function readItem(dataDir: string, itemId: string, opts?: IoOptions): Promise<CollectionItem | null>;
29
+ export interface WriteItemOptions extends IoOptions {
30
+ /** When true (POST/create), refuse to overwrite an existing file
31
+ * and return `kind: "conflict"`. Update flow (PUT) leaves it false. */
32
+ refuseOverwrite?: boolean;
33
+ }
34
+ export type WriteItemResult = {
35
+ kind: "ok";
36
+ itemId: string;
37
+ item: CollectionItem;
38
+ } | {
39
+ kind: "invalid-id";
40
+ itemId: string;
41
+ } | {
42
+ kind: "conflict";
43
+ itemId: string;
44
+ } | {
45
+ kind: "path-escape";
46
+ itemId: string;
47
+ };
48
+ /** Write a record. Ensures the directory exists, validates the id,
49
+ * re-checks symlink containment after mkdir, and writes atomically.
50
+ *
51
+ * Create path (`refuseOverwrite: true`) uses an O_EXCL `wx` open
52
+ * rather than `stat` + `writeFileAtomic` to close a check-then-write
53
+ * race: two concurrent POSTs would otherwise both pass the existence
54
+ * check and one would silently overwrite the other. The trade-off
55
+ * is that the create path is not crash-atomic (a partial file could
56
+ * remain if the process dies mid-write); acceptable here because
57
+ * records are small JSON blobs and the next read either parses or
58
+ * is skipped via the "malformed JSON" branch in `listItems`.
59
+ *
60
+ * Update path (`refuseOverwrite: false`) uses `writeFileAtomic` so
61
+ * PUT remains crash-atomic. No race there — the URL pins the id. */
62
+ export declare function writeItem(dataDir: string, itemId: string, item: CollectionItem, opts?: WriteItemOptions): Promise<WriteItemResult>;
63
+ export type DeleteItemResult = {
64
+ kind: "ok";
65
+ itemId: string;
66
+ } | {
67
+ kind: "invalid-id";
68
+ itemId: string;
69
+ } | {
70
+ kind: "not-found";
71
+ itemId: string;
72
+ } | {
73
+ kind: "path-escape";
74
+ itemId: string;
75
+ };
76
+ export declare function deleteItem(dataDir: string, itemId: string, opts?: IoOptions): Promise<DeleteItemResult>;
77
+ /** Generate a short random hex id. Used by POST when the form doesn't
78
+ * carry a primary-key value (UI shortcut — Claude normally derives a
79
+ * semantic id from the record's name). */
80
+ export declare function generateItemId(): string;
81
+ /** The shape `readSourceAwareFile` (and its public callers
82
+ * `readCustomViewHtml` / `readCustomViewI18n`) need from a loaded collection:
83
+ * the slug for the safe-name check, the source to pick the base set, and the
84
+ * discovered skill dir for the imported-layout fallback. Kept as a named
85
+ * alias so the three signatures stay in lockstep (and so sonarjs stops
86
+ * flagging the inline `Pick` union as duplication). */
87
+ type SourceAwareReadTarget = Pick<LoadedCollection, "slug" | "source" | "skillDir">;
88
+ /** Read a collection's custom-view HTML, path-safely. `viewFile` is a
89
+ * schema-validated `views/*.html` path, resolved with realpath containment.
90
+ * Returns the HTML, or null when the path is unsafe or the file is missing.
91
+ *
92
+ * The base dir is source-aware. A **project** collection AUTHORED in-place
93
+ * keeps its views in the `data/skills/<slug>/` staging dir (host-side
94
+ * rendering; see plans/done/feat-collections-custom-views.md). A **project**
95
+ * collection that was IMPORTED via the discover panel (rename-on-conflict)
96
+ * carries its views inside `.claude/skills/<slug>/views/` — the skill folder
97
+ * itself — without a staging-dir mirror; reading only the staging path would
98
+ * 404 a perfectly valid imported view. We try staging first, then fall back
99
+ * to the discovered `skillDir`, so both layouts read cleanly. A **user** /
100
+ * **feed** collection is always authored in its own discovered `skillDir`,
101
+ * so it only needs the single lookup. `resolveTemplatePath` does the
102
+ * containment / `..` defense per base, so the fallback never broadens the
103
+ * attack surface. */
104
+ export declare function readCustomViewHtml(collection: SourceAwareReadTarget, viewFile: string, opts?: IoOptions): Promise<string | null>;
105
+ export interface CustomViewI18nResult {
106
+ /** The locale the returned `dict` is keyed to — equals the requested locale
107
+ * when available, else the `"en"` fallback, else `""` when neither block is
108
+ * present (empty `dict`). The host echoes this back to the iframe so
109
+ * `__MC_VIEW.locale` reflects what the view actually got. */
110
+ locale: string;
111
+ /** Flat key → string map for the picked locale. Empty when the file is
112
+ * absent, malformed, or has no usable locale block. Non-string values in a
113
+ * locale block are dropped — `__MC_VIEW.dict` is contract-flat. */
114
+ dict: Record<string, string>;
115
+ }
116
+ /** Read a custom view's translation dictionary and return only the strings
117
+ * for the requested locale (or the `"en"` fallback, or empty). Same
118
+ * source-aware fallback as `readCustomViewHtml` so imported and authored
119
+ * project collections both work. The on-disk file is `{ <locale>: { <key>:
120
+ * <string> } }`; the host never streams other locales' strings to the view.
121
+ * Malformed JSON / unknown shape yields an empty dict — an i18n-less view
122
+ * keeps working unchanged (`__MC_VIEW.t(key)` falls back to the key). */
123
+ export declare function readCustomViewI18n(collection: SourceAwareReadTarget, i18nFile: string, locale: string, opts?: IoOptions): Promise<CustomViewI18nResult>;
124
+ /** The item id a CREATE should use for `schema`, or null when the
125
+ * caller should generate one. A singleton collection pins every
126
+ * create to its fixed `schema.singleton` id, so the "at most one
127
+ * record" contract is enforced server-side (a second create targets
128
+ * the same file and hits `writeItem`'s refuseOverwrite conflict) —
129
+ * not only in the UI. Otherwise the record's own primaryKey value
130
+ * wins, falling back to a generated id (null = "generate"). */
131
+ export declare function resolveCreateItemId(schema: CollectionSchema, record: CollectionItem): string | null;
132
+ /** Read an action's template file from `skillDir`, path-safely. Returns
133
+ * the file contents, or null when the path escapes the skill dir, the
134
+ * resolved target isn't a regular file, or the read fails. */
135
+ export declare function readSkillTemplate(skillDir: string, templateRelPath: string): Promise<string | null>;
136
+ /** Build the seed prompt for a `kind: "chat"` action: a security-
137
+ * boundary instruction + the record as a sanitized JSON data block +
138
+ * the template text verbatim. Pure + exported for tests. Domain-free —
139
+ * the template (skill-owned) carries every specific instruction; the
140
+ * host only injects the record's own data. */
141
+ export declare function buildActionSeedPrompt(record: CollectionItem, templateText: string): string;
142
+ /** Build the seed prompt for a collection-level `kind: "chat"` action: a
143
+ * security-boundary instruction + a compact progress summary of every
144
+ * record (see `progressSummary`) + the template verbatim. Pure +
145
+ * exported for tests. Domain-free — the template carries the specifics. */
146
+ export declare function buildCollectionActionSeedPrompt(items: CollectionItem[], schema: CollectionSchema, templateText: string): string;
147
+ export {};
@@ -0,0 +1,52 @@
1
+ export declare const SCHEMA_FILE = "schema.json";
2
+ /** Sanitise a user-supplied slug into a safe directory-name leaf.
3
+ * Returns null for anything that fails the slug whitelist OR isn't a
4
+ * basename (i.e. survives `path.basename` round-trip unchanged).
5
+ * The basename round-trip is the pattern CodeQL recognises as a
6
+ * `js/path-injection` sanitiser. */
7
+ export declare function safeSlugName(slug: string): string | null;
8
+ /** Sanitise a user-supplied record id into a safe filename stem. Like
9
+ * `safeSlugName` but tolerates interior dots (so natural keys work),
10
+ * while still rejecting any `..` substring, path separators, and
11
+ * leading/trailing dots. The `path.basename` round-trip is the same
12
+ * `js/path-injection` sanitiser CodeQL recognises on `safeSlugName`. */
13
+ export declare function safeRecordId(recordId: string): string | null;
14
+ /** True iff the realpath'd closest existing ancestor of `absPath`
15
+ * resolves under `rootPath`'s realpath. Pure helper, takes both
16
+ * paths explicitly so tests can drive it against a `mkdtempSync`
17
+ * root without touching the user's workspace. Defends against the
18
+ * data dir or any ancestor being a symlink to a directory outside
19
+ * the workspace — lexical-only checks (`path.resolve` + prefix
20
+ * match) would miss this case, which is the class of bug the rest
21
+ * of this codebase uses realpath-based containment to avoid (see
22
+ * `server/utils/files/safe.ts#resolveWithinRoot`). */
23
+ export declare function isContainedInRoot(absPath: string, rootPath: string): boolean;
24
+ /** Workspace-bound convenience over `isContainedInRoot`. Production
25
+ * callers use this; the tests exercise the pure helper. */
26
+ export declare function isContainedInWorkspace(absPath: string): boolean;
27
+ /** Resolve a schema-declared dataPath against `rootPath` (default:
28
+ * the live workspace), refusing anything that escapes — absolute
29
+ * paths, `..`-segments, empty string, or symlinks pointing outside
30
+ * the root. Returns the absolute path on success, null on refusal.
31
+ * Does NOT require the directory to exist; the caller may create it
32
+ * on first write. The realpath containment check covers the symlink
33
+ * case at discovery time; io operations re-check before each write
34
+ * to defend against symlinks introduced between discovery and use.
35
+ *
36
+ * `rootPath` exists as an optional override so a test (or a tool
37
+ * driving discovery against a `mkdtempSync` tree) gets a dataDir
38
+ * rooted at the same place it asked to scan, not the real workspace.
39
+ * Without this, `discoverApps({ workspaceRoot: tmpdir })` would
40
+ * discover skills in tmpdir but resolve every app's dataDir against
41
+ * `~/mulmoclaude/`, breaking isolation. */
42
+ export declare function resolveDataDir(dataPath: string, rootPath?: string): string | null;
43
+ /** Compose the absolute path to a single record file. Both arguments
44
+ * must have been passed through `safeSlugName` / `resolveDataDir`
45
+ * before reaching here so the join can't escape. */
46
+ export declare function itemFilePath(dataDir: string, itemId: string): string;
47
+ /** Resolve an action's skill-relative `template` path against
48
+ * `skillDir`, refusing escapes — absolute paths, `..`-segments, or a
49
+ * symlink pointing outside the skill dir. Mirrors `resolveDataDir`;
50
+ * the realpath containment is the hard guarantee. Returns the
51
+ * absolute path on success, null on refusal. */
52
+ export declare function resolveTemplatePath(skillDir: string, templateRelPath: string): string | null;
@@ -0,0 +1,55 @@
1
+ import { IoOptions } from './io';
2
+ import { CollectionEvery, CollectionItem, CollectionSchema, CollectionSpawnEvery } from '../core/schema';
3
+ /** A timezone-free calendar date. `m` is 1-12. */
4
+ export interface CivilDate {
5
+ y: number;
6
+ m: number;
7
+ d: number;
8
+ }
9
+ /** Days in `month` (1-12) of `year`, leap-year-aware. `new Date(y, m, 0)`
10
+ * is day 0 of the *next* month = the last day of `month`; `.getDate()`
11
+ * reads the civil day regardless of timezone. */
12
+ export declare function daysInMonth(year: number, month: number): number;
13
+ /** Parse a `YYYY-MM-DD` string into a CivilDate, or null when the value
14
+ * isn't a well-formed in-range calendar date. */
15
+ export declare function parseCivil(raw: unknown): CivilDate | null;
16
+ /** `YYYY-MM-DD` for storage in a `date` field. */
17
+ export declare function formatCivil(date: CivilDate): string;
18
+ /** Advance a civil date by one `every` step. Month/year units preserve
19
+ * the rule's day-of-month anchor, clamped to the target month's length
20
+ * (no drift); day/week units do civil day arithmetic. */
21
+ export declare function advanceTriggerDate(source: CivilDate, every: CollectionEvery): CivilDate;
22
+ /** True iff `now`'s civil date (local timezone) has reached the fire date
23
+ * for `triggerRaw` — i.e. the trigger date minus `leadDays` (so a 10-day
24
+ * lead fires 10 days early). Returns null when `triggerRaw` isn't a
25
+ * parseable date — callers treat that as "don't fire" and warn. */
26
+ export declare function isTriggerDue(triggerRaw: unknown, now: Date, leadDays?: number): boolean | null;
27
+ /** Deterministic successor id: `<stem>-<YYYYMMDD>`, where `<stem>` is the
28
+ * source id with a trailing `-YYYYMMDD` stripped if present. So a chain
29
+ * shares one stem and each instance is dated:
30
+ * `rent` → `rent-20260610`
31
+ * `rent-20260610` → `rent-20260710`
32
+ * Slug-safe (alphanumeric + hyphen) and a pure function of the inputs,
33
+ * which is what makes create-if-absent idempotent. */
34
+ export declare function successorId(sourceId: string, next: CivilDate): string;
35
+ /** Resolve the literal `every` that applies to `sourceItem`. Literal-arm
36
+ * `spawn.every` passes through unchanged. Field-driven `spawn.every` reads
37
+ * `sourceItem[fromField]` and looks it up in `map`; an empty field or a
38
+ * value with no map entry yields null (caller skips + logs — see plan §5).
39
+ * Discovery rejects a map that doesn't cover the enum's values, so null
40
+ * here means a record that predates a map/enum edit. */
41
+ export declare function resolveEvery(every: CollectionSpawnEvery, sourceItem: CollectionItem): CollectionEvery | null;
42
+ export interface ComputedSuccessor {
43
+ id: string;
44
+ record: CollectionItem;
45
+ }
46
+ /** Build the successor record purely from (schema, source record, source
47
+ * id). Returns null when the schema has no spawn/triggerField or the
48
+ * source's trigger date is unparseable. */
49
+ export declare function computeSuccessor(schema: CollectionSchema, sourceItem: CollectionItem, sourceId: string): ComputedSuccessor | null;
50
+ /** Idempotently create the successor for `sourceItem` when it matches the
51
+ * spawn predicate. No-op when the schema declares no spawn, the
52
+ * predicate doesn't match, the trigger date is unparseable, or the
53
+ * successor already exists (create-if-absent). Never overwrites an
54
+ * existing successor — protects any edits the user made to it. */
55
+ export declare function maybeSpawnSuccessor(slug: string, schema: CollectionSchema, dataDir: string, sourceItem: CollectionItem, sourceId: string, ioOpts?: IoOptions): Promise<void>;
@@ -23,3 +23,12 @@ export declare function isSafeActionTemplatePath(value: string): boolean;
23
23
  * realpath containment is the hard guarantee.
24
24
  */
25
25
  export declare function isSafeCustomViewPath(value: string): boolean;
26
+ /**
27
+ * A custom-view `i18n` value: the JSON dictionary file authors ship next to
28
+ * their `views/<id>.html`. Constrained to `views/<name>.i18n.json` so the
29
+ * field can only point at a translation file co-located with the view it
30
+ * translates — the same `views/` containment the HTML reader uses. The
31
+ * `.i18n.json` suffix (vs plain `.json`) makes the file's purpose grep-able
32
+ * for both authors and tooling.
33
+ */
34
+ export declare function isSafeCustomViewI18nPath(value: string): boolean;
@@ -0,0 +1,3 @@
1
+ /** Human-readable message from an unknown thrown value. */
2
+ export declare function errorMessage(err: unknown): string;
3
+ export declare const ONE_DAY_MS: number;
@@ -0,0 +1,19 @@
1
+ import { LoadedCollection } from './discoveredCollection';
2
+ import { CollectionItem, CollectionSchema } from '../core/schema';
3
+ export interface RecordIssue {
4
+ /** Record filename, e.g. `lesson-003.json`. */
5
+ file: string;
6
+ /** Human-readable problem, written to be actionable by the LLM. */
7
+ problem: string;
8
+ }
9
+ export declare const COMPUTED_TYPES: ReadonlySet<string>;
10
+ export declare function validateCollectionRecords(collection: LoadedCollection, opts?: {
11
+ workspaceRoot?: string;
12
+ }): Promise<RecordIssue[]>;
13
+ /** First schema problem on an in-memory record (primaryKey↔id mismatch,
14
+ * missing required, bad enum value), or null when it's fine. One issue
15
+ * per record keeps the report short and the fix obvious. Pure +
16
+ * exported so write paths (manageCollection putItems) can gate on the
17
+ * SAME rules the post-hoc file scan reports — `itemId` is the id the
18
+ * record is (or would be) stored under. */
19
+ export declare function validateRecordObject(record: CollectionItem, itemId: string, schema: CollectionSchema): string | null;
@@ -0,0 +1,20 @@
1
+ import { IoOptions } from './io';
2
+ import { LoadedCollection } from './discoveredCollection';
3
+ export type DeleteViewResult = {
4
+ kind: "ok";
5
+ viewId: string;
6
+ } | {
7
+ kind: "not-found";
8
+ viewId: string;
9
+ } | {
10
+ kind: "user-scope";
11
+ } | {
12
+ kind: "preset";
13
+ } | {
14
+ kind: "unsafe-path";
15
+ viewId: string;
16
+ };
17
+ /** Delete one custom view from `collection`: unlink its HTML file and drop it
18
+ * from every schema.json copy. User-scope and preset (mc-*) collections are
19
+ * refused (read-only / re-seeded on boot), consistent with `deleteCollection`. */
20
+ export declare function deleteCustomView(collection: LoadedCollection, viewId: string, opts?: IoOptions): Promise<DeleteViewResult>;
@@ -0,0 +1,49 @@
1
+ import { NotifierSeverity } from '../notifier';
2
+ /** Two-level urgency a pending record can carry, derived from the
3
+ * schema's `notifyWhen` spec. The host maps this onto its own severity
4
+ * scale via `priorityToSeverity`. */
5
+ export type CompletionPriority = "normal" | "high";
6
+ export interface CollectionWatcherLogger {
7
+ info: (message: string, data?: Record<string, unknown>) => void;
8
+ warn: (message: string, data?: Record<string, unknown>) => void;
9
+ }
10
+ /** The host-specific notification surface the reconciler binds to. The
11
+ * reconciler owns the internal `legacyId` key (it encodes slug+itemId
12
+ * and round-trips it through `pluginData`); the adapter only wraps /
13
+ * unwraps it into whatever shape the host's bell expects. */
14
+ export interface CollectionNotificationAdapter {
15
+ /** Plugin namespace these bell entries publish under (MulmoClaude: "todo"). */
16
+ pluginPkg: string;
17
+ /** Map a record's completion priority onto the host's bell severity. */
18
+ priorityToSeverity: (priority: CompletionPriority) => NotifierSeverity;
19
+ /** Build the in-app deep-link the bell row routes to on click. */
20
+ buildNavigateTarget: (slug: string, itemId: string) => string;
21
+ /** Wrap the reconciler's internal key + priority into the host's
22
+ * `pluginData` shape. Stored verbatim on the entry; recovered via
23
+ * `readEntry`. */
24
+ buildPluginData: (input: {
25
+ legacyId: string;
26
+ slug: string;
27
+ itemId: string;
28
+ priority: CompletionPriority;
29
+ navigateTarget: string;
30
+ }) => unknown;
31
+ /** Recognise a bell entry produced by this reconciler and recover its
32
+ * internal key + stored priority. Returns null for entries that didn't
33
+ * originate here, so `listAll()` scans skip foreign entries. */
34
+ readEntry: (pluginData: unknown) => {
35
+ legacyId: string;
36
+ priority: CompletionPriority;
37
+ } | null;
38
+ }
39
+ /** Wire the host adapter + logger. Call once at startup, before
40
+ * `startCollectionWatchers` or any direct reconcile call. */
41
+ export declare function configureCollectionWatchers(config: {
42
+ adapter: CollectionNotificationAdapter;
43
+ log?: CollectionWatcherLogger;
44
+ }): void;
45
+ export declare function requireAdapter(): CollectionNotificationAdapter;
46
+ export declare function log(): CollectionWatcherLogger;
47
+ /** Test-only: clear the host wiring. */
48
+ export declare function resetCollectionWatchersConfig(): void;
49
+ export declare function errMsg(err: unknown): string;