@cyanheads/mcp-ts-core 0.9.15 → 0.9.17

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 (41) hide show
  1. package/AGENTS.md +2 -1
  2. package/CLAUDE.md +2 -1
  3. package/README.md +1 -1
  4. package/changelog/0.9.x/0.9.16.md +11 -0
  5. package/changelog/0.9.x/0.9.17.md +23 -0
  6. package/dist/logs/combined.log +8 -0
  7. package/dist/logs/error.log +4 -0
  8. package/dist/logs/interactions.log +0 -0
  9. package/dist/mcp-server/tools/utils/toolDefinition.d.ts +9 -1
  10. package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
  11. package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
  12. package/dist/services/mirror/core/defineMirror.d.ts +62 -0
  13. package/dist/services/mirror/core/defineMirror.d.ts.map +1 -0
  14. package/dist/services/mirror/core/defineMirror.js +99 -0
  15. package/dist/services/mirror/core/defineMirror.js.map +1 -0
  16. package/dist/services/mirror/core/runner.d.ts +29 -0
  17. package/dist/services/mirror/core/runner.d.ts.map +1 -0
  18. package/dist/services/mirror/core/runner.js +95 -0
  19. package/dist/services/mirror/core/runner.js.map +1 -0
  20. package/dist/services/mirror/index.d.ts +18 -0
  21. package/dist/services/mirror/index.d.ts.map +1 -0
  22. package/dist/services/mirror/index.js +17 -0
  23. package/dist/services/mirror/index.js.map +1 -0
  24. package/dist/services/mirror/sqlite/handle.d.ts +48 -0
  25. package/dist/services/mirror/sqlite/handle.d.ts.map +1 -0
  26. package/dist/services/mirror/sqlite/handle.js +105 -0
  27. package/dist/services/mirror/sqlite/handle.js.map +1 -0
  28. package/dist/services/mirror/sqlite/schema.d.ts +38 -0
  29. package/dist/services/mirror/sqlite/schema.d.ts.map +1 -0
  30. package/dist/services/mirror/sqlite/schema.js +124 -0
  31. package/dist/services/mirror/sqlite/schema.js.map +1 -0
  32. package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts +27 -0
  33. package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts.map +1 -0
  34. package/dist/services/mirror/sqlite/sqliteMirrorStore.js +286 -0
  35. package/dist/services/mirror/sqlite/sqliteMirrorStore.js.map +1 -0
  36. package/dist/services/mirror/types.d.ts +211 -0
  37. package/dist/services/mirror/types.d.ts.map +1 -0
  38. package/dist/services/mirror/types.js +9 -0
  39. package/dist/services/mirror/types.js.map +1 -0
  40. package/package.json +11 -2
  41. package/skills/api-mirror/SKILL.md +103 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @fileoverview Public contracts for the MirrorService — the source-agnostic
