@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,142 @@
1
+ /**
2
+ * knowledge_recall tool — LIKE search over the knowledge store.
3
+ *
4
+ * Two detail tiers:
5
+ * 'full' — whole entity pages with citations (the synthesis unit).
6
+ * Stamps last_accessed / hit_count via queryPages — the usage
7
+ * signal the Phase-4 expansion engine depends on.
8
+ * 'index' — one compact entry per page (slug, domain, sourcing, anchor,
9
+ * snippet). Does NOT stamp access: appearing in a listing is
10
+ * not a read.
11
+ *
12
+ * Default tier: 'full' when a query is given, 'index' when browsing
13
+ * (no query). Full output is size-guarded — once the budget is spent,
14
+ * remaining pages degrade to index entries instead of blowing the
15
+ * tool-result limit. Never surfaces archived pages.
16
+ */
17
+ import { createKnowledgeBackend } from '../backends/index.js';
18
+ /**
19
+ * Character budget for full-page output. ~24k chars ≈ 6k tokens — well under
20
+ * typical MCP tool-result caps, with room for the citations and framing.
21
+ */
22
+ const FULL_OUTPUT_CHAR_BUDGET = 24_000;
23
+ /** Max characters of body shown per index entry snippet. */
24
+ const SNIPPET_LENGTH = 200;
25
+ function formatPage(page) {
26
+ const lines = [];
27
+ lines.push(`## ${page.title}`);
28
+ lines.push(`**Slug:** \`${page.slug}\` | **Domain:** ${page.domain} | **Sourcing:** ${page.sourcing}`);
29
+ if (page.sourcing === 'provisional') {
30
+ lines.push('> ⚠️ Provisional — needs independent citation to be sourced.');
31
+ }
32
+ if (page.provenance) {
33
+ lines.push(`> Provenance: ${page.provenance}`);
34
+ }
35
+ lines.push('');
36
+ lines.push(page.body);
37
+ if (page.citations.length > 0) {
38
+ lines.push('');
39
+ lines.push('**Citations:**');
40
+ for (const cit of page.citations) {
41
+ const loc = cit.source_locator ? ` (${cit.source_locator})` : '';
42
+ lines.push(`- [${cit.source_kind}${loc}] *${cit.claim}* — "${cit.excerpt}"`);
43
+ }
44
+ }
45
+ return lines.join('\n');
46
+ }
47
+ /** First non-heading, non-blank line of the body, truncated to SNIPPET_LENGTH. */
48
+ function snippet(body) {
49
+ for (const raw of body.split('\n')) {
50
+ const line = raw.trim();
51
+ if (line.length === 0 || line.startsWith('#'))
52
+ continue;
53
+ return line.length > SNIPPET_LENGTH ? `${line.slice(0, SNIPPET_LENGTH - 1)}…` : line;
54
+ }
55
+ return '';
56
+ }
57
+ function formatIndexEntry(page, showVerified = false) {
58
+ const anchor = page.freshness_anchor ? ` | anchor: ${page.freshness_anchor}` : '';
59
+ const provisional = page.sourcing === 'provisional' ? ' | ⚠️ provisional' : '';
60
+ const verified = showVerified
61
+ ? ` | verified: ${page.verified_at ? page.verified_at.slice(0, 10) : 'never'}`
62
+ : '';
63
+ const lines = [
64
+ `- **${page.title}** (\`${page.slug}\`)`,
65
+ ` ${page.domain}${anchor}${verified}${provisional} | ${page.citations.length} citation${page.citations.length === 1 ? '' : 's'} | hits: ${page.hit_count}`,
66
+ ];
67
+ const snip = snippet(page.body);
68
+ if (snip)
69
+ lines.push(` ${snip}`);
70
+ return lines.join('\n');
71
+ }
72
+ export async function knowledgeRecall(contextDir, input) {
73
+ const detail = input.detail ?? (input.query ? 'full' : 'index');
74
+ const backend = createKnowledgeBackend(contextDir);
75
+ try {
76
+ // Exact-slug lookup short-circuits the search path entirely.
77
+ if (input.slug) {
78
+ const peek = await backend.getPage(input.slug);
79
+ if (!peek) {
80
+ return `No knowledge page found for slug \`${input.slug}\`.`;
81
+ }
82
+ if (peek.status === 'archived') {
83
+ // Not stamped — an archived page surfacing in an error message is
84
+ // not a read, and a phantom hit would survive a later restore.
85
+ return (`No active knowledge page for slug \`${input.slug}\` — it is archived. ` +
86
+ `Use knowledge_restore to bring it back.`);
87
+ }
88
+ const page = await backend.getPage(input.slug, { stampAccess: true });
89
+ return `# Knowledge recall — 1 result\n\n${formatPage(page)}`;
90
+ }
91
+ const pages = await backend.queryPages({
92
+ query: input.query,
93
+ domain: input.domain,
94
+ excludeStatus: 'archived',
95
+ // Index entries are cheap — browse wide by default. Full pages are
96
+ // expensive — keep the default narrow.
97
+ limit: input.limit ?? (detail === 'index' ? 50 : 10),
98
+ // Index browsing must not inflate hit_count (expansion-engine signal).
99
+ stampAccess: detail === 'full',
100
+ sortByVerified: input.sort_by_verified,
101
+ });
102
+ if (pages.length === 0) {
103
+ const q = input.query ? `"${input.query}"` : '(no query)';
104
+ return `No knowledge pages found for ${q}.`;
105
+ }
106
+ const plural = pages.length === 1 ? '' : 's';
107
+ if (detail === 'index') {
108
+ const entries = pages.map((p) => formatIndexEntry(p, input.sort_by_verified === true));
109
+ return (`# Knowledge index — ${pages.length} page${plural}\n\n` +
110
+ `${entries.join('\n')}\n\n` +
111
+ `_Pass slug: "<slug>" (or detail: "full") to read whole pages._`);
112
+ }
113
+ // Full mode with a size guard: emit whole pages until the budget is
114
+ // spent, then degrade the remainder to index entries.
115
+ const fullParts = [];
116
+ const overflow = [];
117
+ let spent = 0;
118
+ for (const page of pages) {
119
+ const formatted = formatPage(page);
120
+ // Always include at least one full page — the first result is what
121
+ // the caller asked for even if it alone exceeds the budget.
122
+ if (fullParts.length > 0 && spent + formatted.length > FULL_OUTPUT_CHAR_BUDGET) {
123
+ overflow.push(page);
124
+ continue;
125
+ }
126
+ fullParts.push(formatted);
127
+ spent += formatted.length;
128
+ }
129
+ let out = `# Knowledge recall — ${pages.length} result${plural}\n\n${fullParts.join('\n\n---\n\n')}`;
130
+ if (overflow.length > 0) {
131
+ const entries = overflow.map((p) => formatIndexEntry(p, input.sort_by_verified === true));
132
+ out +=
133
+ `\n\n---\n\n` +
134
+ `**${overflow.length} more match${overflow.length === 1 ? '' : 'es'}** (output budget reached — shown as index; ` +
135
+ `recall by slug for full pages):\n\n${entries.join('\n')}`;
136
+ }
137
+ return out;
138
+ }
139
+ finally {
140
+ backend.close();
141
+ }
142
+ }
@@ -0,0 +1,4 @@
1
+ export interface KnowledgeRestoreToolInput {
2
+ slug: string;
3
+ }
4
+ export declare function knowledgeRestore(contextDir: string, input: KnowledgeRestoreToolInput): Promise<string>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * knowledge_restore tool — return an archived knowledge page to active status.
3
+ *
4
+ * Clears status back to 'active' and wipes the tombstone note. The page
5
+ * becomes visible to knowledge_recall and knowledge_maintain again.
6
+ * Mirror of memory_restore.
7
+ */
8
+ import { createKnowledgeBackend } from '../backends/index.js';
9
+ export async function knowledgeRestore(contextDir, input) {
10
+ const backend = createKnowledgeBackend(contextDir);
11
+ try {
12
+ const result = await backend.restorePage({ slug: input.slug });
13
+ if (!result.restored) {
14
+ return `Archived knowledge page not found: \`${input.slug}\`. It may not be archived, or the slug may be wrong.`;
15
+ }
16
+ return `Knowledge page restored: \`${input.slug}\``;
17
+ }
18
+ catch (e) {
19
+ return `Error: ${e.message}`;
20
+ }
21
+ finally {
22
+ backend.close();
23
+ }
24
+ }
@@ -0,0 +1,6 @@
1
+ export interface KnowledgeSupersededToolInput {
2
+ old_slug: string;
3
+ new_slug: string;
4
+ note?: string;
5
+ }
6
+ export declare function knowledgeSupersede(contextDir: string, input: KnowledgeSupersededToolInput): Promise<string>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * knowledge_supersede tool — retire a duplicate page in favour of the canonical one.
3
+ *
4
+ * Archives old_slug with a tombstone referencing new_slug, then records the
5
+ * supersession in the supersessions table. This is the dedup-merge primitive:
6
+ * 1. knowledge_write the canonical page (or confirm it exists)
7
+ * 2. knowledge_supersede(loser → canonical)
8
+ *
9
+ * Both pages must already exist. new_slug need not be active (archiving a
10
+ * superseded chain is allowed), but old_slug must not already be archived —
11
+ * if it is, the call returns without error but reports archived=false.
12
+ */
13
+ import { createKnowledgeBackend } from '../backends/index.js';
14
+ export async function knowledgeSupersede(contextDir, input) {
15
+ if (input.old_slug === input.new_slug) {
16
+ return 'Error: old_slug and new_slug must be different.';
17
+ }
18
+ const backend = createKnowledgeBackend(contextDir);
19
+ try {
20
+ const result = await backend.supersedePage({
21
+ old_slug: input.old_slug,
22
+ new_slug: input.new_slug,
23
+ note: input.note,
24
+ });
25
+ const archiveNote = result.archived
26
+ ? ` \`${result.old_slug}\` archived with supersession tombstone.`
27
+ : ` \`${result.old_slug}\` was already archived.`;
28
+ return (`Knowledge supersession recorded: \`${result.old_slug}\` → \`${result.new_slug}\`.${archiveNote}`);
29
+ }
30
+ catch (e) {
31
+ return `Error: ${e.message}`;
32
+ }
33
+ finally {
34
+ backend.close();
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ export interface KnowledgeVerifyToolInput {
2
+ slug?: string;
3
+ slugs?: string[];
4
+ verified_at?: string;
5
+ freshness_anchor?: string;
6
+ note?: string;
7
+ }
8
+ export declare function knowledgeVerify(contextDir: string, input: KnowledgeVerifyToolInput): Promise<string>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * knowledge_verify tool — the stamp-only verification primitive.
3
+ *
4
+ * Sets verified_at / freshness_anchor WITHOUT touching the page body. This is
5
+ * the primitive whose absence caused the 2026-06-01 incident: the verify role
6
+ * had no way to stamp a page, so it pushed its notes through knowledge_write
7
+ * and replaced 13 page bodies. An optional note appends a dated
8
+ * "## Verification" section — append-only, never replace.
9
+ */
10
+ import { createKnowledgeBackend } from '../backends/index.js';
11
+ export async function knowledgeVerify(contextDir, input) {
12
+ const backend = createKnowledgeBackend(contextDir);
13
+ try {
14
+ const result = await backend.verifyPages({
15
+ slug: input.slug,
16
+ slugs: input.slugs,
17
+ verified_at: input.verified_at,
18
+ freshness_anchor: input.freshness_anchor,
19
+ note: input.note,
20
+ });
21
+ const stamp = result.verified_at;
22
+ if (result.verified === 1) {
23
+ const extras = [];
24
+ if (input.freshness_anchor)
25
+ extras.push(`anchor → ${input.freshness_anchor}`);
26
+ if (result.noted)
27
+ extras.push('note appended');
28
+ const suffix = extras.length > 0 ? ` (${extras.join(', ')})` : '';
29
+ return `Verified \`${result.slugs[0]}\` — verified_at stamped to ${stamp}${suffix}. Body untouched${result.noted ? ' except appended note' : ''}.`;
30
+ }
31
+ return `Verified ${result.verified} page(s) — verified_at stamped to ${stamp}: ${result.slugs.map((s) => `\`${s}\``).join(', ')}. Bodies untouched.`;
32
+ }
33
+ catch (e) {
34
+ return `Error: ${e.message}`;
35
+ }
36
+ finally {
37
+ backend.close();
38
+ }
39
+ }
@@ -0,0 +1,25 @@
1
+ export interface KnowledgeWriteInput {
2
+ domain: string;
3
+ title: string;
4
+ body: string;
5
+ slug?: string;
6
+ freshness_anchor?: string;
7
+ /**
8
+ * Body combine mode when the slug already exists: 'replace' (default)
9
+ * overwrites the stored body; 'append' adds this body after the existing
10
+ * one. Citations are always appended (with exact-duplicate dedup),
11
+ * regardless of mode.
12
+ */
13
+ mode?: 'replace' | 'append';
14
+ citations: Array<{
15
+ claim: string;
16
+ source_kind: 'web' | 'loom_memory' | 'conversation' | 'repo';
17
+ source_locator?: string;
18
+ excerpt: string;
19
+ }>;
20
+ /** ours/ class: who created or last owned this artifact (e.g. "art", "jonathan"). */
21
+ created_by?: string;
22
+ /** ours/ class: artifact version or revision tag (e.g. "v2", "2026-08-19"). */
23
+ version?: string;
24
+ }
25
+ export declare function knowledgeWrite(contextDir: string, input: KnowledgeWriteInput): Promise<string>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * knowledge_write tool — upsert an entity page by slug/title.
3
+ *
4
+ * Two knowledge classes:
5
+ * world/ — facts true independent of us (default; domain = "music/gear", etc.)
6
+ * ours/ — Art-created artifacts revised-in-place (domain starts with "ours/").
7
+ * Cited to repo paths/commits, loom_memory refs, or live-system probes.
8
+ *
9
+ * Epistemic gate (§E1):
10
+ * - conversation-only citations → provisional (both classes).
11
+ * - any repo citation → internal (ours/ class; overrides the sourced gate).
12
+ * - any web citation (no repo) → sourced (world/ default).
13
+ *
14
+ * Filing test for world/ pages: knowledge is true independent of Jonathan; if
15
+ * it's about Jonathan / our work use memory or ours/ instead, not the world class.
16
+ */
17
+ import { createKnowledgeBackend } from '../backends/index.js';
18
+ function slugify(title) {
19
+ return title
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9]+/g, '-')
22
+ .replace(/^-+|-+$/g, '')
23
+ .slice(0, 128);
24
+ }
25
+ /**
26
+ * §E1: Epistemic gate.
27
+ * - conversation-only → provisional (both classes).
28
+ * - any repo citation → internal (ours/ class; repo = git path/commit/ref).
29
+ * - any web citation, no repo → sourced (world/ default).
30
+ */
31
+ function determineSourcing(citations) {
32
+ if (citations.length === 0)
33
+ return 'provisional';
34
+ if (citations.every((c) => c.source_kind === 'conversation'))
35
+ return 'provisional';
36
+ if (citations.some((c) => c.source_kind === 'repo'))
37
+ return 'internal';
38
+ return 'sourced';
39
+ }
40
+ export async function knowledgeWrite(contextDir, input) {
41
+ if (input.citations.length === 0) {
42
+ return ('Error: at least one citation is required — knowledge must be supported. ' +
43
+ 'If this is a conversation-distilled insight, pass source_kind="conversation" ' +
44
+ 'and the page will be stored provisional.');
45
+ }
46
+ const slug = input.slug ?? slugify(input.title);
47
+ if (!slug) {
48
+ return 'Error: could not derive a slug from the title. Provide an explicit slug.';
49
+ }
50
+ const sourcing = determineSourcing(input.citations);
51
+ const backend = createKnowledgeBackend(contextDir);
52
+ try {
53
+ const result = await backend.writePage({
54
+ slug,
55
+ title: input.title,
56
+ domain: input.domain,
57
+ body: input.body,
58
+ sourcing,
59
+ freshness_anchor: input.freshness_anchor,
60
+ bodyMode: input.mode,
61
+ citations: input.citations,
62
+ created_by: input.created_by,
63
+ version: input.version,
64
+ });
65
+ const sourcingNote = sourcing === 'provisional'
66
+ ? '\n> **Provisional** — sole support is conversation citations. ' +
67
+ 'Add a web or repo citation to promote to sourced/internal.'
68
+ : sourcing === 'internal'
69
+ ? '\n> **Internal** — ours/ class artifact; cited to repo/live-system sources.'
70
+ : '';
71
+ const action = result.bodyMode === 'create' ? 'created'
72
+ : result.bodyMode === 'append' ? 'updated (body appended)'
73
+ : 'updated (body replaced)';
74
+ const dedupNote = result.citationsDeduped > 0
75
+ ? ` (${result.citationsDeduped} duplicate${result.citationsDeduped === 1 ? '' : 's'} skipped)`
76
+ : '';
77
+ return (`Knowledge page ${action}: **${result.title}** (\`${result.slug}\`)\n` +
78
+ `Domain: ${input.domain} | Citations added: ${result.citationsAdded}${dedupNote} | Sourcing: ${sourcing}` +
79
+ sourcingNote);
80
+ }
81
+ catch (e) {
82
+ return `Error: ${e.message}`;
83
+ }
84
+ finally {
85
+ backend.close();
86
+ }
87
+ }
@@ -0,0 +1,3 @@
1
+ import type { AuditOptions, AuditReport } from '../backends/types.js';
2
+ export declare function formatAuditReport(report: AuditReport): string;
3
+ export declare function memoryAudit(contextDir: string, options?: AuditOptions): Promise<string>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * memory_audit tool — one-shot health report for the memory store.
3
+ * Surfaces counts, stale memories, near-duplicate pairs, and expired refs.
4
+ * Read-only: never deletes. Pair with `forget` / `update` to act on findings.
5
+ */
6
+ import { createBackend } from '../backends/index.js';
7
+ export function formatAuditReport(report) {
8
+ const lines = [];
9
+ lines.push(`# Memory audit`);
10
+ lines.push('');
11
+ lines.push(`**Total memories:** ${report.totalMemories}`);
12
+ const cats = Object.entries(report.byCategory).sort((a, b) => b[1] - a[1]);
13
+ if (cats.length > 0) {
14
+ lines.push('');
15
+ lines.push('**By category:**');
16
+ for (const [c, n] of cats)
17
+ lines.push(`- ${c}: ${n}`);
18
+ }
19
+ lines.push('');
20
+ lines.push(`**Expired (would be pruned):** ${report.expired.length}`);
21
+ for (const ref of report.expired.slice(0, 20))
22
+ lines.push(`- ${ref}`);
23
+ if (report.expired.length > 20) {
24
+ lines.push(`- … and ${report.expired.length - 20} more`);
25
+ }
26
+ lines.push('');
27
+ lines.push(`**Stale (untouched beyond threshold):** ${report.stale.length}`);
28
+ for (const s of report.stale.slice(0, 20)) {
29
+ const proj = s.project ? ` [${s.project}]` : '';
30
+ lines.push(`- ${s.ref} — *${s.title}*${proj} (last touch ${s.lastTouch.slice(0, 10)})`);
31
+ }
32
+ if (report.stale.length > 20) {
33
+ lines.push(`- … and ${report.stale.length - 20} more`);
34
+ }
35
+ lines.push('');
36
+ lines.push(`**Near-duplicate pairs:** ${report.duplicates.length}`);
37
+ for (const d of report.duplicates) {
38
+ lines.push(`- ${d.relevance.toFixed(3)} ${d.a.ref} ↔ ${d.b.ref} ` +
39
+ `(*${d.a.title}* vs *${d.b.title}*)`);
40
+ }
41
+ return lines.join('\n');
42
+ }
43
+ export async function memoryAudit(contextDir, options) {
44
+ const backend = createBackend(contextDir);
45
+ const report = await backend.audit(options);
46
+ return formatAuditReport(report);
47
+ }
@@ -0,0 +1,4 @@
1
+ import type { ListInput, MemoryEntry } from '../backends/types.js';
2
+ export declare function formatEntry(e: MemoryEntry): string;
3
+ export declare function formatEntries(entries: MemoryEntry[]): string;
4
+ export declare function memoryList(contextDir: string, input: ListInput): Promise<string>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Memory list tool — browse memories without semantic search.
3
+ *
4
+ * Lists memories with optional category/project filters. Unlike recall,
5
+ * this doesn't require a search query — useful for auditing, browsing,
6
+ * and memory maintenance.
7
+ */
8
+ import { createBackend } from '../backends/index.js';
9
+ export function formatEntry(e) {
10
+ const projectTag = e.project ? ` [${e.project}]` : '';
11
+ return `- **${e.title}** — ${e.category}${projectTag} (${e.created.slice(0, 10)})\n ref: \`${e.ref}\``;
12
+ }
13
+ export function formatEntries(entries) {
14
+ if (entries.length === 0)
15
+ return 'No memories found.';
16
+ const formatted = entries.map(formatEntry);
17
+ return `Found ${entries.length} memories:\n\n${formatted.join('\n')}`;
18
+ }
19
+ export async function memoryList(contextDir, input) {
20
+ const backend = createBackend(contextDir);
21
+ const entries = await backend.list(input);
22
+ return formatEntries(entries);
23
+ }
@@ -0,0 +1,4 @@
1
+ export declare function prune(contextDir: string, options?: {
2
+ dryRun?: boolean;
3
+ staleDays?: number;
4
+ }): Promise<string>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Prune tool — removes expired memories and reports stale ones.
3
+ *
4
+ * Delegates to the configured memory backend for TTL-based cleanup.
5
+ */
6
+ import { createBackend } from '../backends/index.js';
7
+ export async function prune(contextDir, options) {
8
+ const backend = createBackend(contextDir);
9
+ const result = await backend.prune(options);
10
+ const lines = [];
11
+ if (result.expired.length > 0) {
12
+ const verb = options?.dryRun ? 'Would expire' : 'Expired';
13
+ lines.push(`**${verb} ${result.expired.length} memories (TTL elapsed):**`);
14
+ for (const ref of result.expired) {
15
+ lines.push(`- ${ref}`);
16
+ }
17
+ }
18
+ if (result.stale.length > 0) {
19
+ lines.push(`\n**${result.stale.length} stale memories (not accessed in ${options?.staleDays ?? 30}+ days):**`);
20
+ for (const ref of result.stale) {
21
+ lines.push(`- ${ref}`);
22
+ }
23
+ }
24
+ if (result.expired.length === 0 && result.stale.length === 0) {
25
+ return 'No expired or stale memories found. Memory store is healthy.';
26
+ }
27
+ return lines.join('\n');
28
+ }
@@ -0,0 +1,4 @@
1
+ import type { RecallInput, MemoryMatch } from '../backends/types.js';
2
+ export declare function formatMatchResult(m: MemoryMatch): string;
3
+ export declare function formatResults(matches: MemoryMatch[]): string;
4
+ export declare function recall(contextDir: string, input: RecallInput): Promise<string>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Recall tool — retrieves memories relevant to a query.
3
+ *
4
+ * Delegates to the configured memory backend for search, then formats
5
+ * results for display. The backend handles the actual search strategy
6
+ * (keyword, vector, etc.).
7
+ */
8
+ import { createBackend } from '../backends/index.js';
9
+ export function formatMatchResult(m) {
10
+ const projectTag = m.project ? ` [${m.project}]` : '';
11
+ return `## ${m.title}\n*${m.category}${projectTag} — ${m.created.slice(0, 10)}*\n\n${m.content}`;
12
+ }
13
+ export function formatResults(matches) {
14
+ const results = matches.map(formatMatchResult);
15
+ return `Found ${matches.length} matching memories:\n\n${results.join('\n\n---\n\n')}`;
16
+ }
17
+ export async function recall(contextDir, input) {
18
+ const backend = createBackend(contextDir);
19
+ const matches = await backend.recall(input);
20
+ if (matches.length === 0) {
21
+ return `No memories found matching "${input.query}".`;
22
+ }
23
+ return formatResults(matches);
24
+ }
@@ -0,0 +1,10 @@
1
+ import type { MemoryInput, MemoryRef } from '../backends/types.js';
2
+ /**
3
+ * Lint-on-write (c-loom-strictness §lint): validate the record BEFORE it reaches
4
+ * the backend, so a malformed write is refused with a typed reason and the store
5
+ * is left untouched — never an empty/torn memory committed then discovered. The
6
+ * knowledge wing already gates its writes (a citation is required); this brings
7
+ * the memory wing to parity. Returns a reason string when invalid, else null.
8
+ */
9
+ export declare function validateMemoryInput(input: MemoryInput): string | null;
10
+ export declare function remember(contextDir: string, input: MemoryInput): Promise<MemoryRef>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Remember tool — stores an episodic memory that persists across sessions.
3
+ *
4
+ * Delegates to the configured MemoryBackend. v0.3.1 ships a single
5
+ * backend (sqlite-vec + fastembed); the interface stays generic so
6
+ * future stacks can swap in without changing the tool contract.
7
+ */
8
+ import { createBackend } from '../backends/index.js';
9
+ import { EPISODE_CATEGORY, EPISODE_DEFAULT_TTL } from '../categories.js';
10
+ /**
11
+ * Lint-on-write (c-loom-strictness §lint): validate the record BEFORE it reaches
12
+ * the backend, so a malformed write is refused with a typed reason and the store
13
+ * is left untouched — never an empty/torn memory committed then discovered. The
14
+ * knowledge wing already gates its writes (a citation is required); this brings
15
+ * the memory wing to parity. Returns a reason string when invalid, else null.
16
+ */
17
+ export function validateMemoryInput(input) {
18
+ if (typeof input.content !== 'string' || input.content.trim() === '') {
19
+ return 'memory content is required and cannot be empty';
20
+ }
21
+ if (typeof input.title !== 'string' || input.title.trim() === '') {
22
+ return 'memory title is required and cannot be empty';
23
+ }
24
+ if (typeof input.category !== 'string' || input.category.trim() === '') {
25
+ return 'memory category is required and cannot be empty';
26
+ }
27
+ return null;
28
+ }
29
+ export async function remember(contextDir, input) {
30
+ const reason = validateMemoryInput(input);
31
+ if (reason !== null) {
32
+ // Rejected pre-commit: nothing is opened or written. The thrown reason is
33
+ // the typed rejection (dispatch wraps it into a typed envelope at the edge).
34
+ throw new Error(`Error: ${reason}`);
35
+ }
36
+ const backend = createBackend(contextDir);
37
+ // An episode is short-term by definition: without an explicit ttl it gets the
38
+ // tier default, so a body can never accidentally leave a permanent episode.
39
+ if (input.category === EPISODE_CATEGORY && !input.ttl) {
40
+ input = { ...input, ttl: EPISODE_DEFAULT_TTL };
41
+ }
42
+ return backend.remember(input);
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { RestoreInput } from '../backends/types.js';
2
+ export declare function restore(contextDir: string, input: RestoreInput): Promise<string>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Restore tool — return an archived memory to the active set.
3
+ *
4
+ * Clears the archived flag and wipes the tombstone note. The memory
5
+ * becomes visible to recall, list, audit, and findSimilar again.
6
+ */
7
+ import { createBackend } from '../backends/index.js';
8
+ export async function restore(contextDir, input) {
9
+ const isSingle = input.ref || (input.category && input.title);
10
+ if (!isSingle) {
11
+ return 'Nothing to restore. Provide a ref or category+title.';
12
+ }
13
+ const backend = createBackend(contextDir);
14
+ const result = await backend.restore(input);
15
+ if (result.restored.length === 0) {
16
+ const identifier = input.ref ?? `${input.category}/${input.title}`;
17
+ return `Archived memory not found: "${identifier}". It may not be archived, or the ref may be wrong.`;
18
+ }
19
+ return `Memory restored: ${result.restored[0]}`;
20
+ }
@@ -0,0 +1,34 @@
1
+ export interface IdentitySection {
2
+ header: string;
3
+ content: string;
4
+ startLine: number;
5
+ endLine: number;
6
+ }
7
+ /**
8
+ * Parse a markdown file into H2 sections.
9
+ * Content before the first H2 is captured as a preamble (header: '').
10
+ *
11
+ * Fence-aware: a `## ` line inside a ``` or ~~~ code fence is body content,
12
+ * not a section boundary. Closing fences must use the same character and be
13
+ * at least as long as the opener (CommonMark-style).
14
+ */
15
+ export declare function parseSections(text: string): IdentitySection[];
16
+ /**
17
+ * Rebuild a markdown file from sections.
18
+ */
19
+ export declare function rebuildMarkdown(sections: IdentitySection[]): string;
20
+ export type UpdateMode = 'replace' | 'append';
21
+ export interface UpdateIdentityInput {
22
+ file: string;
23
+ section?: string;
24
+ content?: string;
25
+ mode?: UpdateMode;
26
+ }
27
+ /**
28
+ * List sections in an identity file.
29
+ */
30
+ export declare function listSections(contextDir: string, file: string): Promise<string>;
31
+ /**
32
+ * Update or append a section in an identity file.
33
+ */
34
+ export declare function updateIdentity(contextDir: string, input: UpdateIdentityInput): Promise<string>;