@jbarket/loomai 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/LICENSE +661 -0
  3. package/README.md +702 -0
  4. package/assets/demo.cast +396 -0
  5. package/assets/skill/SKILL.md +192 -0
  6. package/dist/backends/embedding-runtime.d.ts +39 -0
  7. package/dist/backends/embedding-runtime.js +265 -0
  8. package/dist/backends/episodes.d.ts +36 -0
  9. package/dist/backends/episodes.js +126 -0
  10. package/dist/backends/fastembed.d.ts +18 -0
  11. package/dist/backends/fastembed.js +95 -0
  12. package/dist/backends/glob.d.ts +10 -0
  13. package/dist/backends/glob.js +16 -0
  14. package/dist/backends/index.d.ts +6 -0
  15. package/dist/backends/index.js +78 -0
  16. package/dist/backends/migrations.d.ts +38 -0
  17. package/dist/backends/migrations.js +157 -0
  18. package/dist/backends/mmr.d.ts +45 -0
  19. package/dist/backends/mmr.js +82 -0
  20. package/dist/backends/proposals.d.ts +65 -0
  21. package/dist/backends/proposals.js +182 -0
  22. package/dist/backends/recall-log.d.ts +42 -0
  23. package/dist/backends/recall-log.js +143 -0
  24. package/dist/backends/retry.d.ts +29 -0
  25. package/dist/backends/retry.js +60 -0
  26. package/dist/backends/salience.d.ts +57 -0
  27. package/dist/backends/salience.js +186 -0
  28. package/dist/backends/sqlite-knowledge.d.ts +37 -0
  29. package/dist/backends/sqlite-knowledge.js +788 -0
  30. package/dist/backends/sqlite-vec.d.ts +64 -0
  31. package/dist/backends/sqlite-vec.js +737 -0
  32. package/dist/backends/ttl.d.ts +24 -0
  33. package/dist/backends/ttl.js +50 -0
  34. package/dist/backends/types.d.ts +562 -0
  35. package/dist/backends/types.js +8 -0
  36. package/dist/blocks/harness.d.ts +45 -0
  37. package/dist/blocks/harness.js +142 -0
  38. package/dist/blocks/model.d.ts +4 -0
  39. package/dist/blocks/model.js +56 -0
  40. package/dist/blocks/types.d.ts +40 -0
  41. package/dist/blocks/types.js +34 -0
  42. package/dist/categories.d.ts +16 -0
  43. package/dist/categories.js +29 -0
  44. package/dist/cli/args.d.ts +21 -0
  45. package/dist/cli/args.js +47 -0
  46. package/dist/cli/bootstrap.d.ts +2 -0
  47. package/dist/cli/bootstrap.js +214 -0
  48. package/dist/cli/doctor.d.ts +2 -0
  49. package/dist/cli/doctor.js +206 -0
  50. package/dist/cli/forget.d.ts +2 -0
  51. package/dist/cli/forget.js +97 -0
  52. package/dist/cli/harness.d.ts +2 -0
  53. package/dist/cli/harness.js +91 -0
  54. package/dist/cli/index.d.ts +2 -0
  55. package/dist/cli/index.js +127 -0
  56. package/dist/cli/inject.d.ts +2 -0
  57. package/dist/cli/inject.js +298 -0
  58. package/dist/cli/install.d.ts +2 -0
  59. package/dist/cli/install.js +137 -0
  60. package/dist/cli/io.d.ts +14 -0
  61. package/dist/cli/io.js +63 -0
  62. package/dist/cli/knowledge.d.ts +2 -0
  63. package/dist/cli/knowledge.js +216 -0
  64. package/dist/cli/memory.d.ts +2 -0
  65. package/dist/cli/memory.js +608 -0
  66. package/dist/cli/migrate.d.ts +2 -0
  67. package/dist/cli/migrate.js +116 -0
  68. package/dist/cli/recall.d.ts +2 -0
  69. package/dist/cli/recall.js +91 -0
  70. package/dist/cli/remember.d.ts +2 -0
  71. package/dist/cli/remember.js +111 -0
  72. package/dist/cli/serve.d.ts +2 -0
  73. package/dist/cli/serve.js +71 -0
  74. package/dist/cli/subcommands.d.ts +7 -0
  75. package/dist/cli/subcommands.js +11 -0
  76. package/dist/cli/tui/multi-select.d.ts +53 -0
  77. package/dist/cli/tui/multi-select.js +116 -0
  78. package/dist/cli/update-identity.d.ts +2 -0
  79. package/dist/cli/update-identity.js +90 -0
  80. package/dist/cli/update.d.ts +2 -0
  81. package/dist/cli/update.js +87 -0
  82. package/dist/cli/wake.d.ts +2 -0
  83. package/dist/cli/wake.js +53 -0
  84. package/dist/clients.d.ts +8 -0
  85. package/dist/clients.js +63 -0
  86. package/dist/config.d.ts +59 -0
  87. package/dist/config.js +160 -0
  88. package/dist/index.d.ts +15 -0
  89. package/dist/index.js +75 -0
  90. package/dist/injection/harnesses.d.ts +17 -0
  91. package/dist/injection/harnesses.js +51 -0
  92. package/dist/injection/render.d.ts +17 -0
  93. package/dist/injection/render.js +50 -0
  94. package/dist/injection/writer.d.ts +25 -0
  95. package/dist/injection/writer.js +118 -0
  96. package/dist/install/harnesses.d.ts +26 -0
  97. package/dist/install/harnesses.js +90 -0
  98. package/dist/install/names.d.ts +16 -0
  99. package/dist/install/names.js +36 -0
  100. package/dist/install/render.d.ts +18 -0
  101. package/dist/install/render.js +42 -0
  102. package/dist/install/skill-source.d.ts +1 -0
  103. package/dist/install/skill-source.js +11 -0
  104. package/dist/path-safety.d.ts +19 -0
  105. package/dist/path-safety.js +62 -0
  106. package/dist/server.d.ts +8 -0
  107. package/dist/server.js +571 -0
  108. package/dist/tools/append-maintenance-log.d.ts +52 -0
  109. package/dist/tools/append-maintenance-log.js +103 -0
  110. package/dist/tools/archive.d.ts +2 -0
  111. package/dist/tools/archive.js +21 -0
  112. package/dist/tools/bootstrap.d.ts +11 -0
  113. package/dist/tools/bootstrap.js +161 -0
  114. package/dist/tools/dossier.d.ts +1 -0
  115. package/dist/tools/dossier.js +102 -0
  116. package/dist/tools/eurorack-import.d.ts +47 -0
  117. package/dist/tools/eurorack-import.js +199 -0
  118. package/dist/tools/find-similar.d.ts +3 -0
  119. package/dist/tools/find-similar.js +26 -0
  120. package/dist/tools/forget.d.ts +6 -0
  121. package/dist/tools/forget.js +70 -0
  122. package/dist/tools/harness.d.ts +18 -0
  123. package/dist/tools/harness.js +54 -0
  124. package/dist/tools/identity-scaffold.d.ts +22 -0
  125. package/dist/tools/identity-scaffold.js +142 -0
  126. package/dist/tools/identity.d.ts +4 -0
  127. package/dist/tools/identity.js +226 -0
  128. package/dist/tools/knowledge-archive.d.ts +5 -0
  129. package/dist/tools/knowledge-archive.js +24 -0
  130. package/dist/tools/knowledge-history.d.ts +6 -0
  131. package/dist/tools/knowledge-history.js +55 -0
  132. package/dist/tools/knowledge-maintain.d.ts +9 -0
  133. package/dist/tools/knowledge-maintain.js +113 -0
  134. package/dist/tools/knowledge-merge.d.ts +8 -0
  135. package/dist/tools/knowledge-merge.js +40 -0
  136. package/dist/tools/knowledge-move.d.ts +10 -0
  137. package/dist/tools/knowledge-move.js +48 -0
  138. package/dist/tools/knowledge-purge.d.ts +5 -0
  139. package/dist/tools/knowledge-purge.js +20 -0
  140. package/dist/tools/knowledge-recall.d.ts +20 -0
  141. package/dist/tools/knowledge-recall.js +142 -0
  142. package/dist/tools/knowledge-restore.d.ts +4 -0
  143. package/dist/tools/knowledge-restore.js +24 -0
  144. package/dist/tools/knowledge-supersede.d.ts +6 -0
  145. package/dist/tools/knowledge-supersede.js +36 -0
  146. package/dist/tools/knowledge-verify.d.ts +8 -0
  147. package/dist/tools/knowledge-verify.js +39 -0
  148. package/dist/tools/knowledge-write.d.ts +25 -0
  149. package/dist/tools/knowledge-write.js +87 -0
  150. package/dist/tools/memory-audit.d.ts +3 -0
  151. package/dist/tools/memory-audit.js +47 -0
  152. package/dist/tools/memory-list.d.ts +4 -0
  153. package/dist/tools/memory-list.js +23 -0
  154. package/dist/tools/prune.d.ts +4 -0
  155. package/dist/tools/prune.js +28 -0
  156. package/dist/tools/recall.d.ts +4 -0
  157. package/dist/tools/recall.js +24 -0
  158. package/dist/tools/remember.d.ts +10 -0
  159. package/dist/tools/remember.js +43 -0
  160. package/dist/tools/restore.d.ts +2 -0
  161. package/dist/tools/restore.js +20 -0
  162. package/dist/tools/update-identity.d.ts +34 -0
  163. package/dist/tools/update-identity.js +194 -0
  164. package/dist/tools/update.d.ts +2 -0
  165. package/dist/tools/update.js +16 -0
  166. package/dist/transport/guards.d.ts +35 -0
  167. package/dist/transport/guards.js +107 -0
  168. package/dist/transport/http-server.d.ts +17 -0
  169. package/dist/transport/http-server.js +198 -0
  170. package/dist/transport/mcp-app-hello.d.ts +3 -0
  171. package/dist/transport/mcp-app-hello.js +66 -0
  172. package/dist/widgets/display-modes.html +131 -0
  173. package/dist/widgets/hello.html +138 -0
  174. package/dist/widgets/proposals.html +131 -0
  175. package/dist/widgets/top-of-mind.html +131 -0
  176. package/package.json +73 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * TTL parsing and expiration utilities for memory aging.
