@mulmoclaude/core 0.2.2 → 0.2.5

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 (49) hide show
  1. package/assets/helps/custom-view.md +32 -11
  2. package/dist/collection/server/index.cjs +1 -1
  3. package/dist/collection/server/index.js +1 -1
  4. package/dist/collection/server/io.d.ts +12 -6
  5. package/dist/collection-watchers/index.cjs +1 -1
  6. package/dist/collection-watchers/index.js +1 -1
  7. package/dist/feeds/index.cjs +17 -0
  8. package/dist/feeds/index.d.ts +2 -0
  9. package/dist/feeds/index.js +4 -0
  10. package/dist/feeds/ingestTypes.d.ts +57 -0
  11. package/dist/feeds/paths.cjs +48 -0
  12. package/dist/feeds/paths.cjs.map +1 -0
  13. package/dist/feeds/paths.d.ts +22 -0
  14. package/dist/feeds/paths.js +38 -0
  15. package/dist/feeds/paths.js.map +1 -0
  16. package/dist/feeds/server/agentIngest.d.ts +13 -0
  17. package/dist/feeds/server/engine.d.ts +12 -0
  18. package/dist/feeds/server/fetch/httpClient.d.ts +8 -0
  19. package/dist/feeds/server/fetch/rssParser.d.ts +12 -0
  20. package/dist/feeds/server/host.d.ts +51 -0
  21. package/dist/feeds/server/index.cjs +804 -0
  22. package/dist/feeds/server/index.cjs.map +1 -0
  23. package/dist/feeds/server/index.d.ts +7 -0
  24. package/dist/feeds/server/index.js +782 -0
  25. package/dist/feeds/server/index.js.map +1 -0
  26. package/dist/feeds/server/pathResolver.d.ts +6 -0
  27. package/dist/feeds/server/projectItem.d.ts +8 -0
  28. package/dist/feeds/server/refreshResult.d.ts +17 -0
  29. package/dist/feeds/server/registry.d.ts +14 -0
  30. package/dist/feeds/server/retrievers/httpJson.d.ts +3 -0
  31. package/dist/feeds/server/retrievers/index.d.ts +12 -0
  32. package/dist/feeds/server/retrievers/registerAll.d.ts +0 -0
  33. package/dist/feeds/server/retrievers/rss.d.ts +3 -0
  34. package/dist/feeds/server/state.d.ts +28 -0
  35. package/dist/ingestTypes-B8DaQOTy.js +15 -0
  36. package/dist/ingestTypes-B8DaQOTy.js.map +1 -0
  37. package/dist/ingestTypes-DDOVhOYC.cjs +32 -0
  38. package/dist/ingestTypes-DDOVhOYC.cjs.map +1 -0
  39. package/dist/{server-BPDCdvOI.js → server-B2YkcCwl.js} +62 -28
  40. package/dist/server-B2YkcCwl.js.map +1 -0
  41. package/dist/{server-Bego8VyU.cjs → server-D27UzXug.cjs} +62 -28
  42. package/dist/server-D27UzXug.cjs.map +1 -0
  43. package/dist/whisper/index.cjs +1 -1
  44. package/dist/whisper/index.js +1 -1
  45. package/dist/workspace-setup/index.js +5 -1
  46. package/dist/workspace-setup/index.js.map +1 -1
  47. package/package.json +20 -1
  48. package/dist/server-BPDCdvOI.js.map +0 -1
  49. package/dist/server-Bego8VyU.cjs.map +0 -1