3
+ * machinery for a persistent, self-refreshing local mirror of a bulk upstream
4
+ * dataset. The framework owns the store, the sync-state machine, and the runner;
5
+ * each server supplies only the ingester (`sync` generator) and the schema.
6
+ * @module services/mirror/types
7
+ */
8
+ import type { SqliteHandle, SqlValue } from './sqlite/handle.js';
9
+ export type { SqliteHandle, SqliteStatement, SqlValue } from './sqlite/handle.js';
10
+ /** One mirrored record — a flat object keyed by declared column name. */
11
+ export type MirrorRow = Record<string, SqlValue>;
12
+ /** Lifecycle state of a mirror's local dataset. */
13
+ export type SyncStatus = 'pending' | 'in_progress' | 'complete' | 'error';
14
+ /**
15
+ * Duck-typed logger consumed by the runner. A sync runs outside the MCP request
16
+ * pipeline (cron job or CLI script), so it has no request `Context` — any
17
+ * logger with these methods works (the framework `logger`, a `ctx.log`, or a
18
+ * console wrapper). All methods are optional.
19
+ */
20
+ export interface MirrorLogger {
21
+ debug?(message: string, meta?: object): void;
22
+ error?(message: string, meta?: object): void;
23
+ info?(message: string, meta?: object): void;
24
+ notice?(message: string, meta?: object): void;
25
+ warning?(message: string, meta?: object): void;
26
+ }
27
+ /**
28
+ * Persisted sync state — the resume-on-interrupt checkpoint store. The two
29
+ * position fields are deliberately distinct:
30
+ *
31
+ * - `cursor` is the **volatile** intra-run resume position (e.g. an OAI-PMH
32
+ * resumption token). It is valid only within a single run, may expire, and is
33
+ * cleared on completion. The runner threads it back into `sync()` to resume an
34
+ * interrupted init.
35
+ * - `checkpoint` is the **durable** high-water mark (e.g. the max record
36
+ * datestamp). It advances monotonically and only on success, and seeds the
37
+ * next incremental refresh.
38
+ *
39
+ * They cannot be merged: for a token-paged source the mid-run cursor is not a
40
+ * valid refresh seed, and the high-water mark is not a valid mid-run resume
41
+ * position.
42
+ */
43
+ export interface SyncState {
44
+ /** Durable incremental high-water mark; advances only on success. */
45
+ checkpoint?: string | undefined;
46
+ /** ISO 8601 timestamp the last run completed successfully. Drives readiness. */
47
+ completedAt?: string | undefined;
48
+ /** Volatile intra-run resume position; cleared on completion. */
49
+ cursor?: string | undefined;
50
+ /** Message from the last failed run, set when `status === 'error'`. */
51
+ error?: string | undefined;
52
+ /** ISO 8601 timestamp the current run started. */
53
+ startedAt?: string | undefined;
54
+ status: SyncStatus;
55
+ /** Record count, set when a sync completes. */
56
+ total?: number | undefined;
57
+ }
58
+ /** Context passed to a server's `sync` generator on each run. */
59
+ export interface SyncContext {
60
+ /** Durable high-water mark to harvest from (refresh; also init-resume recovery). */
61
+ checkpoint?: string | undefined;
62
+ /** Volatile resume position from a prior interrupted run (init only). */
63
+ cursor?: string | undefined;
64
+ /** `init` for a full harvest, `refresh` for an incremental one. */
65
+ mode: SyncMode;
66
+ /** Aborts the run; the generator should stop and the runner persists state. */
67
+ signal: AbortSignal;
68
+ }
69
+ export type SyncMode = 'init' | 'refresh';
70
+ /**
71
+ * One page yielded by a server's `sync` generator. The framework upserts the
72
+ * records, deletes the tombstones, and persists the cursor/checkpoint — all in
73
+ * one transaction per page — so an interrupt resumes from the last yielded page.
74
+ */
75
+ export interface SyncPage {
76
+ /**
77
+ * Updated durable high-water mark after this page. Must be lexicographically
78
+ * monotonic (e.g. ISO 8601) — the runner advances the stored checkpoint only
79
+ * when a page's value compares greater.
80
+ */
81
+ checkpoint?: string | undefined;
82
+ /** Updated volatile resume position after this page. */
83
+ cursor?: string | undefined;
84
+ /** Records to upsert (keyed by column name). */
85
+ records: MirrorRow[];
86
+ /** Primary-key values to delete (deleted upstream records). */
87
+ tombstones?: string[];
88
+ }
89
+ /** A server's ingester: an async generator of pages from the upstream source. */
90
+ export type SyncGenerator = (ctx: SyncContext) => AsyncGenerator<SyncPage>;
91
+ /** Progress hook invoked after each persisted page. */
92
+ export type SyncProgress = (info: {
93
+ pages: number;
94
+ records: number;
95
+ tombstones: number;
96
+ cursor?: string | undefined;
97
+ checkpoint?: string | undefined;
98
+ }) => void;
99
+ /** Options for a single sync run. */
100
+ export interface RunSyncOptions {
101
+ mode: SyncMode;
102
+ onProgress?: SyncProgress;
103
+ }
104
+ /** Outcome of a completed sync run. */
105
+ export interface SyncResult {
106
+ pagesFetched: number;
107
+ recordsApplied: number;
108
+ tombstonesApplied: number;
109
+ total: number;
110
+ }
111
+ /** Public status of a mirror — what tools surface to agents. */
112
+ export interface MirrorStatus {
113
+ checkpoint?: string | undefined;
114
+ completedAt?: string | undefined;
115
+ error?: string | undefined;
116
+ /**
117
+ * `true` once a full sync has ever completed (`completedAt != null`), NOT
118
+ * `status === 'complete'`. The dataset stays queryable during a refresh, so
119
+ * readiness keys off the durable completion marker — an in-progress or failed
120
+ * refresh on top of a complete mirror is still ready.
121
+ */
122
+ ready: boolean;
123
+ startedAt?: string | undefined;
124
+ status: SyncStatus;
125
+ total?: number | undefined;
126
+ }
127
+ /** Comparison operators for a {@link QueryFilter}. */
128
+ export type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in';
129
+ /** A single structured filter applied as an indexed `WHERE` clause. */
130
+ export interface QueryFilter {
131
+ /** Column to filter — must be a declared column. */
132
+ column: string;
133
+ op: FilterOp;
134
+ /** Scalar for scalar ops; array for `in`. */
135
+ value: SqlValue | SqlValue[];
136
+ }
137
+ /** Sort directive: by a declared column, or `'relevance'` (FTS bm25, requires `match`). */
138
+ export type QuerySort = {
139
+ column: string;
140
+ direction: 'asc' | 'desc';
141
+ } | 'relevance';
142
+ /** Options for the generic {@link MirrorStore.query}. */
143
+ export interface QueryOptions {
144
+ /** Structured filters, AND-combined. */
145
+ filters?: QueryFilter[];
146
+ limit: number;
147
+ /** FTS5 `MATCH` expression. The server translates its own query syntax to FTS5. */
148
+ match?: string | undefined;
149
+ offset: number;
150
+ /** Sort directive. Defaults to insertion order (rowid) when omitted. */
151
+ sort?: QuerySort | undefined;
152
+ }
153
+ /** Result of a {@link MirrorStore.query}. */
154
+ export interface QueryResult {
155
+ rows: MirrorRow[];
156
+ /** Total matches before `limit`/`offset`. */
157
+ total: number;
158
+ }
159
+ /** A schema migration: bump `version` and apply `up` on open when the stored version is lower. */
160
+ export interface Migration {
161
+ /** Apply the migration. Runs inside the open handle; should be idempotent. */
162
+ up(handle: SqliteHandle): void;
163
+ /** Target schema version this migration produces. */
164
+ version: number;
165
+ }
166
+ /**
167
+ * Pluggable backend contract — the embedded-store half of a mirror. The default
168
+ * implementation is `sqliteMirrorStore`; the interface leaves a clean path to
169
+ * other backends (DuckDB, Postgres) without re-architecting the runner. All
170
+ * methods lazy-open the underlying store on first call (async per the Tier-3
171
+ * convention) and are then backed by synchronous driver calls.
172
+ */
173
+ export interface MirrorStore {
174
+ /** Upsert records and delete tombstoned primary keys in one transaction. */
175
+ applyBatch(records: MirrorRow[], tombstones: string[]): Promise<void>;
176
+ /** Close the underlying store. */
177
+ close(): Promise<void>;
178
+ /** Total record count. */
179
+ count(): Promise<number>;
180
+ /** Fetch records by primary-key list, preserving input order; missing keys skipped. */
181
+ getByIds(ids: string[]): Promise<MirrorRow[]>;
182
+ /** `PRAGMA integrity_check` + `quick_check`. */
183
+ integrityCheck(): Promise<{
184
+ ok: boolean;
185
+ results: string[];
186
+ }>;
187
+ /** Generic flat query: FTS `MATCH` + indexed filters + sort + pagination. */
188
+ query(options: QueryOptions): Promise<QueryResult>;
189
+ /**
190
+ * The opened runtime-agnostic handle — the escape hatch for server-specific
191
+ * access paths (auxiliary tables, junctions, custom indexes, bespoke queries)
192
+ * that the generic `query()` does not cover.
193
+ */
194
+ raw(): Promise<SqliteHandle>;
195
+ /** Read the persisted sync state. */
196
+ readState(): Promise<SyncState>;
197
+ /** Write the persisted sync state. Durable fields (`completedAt`/`total`) are preserved when omitted. */
198
+ writeState(state: SyncState): Promise<void>;
199
+ }
200
+ /** Definition passed to {@link defineMirror}. */
201
+ export interface MirrorDefinition {
202
+ /** Logger for sync runs; defaults to the framework `logger`. */
203
+ logger?: MirrorLogger;
204
+ /** Stable name for logs and telemetry (e.g. `'arxiv-papers'`). */
205
+ name: string;
206
+ /** The backend store (e.g. from `sqliteMirrorStore({...})`). */
207
+ store: MirrorStore;
208
+ /** The ingester — the one irreducibly per-source part. */
209
+ sync: SyncGenerator;
210
+ }
211
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/mirror/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEjE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAElF,yEAAyE;AACzE,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAEjD,mDAAmD;AACnD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,MAAM,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChD;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,SAAS;IACxB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,mEAAmE;IACnE,IAAI,EAAE,QAAQ,CAAC;IACf,+EAA+E;IAC/E,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1C;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,gDAAgD;IAChD,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,iFAAiF;AACjF,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,cAAc,CAAC,QAAQ,CAAC,CAAC;AAE3E,uDAAuD;AACvD,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC,KAAK,IAAI,CAAC;AAEX,qCAAqC;AACrC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AAED,uCAAuC;AACvC,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B;;;;;OAKG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,sDAAsD;AACtD,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC;AAExE,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,QAAQ,CAAC;IACb,6CAA6C;IAC7C,KAAK,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAAC;CAC9B;AAED,2FAA2F;AAC3F,MAAM,MAAM,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,GAAG,WAAW,CAAC;AAEpF,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B;AAED,6CAA6C;AAC7C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,kGAAkG;AAClG,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,EAAE,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,UAAU,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,kCAAkC;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,0BAA0B;IAC1B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAC9C,gDAAgD;IAChD,cAAc,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC9D,6EAA6E;IAC7E,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACnD;;;;OAIG;IACH,GAAG,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7B,qCAAqC;IACrC,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,yGAAyG;IACzG,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,KAAK,EAAE,WAAW,CAAC;IACnB,0DAA0D;IAC1D,IAAI,EAAE,aAAa,CAAC;CACrB"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Public contracts for the MirrorService — the source-agnostic
3
+ * machinery for a persistent, self-refreshing local mirror of a bulk upstream
4
+ * dataset. The framework owns the store, the sync-state machine, and the runner;
5
+ * each server supplies only the ingester (`sync` generator) and the schema.
6
+ * @module services/mirror/types
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/mirror/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/mcp-ts-core",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
4
4
  "mcpName": "io.github.cyanheads/mcp-ts-core",