3
+ *
4
+ * Supported formats:
5
+ * "7d" — 7 days
6
+ * "30d" — 30 days
7
+ * "24h" — 24 hours
8
+ * "permanent" — never expires
9
+ */
10
+ /**
11
+ * Parse a TTL string into milliseconds.
12
+ * Returns null for "permanent" or undefined input.
13
+ * Throws on invalid format.
14
+ */
15
+ export declare function parseTTL(ttl: string | undefined): number | null;
16
+ /**
17
+ * Compute an ISO expiration timestamp from a created timestamp and TTL.
18
+ * Returns null if TTL is permanent or undefined.
19
+ */
20
+ export declare function computeExpiresAt(created: string, ttl: string | undefined): string | null;
21
+ /**
22
+ * Check if a memory has expired based on its expiresAt timestamp.
23
+ */
24
+ export declare function isExpired(expiresAt: string | null | undefined, now?: Date): boolean;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * TTL parsing and expiration utilities for memory aging.
3
+ *
4
+ * Supported formats:
5
+ * "7d" — 7 days
6
+ * "30d" — 30 days
7
+ * "24h" — 24 hours
8
+ * "permanent" — never expires
9
+ */
10
+ const DURATION_RE = /^(\d+)(d|h)$/;
11
+ /**
12
+ * Parse a TTL string into milliseconds.
13
+ * Returns null for "permanent" or undefined input.
14
+ * Throws on invalid format.
15
+ */
16
+ export function parseTTL(ttl) {
17
+ if (!ttl || ttl === 'permanent')
18
+ return null;
19
+ const match = ttl.match(DURATION_RE);
20
+ if (!match) {
21
+ throw new Error(`Invalid TTL format: "${ttl}". Use "7d", "24h", or "permanent".`);
22
+ }
23
+ const amount = parseInt(match[1], 10);
24
+ const unit = match[2];
25
+ switch (unit) {
26
+ case 'd': return amount * 24 * 60 * 60 * 1000;
27
+ case 'h': return amount * 60 * 60 * 1000;
28
+ default: throw new Error(`Unknown TTL unit: "${unit}"`);
29
+ }
30
+ }
31
+ /**
32
+ * Compute an ISO expiration timestamp from a created timestamp and TTL.
33
+ * Returns null if TTL is permanent or undefined.
34
+ */
35
+ export function computeExpiresAt(created, ttl) {
36
+ const ms = parseTTL(ttl);
37
+ if (ms === null)
38
+ return null;
39
+ const expiresAt = new Date(new Date(created).getTime() + ms);
40
+ return expiresAt.toISOString();
41
+ }
42
+ /**
43
+ * Check if a memory has expired based on its expiresAt timestamp.
44
+ */
45
+ export function isExpired(expiresAt, now) {
46
+ if (!expiresAt)
47
+ return false;
48
+ const expiry = new Date(expiresAt);
49
+ return (now ?? new Date()) >= expiry;
50
+ }
@@ -0,0 +1,562 @@
1
+ /**
2
+ * Memory backend types — shared across all backend implementations.
3
+ *
4
+ * These types define the contract between the MCP tool layer and the
5
+ * storage backend. Any backend that implements MemoryBackend can be
6
+ * swapped in; v0.3.1 ships SqliteVecBackend as the single opinion.
7
+ */
8
+ export interface MemoryInput {
9
+ category: string;
10
+ title: string;
11
+ content: string;
12
+ project?: string;
13
+ metadata?: Record<string, unknown>;
14
+ /** Optional time-to-live. Parsed durations like "7d", "30d", or "permanent". */
15
+ ttl?: string;
16
+ }
17
+ export interface MemoryRef {
18
+ ref: string;
19
+ category: string;
20
+ filename: string;
21
+ title: string;
22
+ }
23
+ export interface RecallInput {
24
+ query: string;
25
+ category?: string;
26
+ project?: string;
27
+ limit?: number;
28
+ /**
29
+ * MMR diversity, 0..1 (default 0.3, i.e. λ = 0.7). Re-ranks the candidate
30
+ * pool so near-duplicate memories don't crowd out different ones. 0 keeps
31
+ * the pure relevance ranking.
32
+ */
33
+ diversity?: number;
34
+ }
35
+ /**
36
+ * One line of the recall observation log (`<contextDir>/telemetry/recall.jsonl`).
37
+ * Local-only: a file in the agent's own context dir, never sent anywhere.
38
+ */
39
+ export interface RecallObservation {
40
+ /** ISO timestamp of the search. */
41
+ ts: string;
42
+ tool: 'recall' | 'knowledge_recall';
43
+ /** The query, truncated to 120 chars on write. */
44
+ query: string;
45
+ category?: string;
46
+ project?: string;
47
+ limit: number;
48
+ /** MMR diversity that was applied (0 = plain relevance ranking). */
49
+ diversity: number;
50
+ /** Candidate pool size after filters, before MMR. */
51
+ candidates: number;
52
+ returned: number;
53
+ /** Relevance of the best returned match, null on a miss. */
54
+ topScore: number | null;
55
+ /** Relevance floor applied, if any (recall currently applies none). */
56
+ threshold: number | null;
57
+ /** Top-relevance candidates displaced by MMR. */
58
+ diversityDrops: number;
59
+ latencyMs: number;
60
+ hit: boolean;
61
+ }
62
+ export interface MemoryMatch {
63
+ path: string;
64
+ title: string;
65
+ category: string;
66
+ project?: string;
67
+ created: string;
68
+ content: string;
69
+ relevance: number;
70
+ /** ISO timestamp of last recall hit, if tracked */
71
+ lastAccessed?: string;
72
+ /** TTL value if set (e.g. "7d", "permanent") */
73
+ ttl?: string;
74
+ /** ISO timestamp when this memory expires, if TTL is set */
75
+ expiresAt?: string;
76
+ }
77
+ export interface ForgetInput {
78
+ /** Direct reference (category/filename) for single deletion */
79
+ ref?: string;
80
+ /** Find by category + title for single deletion */
81
+ category?: string;
82
+ title?: string;
83
+ /** Bulk: delete all memories in this project */
84
+ project?: string;
85
+ /** Bulk: delete memories whose title matches this pattern.
86
+ * Supports glob-style `*` wildcards: "Forgejo sweep*" matches any title
87
+ * starting with "Forgejo sweep". Requires category or project as a scope guard. */
88
+ title_pattern?: string;
89
+ }
90
+ export interface UpdateInput {
91
+ /** Direct reference (category/filename) from remember's return value */
92
+ ref?: string;
93
+ /** Alternative: find by category + title */
94
+ category?: string;
95
+ title?: string;
96
+ /** New content (replaces body, preserves frontmatter fields unless overridden) */
97
+ content?: string;
98
+ /** Metadata fields to add or update */
99
+ metadata?: Record<string, unknown>;
100
+ }
101
+ export interface ForgetResult {
102
+ /** Refs that were successfully deleted */
103
+ deleted: string[];
104
+ }
105
+ export interface ArchiveInput {
106
+ /** Direct reference (category/filename) for single archive */
107
+ ref?: string;
108
+ /** Find by category + title for single archive */
109
+ category?: string;
110
+ title?: string;
111
+ /** Tombstone note: who/why retired. Stored alongside the original body. */
112
+ note?: string;
113
+ }
114
+ export interface ArchiveResult {
115
+ /** Refs that were successfully archived (soft-retired) */
116
+ archived: string[];
117
+ }
118
+ export interface RestoreInput {
119
+ /** Direct reference (category/filename) to restore */
120
+ ref?: string;
121
+ /** Find by category + title */
122
+ category?: string;
123
+ title?: string;
124
+ }
125
+ export interface RestoreResult {
126
+ /** Refs that were successfully restored to the active set */
127
+ restored: string[];
128
+ }
129
+ export interface UpdateResult {
130
+ /** Whether the update was applied */
131
+ updated: boolean;
132
+ /** The ref of the updated memory (when found) */
133
+ ref?: string;
134
+ /** Id of the body snapshot taken before content was overwritten (if any). */
135
+ snapshotId?: number;
136
+ }
137
+ /** Revision listing entry — metadata only, no body payload. */
138
+ export interface MemoryRevisionMeta {
139
+ id: number;
140
+ memory_id: number;
141
+ /** Ref the memory had when the snapshot was taken. */
142
+ ref: string;
143
+ /** What displaced this body: 'update' or 'revision-restore'. */
144
+ op: string;
145
+ replaced_at: string;
146
+ content_length: number;
147
+ }
148
+ export interface MemoryRevision extends Omit<MemoryRevisionMeta, 'content_length'> {
149
+ content: string;
150
+ }
151
+ export interface MemoryRevisionRestoreInput {
152
+ ref: string;
153
+ revision_id: number;
154
+ }
155
+ export interface MemoryRevisionRestoreResult {
156
+ ref: string;
157
+ revision_id: number;
158
+ restored: boolean;
159
+ /** Id of the snapshot taken of the body that was just displaced. */
160
+ snapshot_id: number;
161
+ }
162
+ export interface MemorySupersededInput {
163
+ /** Ref of the memory being retired (the loser / stale version). */
164
+ old_ref: string;
165
+ /** Ref of the memory replacing it (the canonical version). */
166
+ new_ref: string;
167
+ /** Optional note explaining why this supersession happened. */
168
+ note?: string;
169
+ }
170
+ export interface MemorySupersededResult {
171
+ old_ref: string;
172
+ new_ref: string;
173
+ /** True when old_ref was archived by this call; false if already archived. */
174
+ archived: boolean;
175
+ }
176
+ export interface PruneResult {
177
+ /** Memories that were deleted because their TTL expired */
178
+ expired: string[];
179
+ /** Memories that haven't been accessed within the stale threshold */
180
+ stale: string[];
181
+ }
182
+ export interface ListInput {
183
+ category?: string;
184
+ project?: string;
185
+ limit?: number;
186
+ }
187
+ export interface MemoryEntry {
188
+ ref: string;
189
+ title: string;
190
+ category: string;
191
+ project?: string;
192
+ created: string;
193
+ }
194
+ export interface FindSimilarInput {
195
+ /** Anchor on an existing memory's embedding. Exactly one of `ref`/`text`. */
196
+ ref?: string;
197
+ /** Or anchor on fresh text — embedded on the fly. */
198
+ text?: string;
199
+ /** Max neighbours to return (default 10). */
200
+ limit?: number;
201
+ /** Restrict candidates to a category. */
202
+ category?: string;
203
+ /** Restrict candidates to a project. */
204
+ project?: string;
205
+ /** Filter out matches below this cosine similarity (0..1). */
206
+ minRelevance?: number;
207
+ }
208
+ export interface DuplicatePair {
209
+ a: {
210
+ ref: string;
211
+ title: string;
212
+ };
213
+ b: {
214
+ ref: string;
215
+ title: string;
216
+ };
217
+ relevance: number;
218
+ }
219
+ export interface AuditOptions {
220
+ /** Stale threshold in days (last_accessed/updated/created). Default 30. */
221
+ staleDays?: number;
222
+ /** Cosine-similarity floor for duplicate pairs (0..1). Default 0.85. */
223
+ similarityThreshold?: number;
224
+ /** Max duplicate pairs to surface. Default 20. */
225
+ maxDuplicates?: number;
226
+ }
227
+ export interface AuditStaleEntry {
228
+ ref: string;
229
+ title: string;
230
+ category: string;
231
+ project?: string;
232
+ /** ISO timestamp of last_accessed, falling back to updated, then created. */
233
+ lastTouch: string;
234
+ }
235
+ export interface AuditReport {
236
+ totalMemories: number;
237
+ byCategory: Record<string, number>;
238
+ /** Memories not touched within `staleDays`, excluding TTL=permanent. */
239
+ stale: AuditStaleEntry[];
240
+ /** Pairs of memories whose vector similarity ≥ `similarityThreshold`. */
241
+ duplicates: DuplicatePair[];
242
+ /** Memories whose TTL has expired (would be removed by `prune`). */
243
+ expired: string[];
244
+ }
245
+ export interface MemoryBackend {
246
+ remember(input: MemoryInput): Promise<MemoryRef>;
247
+ recall(input: RecallInput): Promise<MemoryMatch[]>;
248
+ forget(input: ForgetInput): Promise<ForgetResult>;
249
+ update(input: UpdateInput): Promise<UpdateResult>;
250
+ /** Remove expired memories and report stale ones. */
251
+ prune(options?: {
252
+ dryRun?: boolean;
253
+ staleDays?: number;
254
+ }): Promise<PruneResult>;
255
+ list(input: ListInput): Promise<MemoryEntry[]>;
256
+ /** Surface memories near (in embedding space) an existing ref or fresh text. */
257
+ findSimilar(input: FindSimilarInput): Promise<MemoryMatch[]>;
258
+ /** One-shot health report: counts, stale, duplicates, expired. Read-only. */
259
+ audit(options?: AuditOptions): Promise<AuditReport>;
260
+ /** Soft-retire a memory: archive with tombstone instead of hard delete. */
261
+ archive(input: ArchiveInput): Promise<ArchiveResult>;
262
+ /** Restore a previously archived memory to the active set. */
263
+ restore(input: RestoreInput): Promise<RestoreResult>;
264
+ /** List body snapshots for a memory (metadata only, newest-first). */
265
+ listRevisions(ref: string): Promise<MemoryRevisionMeta[]>;
266
+ /** Fetch a single body snapshot including its content. */
267
+ getRevision(revisionId: number): Promise<MemoryRevision | null>;
268
+ /** Roll back a memory to a prior body snapshot. Snapshots the current body first. */
269
+ restoreRevision(input: MemoryRevisionRestoreInput): Promise<MemoryRevisionRestoreResult>;
270
+ /** Archive old_ref with a tombstone and record that new_ref supersedes it. */
271
+ supersede(input: MemorySupersededInput): Promise<MemorySupersededResult>;
272
+ /** Release the underlying store handle. Cached backends evict on close. */
273
+ close(): void;
274
+ }
275
+ export interface EmbeddingProvider {
276
+ embed(text: string): Promise<number[]>;
277
+ embedBatch(texts: string[]): Promise<number[][]>;
278
+ /** Optional: query-optimized embedding for search (BGE-family models).
279
+ * Falls back to embed() when absent. */
280
+ embedQuery?(text: string): Promise<number[]>;
281
+ readonly dimensions: number;
282
+ }
283
+ export interface KnowledgePageInput {
284
+ slug: string;
285
+ title: string;
286
+ domain: string;
287
+ body: string;
288
+ sourcing?: 'sourced' | 'provisional' | 'internal';
289
+ provenance?: string;
290
+ /** ISO timestamp the page's claims were last verified as true. Defaults to write time. */
291
+ verified_at?: string;
292
+ /** The version/date the claims are valid as-of, e.g. "Syntakt OS 1.21" or "as of 2026-05". */
293
+ freshness_anchor?: string;
294
+ citations?: KnowledgeCitationInput[];
295
+ /**
296
+ * How `body` combines with an existing page on upsert.
297
+ * 'replace' (default) overwrites the stored body; 'append' adds the new
298
+ * body after the existing one separated by a blank line. Ignored on create.
299
+ */
300
+ bodyMode?: 'replace' | 'append';
301
+ /**
302
+ * For ours/ class pages: who created or last owned this artifact
303
+ * (e.g. "art", "jonathan"). Preserved across upserts when omitted.
304
+ */
305
+ created_by?: string;
306
+ /**
307
+ * For ours/ class pages: artifact version or revision tag
308
+ * (e.g. "v2", "2026-08-19", "t-81"). Preserved across upserts when omitted.
309
+ */
310
+ version?: string;
311
+ }
312
+ export interface KnowledgeCitationInput {
313
+ claim: string;
314
+ source_kind: 'web' | 'loom_memory' | 'conversation' | 'repo';
315
+ source_locator?: string;
316
+ excerpt: string;
317
+ }
318
+ export interface KnowledgePage {
319
+ id: number;
320
+ uuid: string;
321
+ slug: string;
322
+ title: string;
323
+ domain: string;
324
+ body: string;
325
+ sourcing: string;
326
+ provenance: string | null;
327
+ status: string;
328
+ tombstone_note?: string | null;
329
+ created: string;
330
+ updated: string | null;
331
+ last_accessed: string | null;
332
+ hit_count: number;
333
+ /** ISO timestamp the page's claims were last verified as true. */
334
+ verified_at?: string | null;
335
+ /** The version/date the claims are valid as-of, e.g. "Syntakt OS 1.21". */
336
+ freshness_anchor?: string | null;
337
+ /** ours/ class: who created or last owned this artifact (e.g. "art", "jonathan"). */
338
+ created_by?: string | null;
339
+ /** ours/ class: artifact version or revision tag (e.g. "v2", "2026-08-19"). */
340
+ version?: string | null;
341
+ }
342
+ export interface KnowledgeCitation {
343
+ id: number;
344
+ page_id: number;
345
+ claim: string;
346
+ source_kind: string;
347
+ source_locator: string | null;
348
+ excerpt: string;
349
+ retrieved_at: string;
350
+ created: string;
351
+ }
352
+ export interface KnowledgePageWithCitations extends KnowledgePage {
353
+ citations: KnowledgeCitation[];
354
+ }
355
+ export interface KnowledgeQueryInput {
356
+ query?: string;
357
+ domain?: string;
358
+ excludeStatus?: string;
359
+ limit?: number;
360
+ /**
361
+ * When false, queryPages does NOT stamp last_accessed / increment
362
+ * hit_count on the returned pages. Index-style browsing must pass false —
363
+ * hit_count is the Phase-4 expansion-engine signal and means "this page
364
+ * was actually read", not "this page appeared in a listing".
365
+ */
366
+ stampAccess?: boolean;
367
+ /**
368
+ * Stale-first ordering for the verification engine: verified_at ASC with
369
+ * never-verified pages first. Overrides the default hit-count ordering.
370
+ */
371
+ sortByVerified?: boolean;
372
+ }
373
+ export interface KnowledgePageRef {
374
+ uuid: string;
375
+ slug: string;
376
+ title: string;
377
+ }
378
+ export interface KnowledgeWriteResult extends KnowledgePageRef {
379
+ citationsAdded: number;
380
+ /** Exact-duplicate citations skipped at the write boundary. */
381
+ citationsDeduped: number;
382
+ /** True when the write created a new page rather than updating an existing one. */
383
+ created: boolean;
384
+ /** Body combine mode actually applied: 'create' for new pages, else the requested mode. */
385
+ bodyMode: 'create' | 'replace' | 'append';
386
+ }
387
+ export interface KnowledgeArchiveInput {
388
+ slug: string;
389
+ /** Optional tombstone note explaining why the page was retired. */
390
+ note?: string;
391
+ }
392
+ export interface KnowledgeArchiveResult {
393
+ slug: string;
394
+ /** true if the page was found and archived; false if not found or already archived. */
395
+ archived: boolean;
396
+ }
397
+ export interface KnowledgeRestoreInput {
398
+ slug: string;
399
+ }
400
+ export interface KnowledgeRestoreResult {
401
+ slug: string;
402
+ /** true if the page was found in the archive and restored; false otherwise. */
403
+ restored: boolean;
404
+ }
405
+ export interface KnowledgeSupersededInput {
406
+ /** Slug of the page being retired (the duplicate / loser). */
407
+ old_slug: string;
408
+ /** Slug of the canonical replacement page (must already exist). */
409
+ new_slug: string;
410
+ /** Optional note explaining the merge/supersession decision. */
411
+ note?: string;
412
+ }
413
+ export interface KnowledgeSupersessionRecord {
414
+ id: number;
415
+ old_slug: string;
416
+ new_slug: string;
417
+ note: string | null;
418
+ created: string;
419
+ }
420
+ export interface KnowledgeSupersededResult {
421
+ old_slug: string;
422
+ new_slug: string;
423
+ /** true if the old page was archived as part of this operation. */
424
+ archived: boolean;
425
+ }
426
+ export interface KnowledgeMoveInput {
427
+ slug?: string;
428
+ new_slug?: string;
429
+ new_domain?: string;
430
+ /** Write a supersessions pointer when the slug changes. Default true. */
431
+ leave_pointer?: boolean;
432
+ slugs?: string[];
433
+ from_domain_prefix?: string;
434
+ to_domain_prefix?: string;
435
+ }
436
+ export interface KnowledgeMovedPageRecord {
437
+ /** Slug after the move (= new_slug when slug changed, else original slug). */
438
+ slug: string;
439
+ /** Set only when the slug was renamed. */
440
+ old_slug?: string;
441
+ old_domain: string;
442
+ new_domain: string;
443
+ }
444
+ export interface KnowledgeMoveResult {
445
+ moved: number;
446
+ pages: KnowledgeMovedPageRecord[];
447
+ pointers_written: number;
448
+ }
449
+ export interface KnowledgeMergeInput {
450
+ /** Slugs of the pages to merge into the target (all must exist and differ from target_slug). */
451
+ source_slugs: string[];
452
+ /** Slug of the canonical target page that survives (must already exist). */
453
+ target_slug: string;
454
+ /** Optional note about this merge, stored in supersession tombstones on the losers. */
455
+ note?: string;
456
+ /** Hard-delete losers after archiving them. Losers are archived (supersession pointer written) then DELETEd from the database, cascading their citations. */
457
+ hard_delete_losers?: boolean;
458
+ /** Append loser bodies to the target body under section markers. Default false. */
459
+ append_loser_bodies?: boolean;
460
+ }
461
+ export interface KnowledgeMergeLoserRecord {
462
+ slug: string;
463
+ body: string;
464
+ }
465
+ export interface KnowledgeMergeResult {
466
+ target_slug: string;
467
+ sources_merged: number;
468
+ citations_moved: number;
469
+ citations_deduped: number;
470
+ verified_at: string | null;
471
+ losers: KnowledgeMergeLoserRecord[];
472
+ }
473
+ export interface KnowledgeVerifyInput {
474
+ /** Single-page mode: slug of the page to verify. */
475
+ slug?: string;
476
+ /** Batch mode: stamp many pages at once. Mutually exclusive with slug; no note/anchor allowed. */
477
+ slugs?: string[];
478
+ /** ISO timestamp to stamp. Defaults to now. */
479
+ verified_at?: string;
480
+ /** New freshness anchor (single-page mode only). Preserved when omitted. */
481
+ freshness_anchor?: string;
482
+ /** Optional note — appended to the body as a "## Verification — <date>" section (single-page mode only). */
483
+ note?: string;
484
+ }
485
+ export interface KnowledgeVerifyResult {
486
+ verified: number;
487
+ slugs: string[];
488
+ verified_at: string;
489
+ /** True when a note section was appended to the body. */
490
+ noted: boolean;
491
+ }
492
+ /** Revision listing entry — metadata only, no body payload. */
493
+ export interface KnowledgeRevisionMeta {
494
+ id: number;
495
+ page_id: number;
496
+ /** Slug the page had when the snapshot was taken. */
497
+ slug: string;
498
+ /** What displaced this body: 'write-replace' or 'history-restore'. */
499
+ op: string;
500
+ replaced_at: string;
501
+ body_length: number;
502
+ }
503
+ export interface KnowledgeRevision extends Omit<KnowledgeRevisionMeta, 'body_length'> {
504
+ body: string;
505
+ }
506
+ export interface KnowledgeRevisionRestoreInput {
507
+ slug: string;
508
+ revision_id: number;
509
+ }
510
+ export interface KnowledgeRevisionRestoreResult {
511
+ slug: string;
512
+ revision_id: number;
513
+ restored: boolean;
514
+ /** Id of the snapshot taken of the body that was just displaced. */
515
+ snapshot_id: number;
516
+ }
517
+ export interface KnowledgePurgeInput {
518
+ /** Explicit list of slugs to hard-delete. All must have status='archived' (archive-first guard). */
519
+ slugs: string[];
520
+ /** Must be explicitly true — required to execute an irreversible hard delete. */
521
+ confirm: true;
522
+ }
523
+ export interface KnowledgePurgeResult {
524
+ purged: number;
525
+ slugs: string[];
526
+ }
527
+ export interface KnowledgeBackend {
528
+ /** Upsert an entity page by slug; create or append. */
529
+ writePage(input: KnowledgePageInput): Promise<KnowledgeWriteResult>;
530
+ /** Get a single page by slug. opts.stampAccess marks the fetch as a real read (last_accessed / hit_count). */
531
+ getPage(slug: string, opts?: {
532
+ stampAccess?: boolean;
533
+ }): Promise<KnowledgePageWithCitations | null>;
534
+ /** List pages with optional filters. */
535
+ listPages(input?: KnowledgeQueryInput): Promise<KnowledgePageWithCitations[]>;
536
+ /** LIKE search over title, body, and domain. */
537
+ queryPages(input: KnowledgeQueryInput): Promise<KnowledgePageWithCitations[]>;
538
+ /** Add citations to an existing page by slug. */
539
+ addCitations(slug: string, citations: KnowledgeCitationInput[]): Promise<number>;
540
+ /** Soft-retire a page: set status='archived' with an optional tombstone note. */
541
+ archivePage(input: KnowledgeArchiveInput): Promise<KnowledgeArchiveResult>;
542
+ /** Restore a previously archived page back to active. */
543
+ restorePage(input: KnowledgeRestoreInput): Promise<KnowledgeRestoreResult>;
544
+ /** Mark old_slug as superseded by new_slug, archive old_slug, and record the supersession. */
545
+ supersedePage(input: KnowledgeSupersededInput): Promise<KnowledgeSupersededResult>;
546
+ /** Re-key or re-domain a page in place: change slug and/or domain atomically without creating a new row. */
547
+ movePage(input: KnowledgeMoveInput): Promise<KnowledgeMoveResult>;
548
+ /** Consolidate N source pages into one canonical target: re-parent citations (dedup), take MAX(verified_at), supersede losers. */
549
+ mergePages(input: KnowledgeMergeInput): Promise<KnowledgeMergeResult>;
550
+ /** Hard-delete archived pages and cascade their citations. Archive-first guard: rejects any non-archived slug. */
551
+ purgePages(input: KnowledgePurgeInput): Promise<KnowledgePurgeResult>;
552
+ /** Stamp verified_at / freshness_anchor without touching the body (optional appended note in single-page mode). */
553
+ verifyPages(input: KnowledgeVerifyInput): Promise<KnowledgeVerifyResult>;
554
+ /** List body snapshots for a page, newest first. Metadata only — no body payloads. */
555
+ listRevisions(slug: string): Promise<KnowledgeRevisionMeta[]>;
556
+ /** Fetch one revision including its full body. */
557
+ getRevision(revisionId: number): Promise<KnowledgeRevision | null>;
558
+ /** Restore a revision's body onto its page; the displaced body is snapshotted first. */
559
+ restoreRevision(input: KnowledgeRevisionRestoreInput): Promise<KnowledgeRevisionRestoreResult>;
560
+ /** Close the underlying SQLite connection. */
561
+ close(): void;
562
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Memory backend types — shared across all backend implementations.
3
+ *
4
+ * These types define the contract between the MCP tool layer and the
5
+ * storage backend. Any backend that implements MemoryBackend can be
6
+ * swapped in; v0.3.1 ships SqliteVecBackend as the single opinion.
7
+ */
8
+ export {};
@@ -0,0 +1,45 @@
1
+ import { type Block } from './types.js';
2
+ export declare function read(contextDir: string, key: string): Promise<Block | null>;
3
+ export declare function list(contextDir: string): Promise<string[]>;
4
+ /**
5
+ * Normalize an MCP peer's clientInfo.name to a harness-filename-shaped key.
6
+ *
7
+ * Proxies annotate the name with a trailing parenthetical, e.g.
8
+ * "claude-ai (via mcp-remote 0.1.37)" — strip that first, then lowercase and
9
+ * collapse spaces/underscores to hyphens so "Claude Desktop" and "claude_desktop"
10
+ * both land on "claude-desktop".
11
+ */
12
+ export declare function normalizePeer(peerName: string): string;
13
+ /**
14
+ * Resolve a connected MCP peer to a harness manifest key, DATA-DRIVEN from the
15
+ * manifest files on disk (not a hardcoded code map). A harness is recognized
16
+ * when the normalized peer matches either the manifest's filename OR one of the
17
+ * comma-separated values in its `answersTo` frontmatter (each normalized the
18
+ * same way). A brand-new harness is recognized by dropping a file — no code
19
+ * change. Returns undefined when nothing matches.
20
+ */
21
+ export declare function resolvePeerToHarness(contextDir: string, peerName?: string): Promise<string | undefined>;
22
+ export interface DescribeResult {
23
+ key: string;
24
+ path: string;
25
+ action: 'created' | 'overwritten';
26
+ }
27
+ /**
28
+ * Write (or overwrite) a harness manifest from a connected harness describing
29
+ * itself. Lands strictly under `<contextDir>/harnesses/<targetKey>.md` with
30
+ * stamped frontmatter (harness/version/updated) followed by the provided body.
31
+ * Re-runnable — overwrites. assertSafePathSegment guards the key so the creed
32
+ * and other tiers are structurally unreachable.
33
+ */
34
+ export declare function describeHarness(contextDir: string, targetKey: string, content: string, opts?: {
35
+ version?: string;
36
+ }): Promise<DescribeResult>;
37
+ export declare function template(key: string): string;
38
+ export interface InitResult {
39
+ name: string;
40
+ path: string;
41
+ action: 'created' | 'skipped-exists' | 'overwritten';
42
+ }
43
+ export declare function initHarness(contextDir: string, name: string, opts?: {
44
+ overwrite?: boolean;
45
+ }): Promise<InitResult>;