@@ -69,19 +69,36 @@ window.__MC_VIEW = {
69
69
 
70
70
  ### Reading records
71
71
 
72
+ > **Always project `fields`.** The host refuses any read of more than 200
73
+ > records that does not pass `?fields=…` or `?ids=…`, because an unscoped
74
+ > fetch wastes work and bandwidth on columns your view never reads. Once a
75
+ > user's collection grows past the threshold an unprojected view starts
76
+ > returning `400` instead of records — write the projection from day one so
77
+ > the same view keeps working forever.
78
+
72
79
  ```js
73
80
  const { token, dataUrl } = window.__MC_VIEW;
74
- const res = await fetch(dataUrl, { headers: { Authorization: "Bearer " + token } });
75
- if (!res.ok) throw new Error("load failed: " + res.status);
81
+ // List the columns this view actually reads never send `dataUrl` raw.
82
+ const url = dataUrl + "?fields=" + encodeURIComponent("title,start,end,status");
83
+ const res = await fetch(url, { headers: { Authorization: "Bearer " + token } });
84
+ if (!res.ok) {
85
+ // Surface the server's reason (the 400 carries `{ error: "<message>" }`),
86
+ // not just the status code — that text is what tells the user / Claude
87
+ // how to fix the call (e.g. "pass `fields` to project only the columns").
88
+ const detail = await res.text();
89
+ throw new Error("load failed (" + res.status + "): " + detail);
90
+ }
76
91
  const { items } = await res.json(); // { collection, count, items: [...] }
77
92
  ```
78
93
 
79
94
  Records come back **with computed fields already resolved** (derived formulas,
80
95
  toggles, embeds) — the same numbers the user sees elsewhere.
81
96
 
82
- - Narrow large reads: `dataUrl + "?fields=title,start,end"` or `?ids=a,b,c`
83
- (comma-separated). A read of **more than 200 records** without `ids`/`fields`
84
- is refusedalways project `fields` for big collections.
97
+ - **`?fields=a,b,c`** only these columns per record (the primary key is always
98
+ included). Use this on every read; it is mandatory above 200 records.
99
+ - **`?ids=x,y,z`**only these specific record ids. Use it when the view edits
100
+ / shows one record at a time. Combinable with `fields`.
101
+ - The primary key is always returned regardless of `fields`.
85
102
 
86
103
  ### Writing records (only with the `write` capability)
87
104
 
@@ -111,9 +128,11 @@ By default a view paints once, on load. To keep it fresh, register a callback
111
128
  it runs whenever the collection's data changes on the server:
112
129
 
113
130
  ```js
131
+ // Same projection rule as the initial read — list the columns this view uses.
132
+ const url = dataUrl + "?fields=" + encodeURIComponent("title,start,end,status");
114
133
  async function render() {
115
- const res = await fetch(dataUrl, { headers: { Authorization: "Bearer " + token } });
116
- if (!res.ok) return; // handle the error per your UI
134
+ const res = await fetch(url, { headers: { Authorization: "Bearer " + token } });
135
+ if (!res.ok) return; // surface the body if you want a richer error UI
117
136
  const { items } = await res.json();
118
137
  // …draw items…
119
138
  }
@@ -303,8 +322,9 @@ fields `start` / `end` and a `title`. `capabilities: ["read"]`.
303
322
  const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
304
323
  async function main() {
305
324
  const { token, dataUrl } = window.__MC_VIEW;
306
- const res = await fetch(dataUrl, { headers: { Authorization: "Bearer " + token } });
307
- if (!res.ok) throw new Error("HTTP " + res.status);
325
+ const url = dataUrl + "?fields=" + encodeURIComponent("title,start");
326
+ const res = await fetch(url, { headers: { Authorization: "Bearer " + token } });
327
+ if (!res.ok) throw new Error("HTTP " + res.status + ": " + (await res.text()));
308
328
  const { items } = await res.json();
309
329
  const buckets = MONTHS.map(() => []);
310
330
  for (const it of items) {
@@ -382,10 +402,11 @@ writes it back. `capabilities: ["read","write"]`.
382
402
  const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
383
403
  const { token, dataUrl } = window.__MC_VIEW;
384
404
  const auth = { Authorization: "Bearer " + token, "Content-Type": "application/json" };
405
+ const readUrl = dataUrl + "?fields=" + encodeURIComponent("title,start");
385
406
 
386
407
  async function read() {
387
- const res = await fetch(dataUrl, { headers: { Authorization: "Bearer " + token } });
388
- if (!res.ok) throw new Error("HTTP " + res.status);
408
+ const res = await fetch(readUrl, { headers: { Authorization: "Bearer " + token } });
409
+ if (!res.ok) throw new Error("HTTP " + res.status + ": " + (await res.text()));
389
410
  return (await res.json()).items;
390
411
  }
391
412
  async function bump(item) {
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_server = require("../../server-Bego8VyU.cjs");
2
+ const require_server = require("../../server-D27UzXug.cjs");
3
3
  const require_collection_paths = require("../paths.cjs");
4
4
  exports.COMPUTED_TYPES = require_server.COMPUTED_TYPES;
5
5
  exports.CollectionSchemaZ = require_server.CollectionSchemaZ;
@@ -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-BPDCdvOI.js";
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-B2YkcCwl.js";
2
2
  import { isSafeActionTemplatePath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
3
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 };
@@ -82,12 +82,18 @@ export declare function generateItemId(): string;
82
82
  * schema-validated `views/*.html` path, resolved with realpath containment.
83
83
  * Returns the HTML, or null when the path is unsafe or the file is missing.
84
84
  *
85
- * The base dir is source-aware: a **project** collection authors views in the
86
- * `data/skills/<slug>/` staging dir (custom-view HTML is staging-only — NOT
87
- * mirrored to `.claude/skills`, because rendering is host-side; see
88
- * plans/done/feat-collections-custom-views.md), whereas a **user** / **feed**
89
- * collection is authored directly in its own discovered `skillDir`. Reading
90
- * relative to the wrong tree would 404 a perfectly valid view. */
85
+ * The base dir is source-aware. A **project** collection AUTHORED in-place
86
+ * keeps its views in the `data/skills/<slug>/` staging dir (host-side
87
+ * rendering; see plans/done/feat-collections-custom-views.md). A **project**
88
+ * collection that was IMPORTED via the discover panel (rename-on-conflict)
89
+ * carries its views inside `.claude/skills/<slug>/views/` the skill folder
90
+ * itself without a staging-dir mirror; reading only the staging path would
91
+ * 404 a perfectly valid imported view. We try staging first, then fall back
92
+ * to the discovered `skillDir`, so both layouts read cleanly. A **user** /
93
+ * **feed** collection is always authored in its own discovered `skillDir`,
94
+ * so it only needs the single lookup. `resolveTemplatePath` does the
95
+ * containment / `..` defense per base, so the fallback never broadens the
96
+ * attack surface. */
91
97
  export declare function readCustomViewHtml(collection: Pick<LoadedCollection, "slug" | "source" | "skillDir">, viewFile: string, opts?: IoOptions): Promise<string | null>;
92
98
  /** The item id a CREATE should use for `schema`, or null when the
93
99
  * caller should generate one. A singleton collection pins every
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_collection_index = require("../collection/index.cjs");
3
- const require_server = require("../server-Bego8VyU.cjs");
3
+ const require_server = require("../server-D27UzXug.cjs");
4
4
  const require_notifier = require("../notifier-bS8IEeLA.cjs");
5
5
  let node_fs = require("node:fs");
6
6
  let node_fs_promises = require("node:fs/promises");
@@ -1,5 +1,5 @@
1
1
  import { whenMatches } from "../collection/index.js";
2
- import { D as listItems, _ as loadCollection, c as isTriggerDue, g as discoverCollections, k as readItem, l as maybeSpawnSuccessor } from "../server-BPDCdvOI.js";
2
+ import { D as listItems, _ as loadCollection, c as isTriggerDue, g as discoverCollections, k as readItem, l as maybeSpawnSuccessor } from "../server-B2YkcCwl.js";
3
3
  import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-ChpY0XrY.js";
4
4
  import { watch } from "node:fs";
5
5
  import { mkdir } from "node:fs/promises";
@@ -0,0 +1,17 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_deriveAll = require("../deriveAll-VRWrs3SF.cjs");
3
+ const require_ingestTypes = require("../ingestTypes-DDOVhOYC.cjs");
4
+ const require_feeds_paths = require("./paths.cjs");
5
+ exports.AGENT_INGEST_KIND = require_ingestTypes.AGENT_INGEST_KIND;
6
+ exports.DEFAULT_FEED_MAX_ITEMS = require_ingestTypes.DEFAULT_FEED_MAX_ITEMS;
7
+ exports.FEEDS_DIR = require_feeds_paths.FEEDS_DIR;
8
+ exports.FEED_SCHEDULES = require_deriveAll.FEED_SCHEDULES;
9
+ exports.FEED_STATE_FILE = require_feeds_paths.FEED_STATE_FILE;
10
+ exports.INGEST_KINDS = require_deriveAll.INGEST_KINDS;
11
+ exports.INGEST_STATE_DIR = require_feeds_paths.INGEST_STATE_DIR;
12
+ exports.feedDir = require_feeds_paths.feedDir;
13
+ exports.feedStatePath = require_feeds_paths.feedStatePath;
14
+ exports.feedsRoot = require_feeds_paths.feedsRoot;
15
+ exports.ingestStateDir = require_feeds_paths.ingestStateDir;
16
+ exports.ingestStatePath = require_feeds_paths.ingestStatePath;
17
+ exports.isFeedSchedule = require_ingestTypes.isFeedSchedule;
@@ -0,0 +1,2 @@
1
+ export * from './ingestTypes.js';
2
+ export * from './paths.js';
@@ -0,0 +1,4 @@
1
+ import { a as FEED_SCHEDULES, o as INGEST_KINDS } from "../deriveAll-vzIhhKBK.js";
2
+ import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../ingestTypes-B8DaQOTy.js";
3
+ import { FEEDS_DIR, FEED_STATE_FILE, INGEST_STATE_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "./paths.js";
4
+ export { AGENT_INGEST_KIND, DEFAULT_FEED_MAX_ITEMS, FEEDS_DIR, FEED_SCHEDULES, FEED_STATE_FILE, INGEST_KINDS, INGEST_STATE_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath, isFeedSchedule };
@@ -0,0 +1,57 @@
1
+ import { CollectionIngest, INGEST_KINDS, FEED_SCHEDULES, IngestKind, FeedSchedule } from '../collection/index.js';
2
+ export declare const AGENT_INGEST_KIND: "agent";
3
+ export type AgentIngestKind = typeof AGENT_INGEST_KIND;
4
+ export { INGEST_KINDS, FEED_SCHEDULES, type IngestKind, type FeedSchedule };
5
+ export declare function isFeedSchedule(value: unknown): value is FeedSchedule;
6
+ /** Default cap on stored records per feed when `ingest.maxItems` is
7
+ * omitted. Keeps high-volume feeds (news / podcasts) bounded. */
8
+ export declare const DEFAULT_FEED_MAX_ITEMS = 100;
9
+ /** Declarative field map: target collection field name → source path
10
+ * into the raw item (dot/bracket path, e.g. `"title"` or
11
+ * `"data.name"`). */
12
+ export type IngestFieldMap = Record<string, string>;
13
+ /** Declarative retrieval (`rss`/`atom`/`http-json`): the host fetches `url`
14
+ * and projects each item through `map`. The canonical schema (in
15
+ * `../collection`) only promises the minimal `CollectionIngest`; this
16
+ * feeds-only subtype narrows those + adds the retrieval fields the engine
17
+ * needs. */
18
+ export interface DeclarativeIngestSpec extends CollectionIngest {
19
+ /** Which declarative retriever handles this feed. */
20
+ kind: IngestKind;
21
+ /** Endpoint to fetch (http/https). */
22
+ url: string;
23
+ /** Refresh cadence. */
24
+ schedule: FeedSchedule;
25
+ /** `http-json` only: dot/bracket path to the array of items in the
26
+ * response (e.g. `"hourly[]"` or `"data.results[]"`). Ignored for
27
+ * `rss`/`atom`, which yield items natively. */
28
+ itemsAt?: string;
29
+ /** target field → source path. Projects each raw item into a record
30
+ * whose keys match the schema's `fields`. */
31
+ map: IngestFieldMap;
32
+ /** Optional source path used to derive the primaryKey value when the
33
+ * mapped record's primaryKey is empty (e.g. `"feedId"`). Falls back
34
+ * to a content hash of the record. */
35
+ idFrom?: string;
36
+ /** Cap on stored records. After each fetch the feed keeps only the
37
+ * newest `maxItems` (ordered by the schema's first `date` field) and
38
+ * deletes the rest. Defaults to {@link DEFAULT_FEED_MAX_ITEMS} when
39
+ * omitted; `0` disables the cap (keep everything). Pruning is skipped
40
+ * when the schema has no `date` field to order by. */
41
+ maxItems?: number;
42
+ }
43
+ /** Agent-performed retrieval (`kind: "agent"`). No `url`/`map`: the host seeds
44
+ * a hidden background worker (origin `system`) in `role` with `template` + a
45
+ * summary of every record, and the worker edits the records itself via the
46
+ * collections io layer. Valid on any collection (primarily skill-backed). */
47
+ export interface AgentIngestSpec extends CollectionIngest {
48
+ kind: AgentIngestKind;
49
+ /** Refresh cadence (same vocabulary as declarative feeds). */
50
+ schedule: FeedSchedule;
51
+ /** Role id the scheduled hidden worker runs in. */
52
+ role: string;
53
+ /** Skill-relative template path (under `templates/`) seeding the worker. */
54
+ template: string;
55
+ }
56
+ /** The `ingest` block carried on a `CollectionSchema`, discriminated on `kind`. */
57
+ export type IngestSpec = DeclarativeIngestSpec | AgentIngestSpec;
@@ -0,0 +1,48 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
3
+ let node_path = require("node:path");
4
+ node_path = require_rolldown_runtime.__toESM(node_path, 1);
5
+ //#region src/feeds/paths.ts
6
+ var FEEDS_DIR = "feeds";
7
+ var FEED_STATE_FILE = "_state.json";
8
+ /** Where retrieval state for NON-feed collections with an `ingest` block
9
+ * (`kind: "agent"`) lives — one file per collection, OUTSIDE the collection's
10
+ * dataDir (where `listItems` would read it as a record) and outside `feeds/`
11
+ * (a schema-less `feeds/<slug>/` dir confuses feed discovery). Mirrors the
12
+ * host's `WORKSPACE_DIRS.ingestState`. */
13
+ var INGEST_STATE_DIR = "data/ingest-state";
14
+ /** Absolute path to the feeds registry root for a workspace. */
15
+ function feedsRoot(workspaceRoot) {
16
+ return node_path.default.join(workspaceRoot, FEEDS_DIR);
17
+ }
18
+ /** Absolute path to one feed's directory (`<root>/<slug>/`). */
19
+ function feedDir(slug, workspaceRoot) {
20
+ return node_path.default.join(feedsRoot(workspaceRoot), slug);
21
+ }
22
+ /** Absolute path to one feed's retrieval-state file. */
23
+ function feedStatePath(slug, workspaceRoot) {
24
+ return node_path.default.join(feedsRoot(workspaceRoot), slug, FEED_STATE_FILE);
25
+ }
26
+ /** Directory holding retrieval state for NON-feed collections with an
27
+ * `ingest` block (`kind: "agent"`). One file per collection. */
28
+ function ingestStateDir(workspaceRoot) {
29
+ return node_path.default.join(workspaceRoot, INGEST_STATE_DIR);
30
+ }
31
+ /** Absolute path to a non-feed collection's ingest-state file
32
+ * (`data/ingest-state/<slug>.json`). Kept OUT of the collection's dataDir
33
+ * (where `listItems` would read it as a record) and out of `feeds/` (a
34
+ * schema-less `feeds/<slug>/` dir confuses feed discovery). */
35
+ function ingestStatePath(slug, workspaceRoot) {
36
+ return node_path.default.join(ingestStateDir(workspaceRoot), `${slug}.json`);
37
+ }
38
+ //#endregion
39
+ exports.FEEDS_DIR = FEEDS_DIR;
40
+ exports.FEED_STATE_FILE = FEED_STATE_FILE;
41
+ exports.INGEST_STATE_DIR = INGEST_STATE_DIR;
42
+ exports.feedDir = feedDir;
43
+ exports.feedStatePath = feedStatePath;
44
+ exports.feedsRoot = feedsRoot;
45
+ exports.ingestStateDir = ingestStateDir;
46
+ exports.ingestStatePath = ingestStatePath;
47
+
48
+ //# sourceMappingURL=paths.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.cjs","names":[],"sources":["../../src/feeds/paths.ts"],"sourcesContent":["// Path helpers for the non-skill Feeds registry. Each feed lives at\n// `<workspace>/feeds/<slug>/schema.json` (a CollectionSchema + `ingest`\n// block) with its retrieval state alongside at\n// `<workspace>/feeds/<slug>/_state.json`. Records land wherever the\n// schema's `dataPath` points (validated by `resolveDataDir`), exactly\n// like every other collection.\n//\n// Pure root→path functions: `workspaceRoot` is always passed in (the host\n// supplies its default). Slugs reaching these helpers must already have\n// passed `safeSlugName` — these joins do not re-sanitize.\n\nimport path from \"node:path\";\n\nexport const FEEDS_DIR = \"feeds\";\nexport const FEED_STATE_FILE = \"_state.json\";\n\n/** Where retrieval state for NON-feed collections with an `ingest` block\n * (`kind: \"agent\"`) lives — one file per collection, OUTSIDE the collection's\n * dataDir (where `listItems` would read it as a record) and outside `feeds/`\n * (a schema-less `feeds/<slug>/` dir confuses feed discovery). Mirrors the\n * host's `WORKSPACE_DIRS.ingestState`. */\nexport const INGEST_STATE_DIR = \"data/ingest-state\";\n\n/** Absolute path to the feeds registry root for a workspace. */\nexport function feedsRoot(workspaceRoot: string): string {\n return path.join(workspaceRoot, FEEDS_DIR);\n}\n\n/** Absolute path to one feed's directory (`<root>/<slug>/`). */\nexport function feedDir(slug: string, workspaceRoot: string): string {\n return path.join(feedsRoot(workspaceRoot), slug);\n}\n\n/** Absolute path to one feed's retrieval-state file. */\nexport function feedStatePath(slug: string, workspaceRoot: string): string {\n return path.join(feedsRoot(workspaceRoot), slug, FEED_STATE_FILE);\n}\n\n/** Directory holding retrieval state for NON-feed collections with an\n * `ingest` block (`kind: \"agent\"`). One file per collection. */\nexport function ingestStateDir(workspaceRoot: string): string {\n return path.join(workspaceRoot, INGEST_STATE_DIR);\n}\n\n/** Absolute path to a non-feed collection's ingest-state file\n * (`data/ingest-state/<slug>.json`). Kept OUT of the collection's dataDir\n * (where `listItems` would read it as a record) and out of `feeds/` (a\n * schema-less `feeds/<slug>/` dir confuses feed discovery). */\nexport function ingestStatePath(slug: string, workspaceRoot: string): string {\n return path.join(ingestStateDir(workspaceRoot), `${slug}.json`);\n}\n"],"mappings":";;;;;AAaA,IAAa,YAAY;AACzB,IAAa,kBAAkB;;;;;;AAO/B,IAAa,mBAAmB;;AAGhC,SAAgB,UAAU,eAA+B;CACvD,OAAO,UAAA,QAAK,KAAK,eAAe,SAAS;AAC3C;;AAGA,SAAgB,QAAQ,MAAc,eAA+B;CACnE,OAAO,UAAA,QAAK,KAAK,UAAU,aAAa,GAAG,IAAI;AACjD;;AAGA,SAAgB,cAAc,MAAc,eAA+B;CACzE,OAAO,UAAA,QAAK,KAAK,UAAU,aAAa,GAAG,MAAM,eAAe;AAClE;;;AAIA,SAAgB,eAAe,eAA+B;CAC5D,OAAO,UAAA,QAAK,KAAK,eAAe,gBAAgB;AAClD;;;;;AAMA,SAAgB,gBAAgB,MAAc,eAA+B;CAC3E,OAAO,UAAA,QAAK,KAAK,eAAe,aAAa,GAAG,GAAG,KAAK,MAAM;AAChE"}
@@ -0,0 +1,22 @@
1
+ export declare const FEEDS_DIR = "feeds";
2
+ export declare const FEED_STATE_FILE = "_state.json";
3
+ /** Where retrieval state for NON-feed collections with an `ingest` block
4
+ * (`kind: "agent"`) lives — one file per collection, OUTSIDE the collection's
5
+ * dataDir (where `listItems` would read it as a record) and outside `feeds/`
6
+ * (a schema-less `feeds/<slug>/` dir confuses feed discovery). Mirrors the
7
+ * host's `WORKSPACE_DIRS.ingestState`. */
8
+ export declare const INGEST_STATE_DIR = "data/ingest-state";
9
+ /** Absolute path to the feeds registry root for a workspace. */
10
+ export declare function feedsRoot(workspaceRoot: string): string;
11
+ /** Absolute path to one feed's directory (`<root>/<slug>/`). */
12
+ export declare function feedDir(slug: string, workspaceRoot: string): string;
13
+ /** Absolute path to one feed's retrieval-state file. */
14
+ export declare function feedStatePath(slug: string, workspaceRoot: string): string;
15
+ /** Directory holding retrieval state for NON-feed collections with an
16
+ * `ingest` block (`kind: "agent"`). One file per collection. */
17
+ export declare function ingestStateDir(workspaceRoot: string): string;
18
+ /** Absolute path to a non-feed collection's ingest-state file
19
+ * (`data/ingest-state/<slug>.json`). Kept OUT of the collection's dataDir
20
+ * (where `listItems` would read it as a record) and out of `feeds/` (a
21
+ * schema-less `feeds/<slug>/` dir confuses feed discovery). */
22
+ export declare function ingestStatePath(slug: string, workspaceRoot: string): string;
@@ -0,0 +1,38 @@
1
+ import path from "node:path";
2
+ //#region src/feeds/paths.ts
3
+ var FEEDS_DIR = "feeds";
4
+ var FEED_STATE_FILE = "_state.json";
5
+ /** Where retrieval state for NON-feed collections with an `ingest` block
6
+ * (`kind: "agent"`) lives — one file per collection, OUTSIDE the collection's
7
+ * dataDir (where `listItems` would read it as a record) and outside `feeds/`
8
+ * (a schema-less `feeds/<slug>/` dir confuses feed discovery). Mirrors the
9
+ * host's `WORKSPACE_DIRS.ingestState`. */
10
+ var INGEST_STATE_DIR = "data/ingest-state";
11
+ /** Absolute path to the feeds registry root for a workspace. */
12
+ function feedsRoot(workspaceRoot) {
13
+ return path.join(workspaceRoot, FEEDS_DIR);
14
+ }
15
+ /** Absolute path to one feed's directory (`<root>/<slug>/`). */
16
+ function feedDir(slug, workspaceRoot) {
17
+ return path.join(feedsRoot(workspaceRoot), slug);
18
+ }
19
+ /** Absolute path to one feed's retrieval-state file. */
20
+ function feedStatePath(slug, workspaceRoot) {
21
+ return path.join(feedsRoot(workspaceRoot), slug, FEED_STATE_FILE);
22
+ }
23
+ /** Directory holding retrieval state for NON-feed collections with an
24
+ * `ingest` block (`kind: "agent"`). One file per collection. */
25
+ function ingestStateDir(workspaceRoot) {
26
+ return path.join(workspaceRoot, INGEST_STATE_DIR);
27
+ }
28
+ /** Absolute path to a non-feed collection's ingest-state file
29
+ * (`data/ingest-state/<slug>.json`). Kept OUT of the collection's dataDir
30
+ * (where `listItems` would read it as a record) and out of `feeds/` (a
31
+ * schema-less `feeds/<slug>/` dir confuses feed discovery). */
32
+ function ingestStatePath(slug, workspaceRoot) {
33
+ return path.join(ingestStateDir(workspaceRoot), `${slug}.json`);
34
+ }
35
+ //#endregion
36
+ export { FEEDS_DIR, FEED_STATE_FILE, INGEST_STATE_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath };
37
+
38
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","names":[],"sources":["../../src/feeds/paths.ts"],"sourcesContent":["// Path helpers for the non-skill Feeds registry. Each feed lives at\n// `<workspace>/feeds/<slug>/schema.json` (a CollectionSchema + `ingest`\n// block) with its retrieval state alongside at\n// `<workspace>/feeds/<slug>/_state.json`. Records land wherever the\n// schema's `dataPath` points (validated by `resolveDataDir`), exactly\n// like every other collection.\n//\n// Pure root→path functions: `workspaceRoot` is always passed in (the host\n// supplies its default). Slugs reaching these helpers must already have\n// passed `safeSlugName` — these joins do not re-sanitize.\n\nimport path from \"node:path\";\n\nexport const FEEDS_DIR = \"feeds\";\nexport const FEED_STATE_FILE = \"_state.json\";\n\n/** Where retrieval state for NON-feed collections with an `ingest` block\n * (`kind: \"agent\"`) lives — one file per collection, OUTSIDE the collection's\n * dataDir (where `listItems` would read it as a record) and outside `feeds/`\n * (a schema-less `feeds/<slug>/` dir confuses feed discovery). Mirrors the\n * host's `WORKSPACE_DIRS.ingestState`. */\nexport const INGEST_STATE_DIR = \"data/ingest-state\";\n\n/** Absolute path to the feeds registry root for a workspace. */\nexport function feedsRoot(workspaceRoot: string): string {\n return path.join(workspaceRoot, FEEDS_DIR);\n}\n\n/** Absolute path to one feed's directory (`<root>/<slug>/`). */\nexport function feedDir(slug: string, workspaceRoot: string): string {\n return path.join(feedsRoot(workspaceRoot), slug);\n}\n\n/** Absolute path to one feed's retrieval-state file. */\nexport function feedStatePath(slug: string, workspaceRoot: string): string {\n return path.join(feedsRoot(workspaceRoot), slug, FEED_STATE_FILE);\n}\n\n/** Directory holding retrieval state for NON-feed collections with an\n * `ingest` block (`kind: \"agent\"`). One file per collection. */\nexport function ingestStateDir(workspaceRoot: string): string {\n return path.join(workspaceRoot, INGEST_STATE_DIR);\n}\n\n/** Absolute path to a non-feed collection's ingest-state file\n * (`data/ingest-state/<slug>.json`). Kept OUT of the collection's dataDir\n * (where `listItems` would read it as a record) and out of `feeds/` (a\n * schema-less `feeds/<slug>/` dir confuses feed discovery). */\nexport function ingestStatePath(slug: string, workspaceRoot: string): string {\n return path.join(ingestStateDir(workspaceRoot), `${slug}.json`);\n}\n"],"mappings":";;AAaA,IAAa,YAAY;AACzB,IAAa,kBAAkB;;;;;;AAO/B,IAAa,mBAAmB;;AAGhC,SAAgB,UAAU,eAA+B;CACvD,OAAO,KAAK,KAAK,eAAe,SAAS;AAC3C;;AAGA,SAAgB,QAAQ,MAAc,eAA+B;CACnE,OAAO,KAAK,KAAK,UAAU,aAAa,GAAG,IAAI;AACjD;;AAGA,SAAgB,cAAc,MAAc,eAA+B;CACzE,OAAO,KAAK,KAAK,UAAU,aAAa,GAAG,MAAM,eAAe;AAClE;;;AAIA,SAAgB,eAAe,eAA+B;CAC5D,OAAO,KAAK,KAAK,eAAe,gBAAgB;AAClD;;;;;AAMA,SAAgB,gBAAgB,MAAc,eAA+B;CAC3E,OAAO,KAAK,KAAK,eAAe,aAAa,GAAG,GAAG,KAAK,MAAM;AAChE"}
@@ -0,0 +1,13 @@
1
+ import { LoadedCollection } from '../../collection/server/index.js';
2
+ import { RefreshResult } from './refreshResult.js';
3
+ export type { AgentWorkerResult, AgentWorkerRunner } from './host.js';
4
+ /** Dispatch one agent-ingest refresh: build the seed, launch a worker, and (on a
5
+ * successful launch) stamp `lastFetchedAt` with the DISPATCH time — not
6
+ * completion. That's what gates the due-loop, so a slow worker can't cause a
7
+ * double-dispatch. `opts.hidden` (default true) runs an invisible system worker
8
+ * for SCHEDULED refreshes; a MANUAL Refresh passes `hidden:false` for a visible,
9
+ * debuggable session. Failure-isolated: never throws; cap-miss / template-miss /
10
+ * launch error leave state untouched and report via `errors`. */
11
+ export declare function refreshViaAgent(workspaceRoot: string, collection: LoadedCollection, opts?: {
12
+ hidden?: boolean;
13
+ }): Promise<RefreshResult>;
@@ -0,0 +1,12 @@
1
+ import { LoadedCollection } from '../../collection/server/index.js';
2
+ import { RefreshResult } from './refreshResult.js';
3
+ export type { RefreshResult } from './refreshResult.js';
4
+ /** Fetch one feed now, upsert its records, then enforce the maxItems cap.
5
+ * Failure-isolated: returns an errors array rather than throwing. */
6
+ export declare function refreshOne(workspaceRoot: string, feed: LoadedCollection, opts?: {
7
+ hidden?: boolean;
8
+ }): Promise<RefreshResult>;
9
+ /** Refresh every collection whose ingest schedule says it's due — declarative
10
+ * feeds AND skill-backed collections with `ingest.kind: "agent"`. Called by the
11
+ * hourly system task. Sequential + failure-isolated. */
12
+ export declare function refreshDue(workspaceRoot?: string): Promise<RefreshResult[]>;
@@ -0,0 +1,8 @@
1
+ /** Identifies the bot to site operators. */
2
+ export declare const FEED_USER_AGENT = "MulmoClaude-FeedBot/1.0 (+https://github.com/receptron/mulmoclaude)";
3
+ /** Per-request wall-clock cap so a hung server can't wedge a refresh. */
4
+ export declare const DEFAULT_FEED_TIMEOUT_MS: number;
5
+ /** Fetch a URL as text, throwing on guard rejection, network error, or non-2xx. */
6
+ export declare function fetchText(url: string, timeoutMs?: number): Promise<string>;
7
+ /** Fetch a URL as parsed JSON, throwing on guard rejection, network error, or non-2xx. */
8
+ export declare function fetchJson(url: string, timeoutMs?: number): Promise<unknown>;
@@ -0,0 +1,12 @@
1
+ export interface ParsedFeedItem {
2
+ /** The raw parsed XML <item>/<entry> object, verbatim. */
3
+ raw: Record<string, unknown>;
4
+ }
5
+ export interface ParsedFeed {
6
+ kind: "rss" | "atom";
7
+ title: string | null;
8
+ items: ParsedFeedItem[];
9
+ }
10
+ /** Parse an RSS/Atom/RDF feed body. Returns null when the input doesn't
11
+ * look like a feed we understand. */
12
+ export declare function parseFeed(body: string): ParsedFeed | null;
@@ -0,0 +1,51 @@
1
+ /** Outcome of launching one hidden/visible agent-ingest worker. `chatId` lets
2
+ * the caller register a completion hook so a failed refresh doesn't die
3
+ * silently. */
4
+ export type AgentWorkerResult = {
5
+ ok: true;
6
+ chatId: string;
7
+ } | {
8
+ ok: false;
9
+ error: string;
10
+ };
11
+ /** Launches a worker chat. Injected at boot to keep the feeds engine from
12
+ * importing a host's routes/session layer. `hidden` chooses an invisible
13
+ * system worker (scheduled refresh) vs a visible session the user can watch
14
+ * (manual Refresh — debuggable). `onComplete` is a one-shot completion hook
15
+ * (only honoured for hidden workers) so the dispatcher learns success/failure.
16
+ * Returns `ok:false` on the concurrency-cap miss or a launch error — the caller
17
+ * leaves state untouched and retries next tick. */
18
+ export type AgentWorkerRunner = (args: {
19
+ message: string;
20
+ roleId: string;
21
+ hidden: boolean;
22
+ onComplete?: (outcome: {
23
+ didError: boolean;
24
+ }) => void | Promise<void>;
25
+ }) => Promise<AgentWorkerResult>;
26
+ /** Structured logger, `(prefix, msg, data?)` — same shape as `CollectionLogger`. */
27
+ export interface FeedsLogger {
28
+ error: (prefix: string, message: string, data?: Record<string, unknown>) => void;
29
+ warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;
30
+ info: (prefix: string, message: string, data?: Record<string, unknown>) => void;
31
+ debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;
32
+ }
33
+ export interface FeedsHost {
34
+ /** Absolute workspace root — the default for `refreshDue()` and state paths. */
35
+ workspaceRoot: string;
36
+ /** Host logger. */
37
+ log: FeedsLogger;
38
+ /** Host atomic file writer (state files). */
39
+ writeFileAtomic: (filePath: string, content: string) => Promise<void>;
40
+ /** Launches the agent-ingest worker (was `setAgentWorkerRunner`). */
41
+ spawnWorker: AgentWorkerRunner;
42
+ }
43
+ /** Wire the feeds engine to a host. Call once at startup, before any refresh. */
44
+ export declare function configureFeedsHost(host: FeedsHost): void;
45
+ /** The configured host, or throw if `configureFeedsHost` was never called. */
46
+ export declare function requireFeedsHost(): FeedsHost;
47
+ /** Test-only: clear the configured host. */
48
+ export declare function resetFeedsHostForTesting(): void;
49
+ /** Forwarding logger so engine modules can `import { log }` without each
50
+ * reaching for `requireFeedsHost().log`. */
51
+ export declare const log: FeedsLogger;