5
5
  "description": "Agent-native TypeScript framework for building MCP servers. Declarative definitions with auth, multi-backend storage, OpenTelemetry, and first-class support for Bun/Node/Cloudflare Workers.",
6
6
  "main": "dist/core/index.js",
@@ -79,6 +79,10 @@
79
79
  "types": "./dist/services/canvas/index.d.ts",
80
80
  "import": "./dist/services/canvas/index.js"
81
81
  },
82
+ "./mirror": {
83
+ "types": "./dist/services/mirror/index.d.ts",
84
+ "import": "./dist/services/mirror/index.js"
85
+ },
82
86
  "./utils": {
83
87
  "types": "./dist/utils/index.d.ts",
84
88
  "import": "./dist/utils/index.js"
@@ -171,7 +175,7 @@
171
175
  "devDependencies": {
172
176
  "@biomejs/biome": "2.4.16",
173
177
  "@cloudflare/vitest-pool-workers": "^0.16.10",
174
- "@cloudflare/workers-types": "^4.20260530.1",
178
+ "@cloudflare/workers-types": "4.20260531.1",
175
179
  "@duckdb/node-api": "^1.5.3-r.2",
176
180
  "@hono/otel": "^1.1.2",
177
181
  "@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
@@ -192,6 +196,7 @@
192
196
  "@types/validator": "^13.15.10",
193
197
  "@vitest/coverage-istanbul": "4.1.7",
194
198
  "@vitest/ui": "4.1.7",
199
+ "better-sqlite3": "^12.10.0",
195
200
  "bun-types": "^1.3.14",
196
201
  "chrono-node": "^2.9.1",
197
202
  "clipboardy": "^5.3.1",
@@ -292,6 +297,7 @@
292
297
  "@opentelemetry/sdk-trace-node": "^2.7.0",
293
298
  "@opentelemetry/semantic-conventions": "^1.40.0",
294
299
  "@supabase/supabase-js": "^2.103.3",
300
+ "better-sqlite3": "^12.0.0",
295
301
  "chrono-node": "^2.9.0",
296
302
  "defuddle": "^0.18.1",
297
303
  "diff": "latest",
@@ -346,6 +352,9 @@
346
352
  "@supabase/supabase-js": {
347
353
  "optional": true
348
354
  },
355
+ "better-sqlite3": {
356
+ "optional": true
357
+ },
349
358
  "chrono-node": {
350
359
  "optional": true
351
360
  },
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: api-mirror
3
+ description: >
4
+ Stand up a persistent, self-refreshing local mirror of a bulk upstream dataset with the MirrorService (@cyanheads/mcp-ts-core/mirror). Use when a server wraps a large or slow API and should query a synced local index (embedded SQLite + FTS5) instead of paginating the live API per request.
5
+ metadata:
6
+ author: cyanheads
7
+ version: "1.0"
8
+ audience: external
9
+ type: reference
10
+ ---
11
+
12
+ ## Context
13
+
14
+ The MirrorService owns the source-agnostic half of a local mirror — the embedded store, the sync-state machine, the runner — so a server supplies only the two parts that are irreducibly per-source: the **ingester** (a `sync` generator) and the **schema**. It targets the embedded-SQLite tier (~10⁴–10⁷ rows). Node/Bun only: `bun:sqlite` is built-in on Bun, `better-sqlite3` is an optional peer dependency on Node; the store is unavailable on Workers (no SQLite, no persistent filesystem).
15
+
16
+ Import from `@cyanheads/mcp-ts-core/mirror`.
17
+
18
+ ## The shape
19
+
20
+ ```ts
21
+ import { defineMirror, sqliteMirrorStore } from '@cyanheads/mcp-ts-core/mirror';
22
+
23
+ const papers = defineMirror({
24
+ name: 'arxiv-papers',
25
+ store: sqliteMirrorStore({
26
+ path: config.mirrorPath,
27
+ primaryKey: 'id',
28
+ columns: { id: 'TEXT', title: 'TEXT', authors: 'TEXT', abstract: 'TEXT', updated: 'TEXT' },
29
+ fts: ['title', 'authors', 'abstract'], // opt-in FTS5 external-content index
30
+ indexes: [{ columns: ['updated'] }],
31
+ }),
32
+ // The ingester — the one part that is always server-specific.
33
+ async *sync({ mode, cursor, checkpoint, signal }) {
34
+ for await (const page of harvestPages({ resumeFrom: cursor, since: checkpoint, signal })) {
35
+ yield {
36
+ records: page.rows, // objects keyed by declared column
37
+ tombstones: page.deletedIds, // primary-key values to delete
38
+ cursor: page.token, // volatile resume position (see below)
39
+ checkpoint: page.maxStamp, // durable high-water mark (see below)
40
+ };
41
+ }
42
+ },
43
+ });
44
+
45
+ await papers.runSync({ mode: 'init', signal: AbortSignal.timeout(3_600_000) }); // full; resumes on interrupt
46
+ await papers.runSync({ mode: 'refresh' }); // incremental
47
+ const { rows, total } = await papers.query({ match: 'transformers', limit: 10, offset: 0 });
48
+ const status = await papers.status(); // { status, ready, checkpoint, total, ... }
49
+ ```
50
+
51
+ ## cursor vs. checkpoint — the core distinction
52
+
53
+ Two resume dimensions, deliberately separate. Conflating them silently corrupts resume for token-paged sources.
54
+
55
+ | | `cursor` | `checkpoint` |
56
+ |---|---|---|
57
+ | Meaning | Volatile intra-run resume position (e.g. an OAI-PMH resumption token, a page token) | Durable incremental high-water mark (e.g. the max record datestamp) |
58
+ | Lifetime | One run; may expire; **cleared on completion** | Persists; **advances monotonically, only on success** |
59
+ | Used for | Resuming an interrupted `init` | Seeding the next `refresh` |
60
+
61
+ Why they can't merge: during a from-scratch init the records aren't ordered by the high-water field, so the max-so-far is not a valid resume position — only the cursor is. After a completed init the cursor is meaningless, but the high-water mark is the correct refresh seed. The framework persists both per page and threads the right one back into `sync()` per mode. **The checkpoint must be lexicographically monotonic** (ISO 8601 works); the runner advances the stored checkpoint only when a page's value compares greater.
62
+
63
+ ## What you own vs. what the framework owns
64
+
65
+ | Framework | Server |
66
+ |---|---|
67
+ | Cross-runtime SQLite handle, WAL + `busy_timeout` | The `sync` generator (the ingester) |
68
+ | `mirror_sync_state` + cursor/checkpoint state machine | Translating your query syntax → FTS5 `match` |
69
+ | `runSync({ init \| refresh })`, per-page persist, resume | Mapping upstream records → row objects |
70
+ | Schema gen (columns + FTS + tokenizer + triggers) | Migration *content* (the `up` functions) |
71
+ | `schema_version` + migration *runner* | Scheduling + init/refresh bootstrap (see below) |
72
+ | Generic `query()` + the raw-handle escape hatch | Server-specific access paths via the raw handle |
73
+
74
+ ## Querying
75
+
76
+ `query({ match?, filters?, sort?, limit, offset })` covers the common case:
77
+
78
+ - `match` — an FTS5 `MATCH` expression (only when the store declares `fts` columns). Translate your own query grammar to FTS5 before calling.
79
+ - `filters` — `[{ column, op, value }]`, AND-combined, over declared columns. `op` ∈ `eq|ne|gt|gte|lt|lte|in` (`in` takes an array).
80
+ - `sort` — `{ column, direction }` or `'relevance'` (FTS bm25; requires `match`). Defaults to insertion order.
81
+
82
+ For access paths the generic query can't express — junction tables for index-backed multi-value filtering, denormalized counters, bespoke `bm25` weighting — use the **raw handle**: `const db = await mirror.raw();` then run prepared statements against your own auxiliary tables (declare them via a migration). Add the auxiliary DDL in a `migrations` step; maintain it from your `sync` mapping or SQL triggers.
83
+
84
+ ## Readiness — key off the completion marker, not live status
85
+
86
+ `status().ready` is `true` once a full sync has **ever completed** (`completedAt != null`), not when `status === 'complete'`. The dataset stays transactionally queryable during a refresh, so a mirror mid-refresh — or one whose last refresh failed — is still ready and should keep serving. Gate the mirror read path on `await mirror.ready()`; fall back to the live API only when it is `false` (cold, never-completed init).
87
+
88
+ ## Scheduling and bootstrap (server-owned)
89
+
90
+ The service owns `runSync` + state; it does not schedule. Wire "self-refreshing" yourself:
91
+
92
+ - **Refresh** — register `runSync({ mode: 'refresh' })` on a cron via `schedulerService` from `@cyanheads/mcp-ts-core/utils`, inside `setup()`. Gate on transport (HTTP) when stdio operators run it out-of-band.
93
+ - **Init** — run out-of-band (a CLI script / one-shot), never on startup: a full init can take hours and must not block the server. It is idempotent and resumable — re-running after an interrupt continues from the persisted cursor.
94
+
95
+ ## Checklist
96
+
97
+ - [ ] `defineMirror({ name, store, sync })`; the server holds the instance (one per mirror)
98
+ - [ ] `sqliteMirrorStore` spec declares `primaryKey`, `columns`, and (if searching) `fts`
99
+ - [ ] `sync` yields `{ records, tombstones?, cursor?, checkpoint? }` per page; checkpoint is lexicographically monotonic
100
+ - [ ] Read path gated on `await mirror.ready()` with a live fallback when not ready
101
+ - [ ] `better-sqlite3` added as a peer dependency for Node deployments; mirror disabled on Workers
102
+ - [ ] Refresh wired via `schedulerService` in `setup()`; init runs out-of-band
103
+ - [ ] `bun run devcheck` passes