claude-code-swarm 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +22 -1
  3. package/.claude-plugin/run-agent-inbox-mcp.sh +76 -0
  4. package/.claude-plugin/run-minimem-mcp.sh +98 -0
  5. package/.claude-plugin/run-opentasks-mcp.sh +65 -0
  6. package/CLAUDE.md +200 -36
  7. package/README.md +65 -0
  8. package/e2e/helpers/cleanup.mjs +17 -3
  9. package/e2e/helpers/map-mock-server.mjs +201 -25
  10. package/e2e/helpers/sidecar.mjs +222 -0
  11. package/e2e/helpers/workspace.mjs +2 -1
  12. package/e2e/tier5-sidecar-inbox.test.mjs +900 -0
  13. package/e2e/tier6-inbox-mcp.test.mjs +173 -0
  14. package/e2e/tier6-live-agent.test.mjs +759 -0
  15. package/e2e/vitest.config.e2e.mjs +1 -1
  16. package/hooks/hooks.json +15 -8
  17. package/package.json +13 -1
  18. package/references/agent-inbox/CLAUDE.md +151 -0
  19. package/references/agent-inbox/README.md +238 -0
  20. package/references/agent-inbox/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
  21. package/references/agent-inbox/docs/DESIGN.md +1156 -0
  22. package/references/agent-inbox/hooks/inbox-hook.mjs +119 -0
  23. package/references/agent-inbox/hooks/register-hook.mjs +69 -0
  24. package/references/agent-inbox/package-lock.json +3347 -0
  25. package/references/agent-inbox/package.json +58 -0
  26. package/references/agent-inbox/rules/agent-inbox.md +78 -0
  27. package/references/agent-inbox/src/federation/address.ts +61 -0
  28. package/references/agent-inbox/src/federation/connection-manager.ts +573 -0
  29. package/references/agent-inbox/src/federation/delivery-queue.ts +222 -0
  30. package/references/agent-inbox/src/federation/index.ts +6 -0
  31. package/references/agent-inbox/src/federation/routing-engine.ts +188 -0
  32. package/references/agent-inbox/src/federation/trust.ts +71 -0
  33. package/references/agent-inbox/src/index.ts +390 -0
  34. package/references/agent-inbox/src/ipc/ipc-server.ts +207 -0
  35. package/references/agent-inbox/src/jsonrpc/mail-server.ts +382 -0
  36. package/references/agent-inbox/src/map/map-client.ts +414 -0
  37. package/references/agent-inbox/src/mcp/mcp-server.ts +272 -0
  38. package/references/agent-inbox/src/mesh/delivery-bridge.ts +110 -0
  39. package/references/agent-inbox/src/mesh/mesh-connector.ts +41 -0
  40. package/references/agent-inbox/src/mesh/mesh-transport.ts +157 -0
  41. package/references/agent-inbox/src/mesh/type-mapper.ts +239 -0
  42. package/references/agent-inbox/src/push/notifier.ts +233 -0
  43. package/references/agent-inbox/src/registry/warm-registry.ts +255 -0
  44. package/references/agent-inbox/src/router/message-router.ts +175 -0
  45. package/references/agent-inbox/src/storage/interface.ts +48 -0
  46. package/references/agent-inbox/src/storage/memory.ts +145 -0
  47. package/references/agent-inbox/src/storage/sqlite.ts +671 -0
  48. package/references/agent-inbox/src/traceability/traceability.ts +183 -0
  49. package/references/agent-inbox/src/types.ts +303 -0
  50. package/references/agent-inbox/test/federation/address.test.ts +101 -0
  51. package/references/agent-inbox/test/federation/connection-manager.test.ts +546 -0
  52. package/references/agent-inbox/test/federation/delivery-queue.test.ts +159 -0
  53. package/references/agent-inbox/test/federation/integration.test.ts +857 -0
  54. package/references/agent-inbox/test/federation/routing-engine.test.ts +117 -0
  55. package/references/agent-inbox/test/federation/sdk-integration.test.ts +744 -0
  56. package/references/agent-inbox/test/federation/trust.test.ts +89 -0
  57. package/references/agent-inbox/test/ipc-jsonrpc.test.ts +113 -0
  58. package/references/agent-inbox/test/ipc-server.test.ts +197 -0
  59. package/references/agent-inbox/test/mail-server.test.ts +285 -0
  60. package/references/agent-inbox/test/map-client.test.ts +408 -0
  61. package/references/agent-inbox/test/mesh/delivery-bridge.test.ts +178 -0
  62. package/references/agent-inbox/test/mesh/e2e-mesh.test.ts +527 -0
  63. package/references/agent-inbox/test/mesh/e2e-real-meshpeer.test.ts +629 -0
  64. package/references/agent-inbox/test/mesh/federation-mesh.test.ts +269 -0
  65. package/references/agent-inbox/test/mesh/mesh-connector.test.ts +66 -0
  66. package/references/agent-inbox/test/mesh/mesh-transport.test.ts +191 -0
  67. package/references/agent-inbox/test/mesh/meshpeer-integration.test.ts +442 -0
  68. package/references/agent-inbox/test/mesh/mock-mesh.ts +125 -0
  69. package/references/agent-inbox/test/mesh/mock-meshpeer.ts +266 -0
  70. package/references/agent-inbox/test/mesh/type-mapper.test.ts +226 -0
  71. package/references/agent-inbox/test/message-router.test.ts +184 -0
  72. package/references/agent-inbox/test/push-notifier.test.ts +139 -0
  73. package/references/agent-inbox/test/registry/warm-registry.test.ts +171 -0
  74. package/references/agent-inbox/test/sqlite-prefix.test.ts +192 -0
  75. package/references/agent-inbox/test/sqlite-storage.test.ts +243 -0
  76. package/references/agent-inbox/test/storage.test.ts +196 -0
  77. package/references/agent-inbox/test/traceability.test.ts +123 -0
  78. package/references/agent-inbox/test/wake.test.ts +330 -0
  79. package/references/agent-inbox/tsconfig.json +20 -0
  80. package/references/agent-inbox/tsup.config.ts +10 -0
  81. package/references/agent-inbox/vitest.config.ts +8 -0
  82. package/references/minimem/.claude/settings.json +7 -0
  83. package/references/minimem/.sudocode/issues.jsonl +18 -0
  84. package/references/minimem/.sudocode/specs.jsonl +1 -0
  85. package/references/minimem/CLAUDE.md +329 -0
  86. package/references/minimem/README.md +565 -0
  87. package/references/minimem/claude-plugin/.claude-plugin/plugin.json +10 -0
  88. package/references/minimem/claude-plugin/.mcp.json +7 -0
  89. package/references/minimem/claude-plugin/README.md +158 -0
  90. package/references/minimem/claude-plugin/commands/recall.md +47 -0
  91. package/references/minimem/claude-plugin/commands/remember.md +41 -0
  92. package/references/minimem/claude-plugin/hooks/__tests__/hooks.test.ts +272 -0
  93. package/references/minimem/claude-plugin/hooks/hooks.json +27 -0
  94. package/references/minimem/claude-plugin/hooks/session-end.sh +86 -0
  95. package/references/minimem/claude-plugin/hooks/session-start.sh +85 -0
  96. package/references/minimem/claude-plugin/skills/memory/SKILL.md +108 -0
  97. package/references/minimem/media/banner.png +0 -0
  98. package/references/minimem/package-lock.json +5373 -0
  99. package/references/minimem/package.json +76 -0
  100. package/references/minimem/scripts/postbuild.js +49 -0
  101. package/references/minimem/src/__tests__/edge-cases.test.ts +371 -0
  102. package/references/minimem/src/__tests__/errors.test.ts +265 -0
  103. package/references/minimem/src/__tests__/helpers.ts +199 -0
  104. package/references/minimem/src/__tests__/internal.test.ts +407 -0
  105. package/references/minimem/src/__tests__/knowledge-frontmatter.test.ts +148 -0
  106. package/references/minimem/src/__tests__/knowledge.test.ts +148 -0
  107. package/references/minimem/src/__tests__/minimem.integration.test.ts +1127 -0
  108. package/references/minimem/src/__tests__/session.test.ts +190 -0
  109. package/references/minimem/src/cli/__tests__/commands.test.ts +760 -0
  110. package/references/minimem/src/cli/__tests__/contained-layout.test.ts +286 -0
  111. package/references/minimem/src/cli/commands/__tests__/conflicts.test.ts +141 -0
  112. package/references/minimem/src/cli/commands/append.ts +76 -0
  113. package/references/minimem/src/cli/commands/config.ts +262 -0
  114. package/references/minimem/src/cli/commands/conflicts.ts +415 -0
  115. package/references/minimem/src/cli/commands/daemon.ts +169 -0
  116. package/references/minimem/src/cli/commands/index.ts +12 -0
  117. package/references/minimem/src/cli/commands/init.ts +166 -0
  118. package/references/minimem/src/cli/commands/mcp.ts +221 -0
  119. package/references/minimem/src/cli/commands/push-pull.ts +213 -0
  120. package/references/minimem/src/cli/commands/search.ts +223 -0
  121. package/references/minimem/src/cli/commands/status.ts +84 -0
  122. package/references/minimem/src/cli/commands/store.ts +189 -0
  123. package/references/minimem/src/cli/commands/sync-init.ts +290 -0
  124. package/references/minimem/src/cli/commands/sync.ts +70 -0
  125. package/references/minimem/src/cli/commands/upsert.ts +197 -0
  126. package/references/minimem/src/cli/config.ts +611 -0
  127. package/references/minimem/src/cli/index.ts +299 -0
  128. package/references/minimem/src/cli/shared.ts +189 -0
  129. package/references/minimem/src/cli/sync/__tests__/central.test.ts +152 -0
  130. package/references/minimem/src/cli/sync/__tests__/conflicts.test.ts +209 -0
  131. package/references/minimem/src/cli/sync/__tests__/daemon.test.ts +118 -0
  132. package/references/minimem/src/cli/sync/__tests__/detection.test.ts +207 -0
  133. package/references/minimem/src/cli/sync/__tests__/integration.test.ts +476 -0
  134. package/references/minimem/src/cli/sync/__tests__/registry.test.ts +363 -0
  135. package/references/minimem/src/cli/sync/__tests__/state.test.ts +255 -0
  136. package/references/minimem/src/cli/sync/__tests__/validation.test.ts +193 -0
  137. package/references/minimem/src/cli/sync/__tests__/watcher.test.ts +178 -0
  138. package/references/minimem/src/cli/sync/central.ts +292 -0
  139. package/references/minimem/src/cli/sync/conflicts.ts +205 -0
  140. package/references/minimem/src/cli/sync/daemon.ts +407 -0
  141. package/references/minimem/src/cli/sync/detection.ts +138 -0
  142. package/references/minimem/src/cli/sync/index.ts +107 -0
  143. package/references/minimem/src/cli/sync/operations.ts +373 -0
  144. package/references/minimem/src/cli/sync/registry.ts +279 -0
  145. package/references/minimem/src/cli/sync/state.ts +358 -0
  146. package/references/minimem/src/cli/sync/validation.ts +206 -0
  147. package/references/minimem/src/cli/sync/watcher.ts +237 -0
  148. package/references/minimem/src/cli/version.ts +34 -0
  149. package/references/minimem/src/core/index.ts +9 -0
  150. package/references/minimem/src/core/indexer.ts +628 -0
  151. package/references/minimem/src/core/searcher.ts +221 -0
  152. package/references/minimem/src/db/schema.ts +183 -0
  153. package/references/minimem/src/db/sqlite-vec.ts +24 -0
  154. package/references/minimem/src/embeddings/__tests__/embeddings.test.ts +431 -0
  155. package/references/minimem/src/embeddings/batch-gemini.ts +392 -0
  156. package/references/minimem/src/embeddings/batch-openai.ts +409 -0
  157. package/references/minimem/src/embeddings/embeddings.ts +434 -0
  158. package/references/minimem/src/index.ts +132 -0
  159. package/references/minimem/src/internal.ts +299 -0
  160. package/references/minimem/src/minimem.ts +1291 -0
  161. package/references/minimem/src/search/__tests__/hybrid.test.ts +247 -0
  162. package/references/minimem/src/search/graph.ts +234 -0
  163. package/references/minimem/src/search/hybrid.ts +151 -0
  164. package/references/minimem/src/search/search.ts +256 -0
  165. package/references/minimem/src/server/__tests__/mcp.test.ts +347 -0
  166. package/references/minimem/src/server/__tests__/tools.test.ts +364 -0
  167. package/references/minimem/src/server/mcp.ts +326 -0
  168. package/references/minimem/src/server/tools.ts +720 -0
  169. package/references/minimem/src/session.ts +460 -0
  170. package/references/minimem/src/store/__tests__/manifest.test.ts +177 -0
  171. package/references/minimem/src/store/__tests__/materialize.test.ts +52 -0
  172. package/references/minimem/src/store/__tests__/store-graph.test.ts +228 -0
  173. package/references/minimem/src/store/index.ts +27 -0
  174. package/references/minimem/src/store/manifest.ts +203 -0
  175. package/references/minimem/src/store/materialize.ts +185 -0
  176. package/references/minimem/src/store/store-graph.ts +252 -0
  177. package/references/minimem/tsconfig.json +19 -0
  178. package/references/minimem/tsup.config.ts +26 -0
  179. package/references/minimem/vitest.config.ts +29 -0
  180. package/references/openteams/src/cli/generate.ts +23 -1
  181. package/references/openteams/src/generators/agent-prompt-generator.test.ts +94 -0
  182. package/references/openteams/src/generators/agent-prompt-generator.ts +42 -13
  183. package/references/openteams/src/generators/package-generator.ts +9 -1
  184. package/references/openteams/src/generators/skill-generator.test.ts +28 -0
  185. package/references/openteams/src/generators/skill-generator.ts +10 -4
  186. package/references/skill-tree/.claude/settings.json +6 -0
  187. package/references/skill-tree/.sudocode/issues.jsonl +19 -0
  188. package/references/skill-tree/.sudocode/specs.jsonl +3 -0
  189. package/references/skill-tree/CLAUDE.md +132 -0
  190. package/references/skill-tree/README.md +396 -0
  191. package/references/skill-tree/docs/GAPS_v1.md +221 -0
  192. package/references/skill-tree/docs/INTEGRATION_PLAN.md +467 -0
  193. package/references/skill-tree/docs/TODOS.md +91 -0
  194. package/references/skill-tree/docs/anthropic_skill_guide.md +1364 -0
  195. package/references/skill-tree/docs/design/federated-skill-trees.md +524 -0
  196. package/references/skill-tree/docs/design/multi-agent-sync.md +759 -0
  197. package/references/skill-tree/docs/scraper/BRAINSTORM.md +583 -0
  198. package/references/skill-tree/docs/scraper/POC_PLAN.md +420 -0
  199. package/references/skill-tree/docs/scraper/README.md +170 -0
  200. package/references/skill-tree/examples/basic-usage.ts +157 -0
  201. package/references/skill-tree/package-lock.json +1852 -0
  202. package/references/skill-tree/package.json +66 -0
  203. package/references/skill-tree/plan.md +78 -0
  204. package/references/skill-tree/scraper/README.md +123 -0
  205. package/references/skill-tree/scraper/docs/DESIGN.md +683 -0
  206. package/references/skill-tree/scraper/docs/PLAN.md +336 -0
  207. package/references/skill-tree/scraper/drizzle.config.ts +10 -0
  208. package/references/skill-tree/scraper/package-lock.json +6329 -0
  209. package/references/skill-tree/scraper/package.json +68 -0
  210. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-description.md +7 -0
  211. package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-name.md +7 -0
  212. package/references/skill-tree/scraper/test/fixtures/minimal-skill/SKILL.md +27 -0
  213. package/references/skill-tree/scraper/test/fixtures/skill-json/SKILL.json +21 -0
  214. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/SKILL.md +54 -0
  215. package/references/skill-tree/scraper/test/fixtures/skill-with-meta/_meta.json +24 -0
  216. package/references/skill-tree/scraper/test/fixtures/valid-skill/SKILL.md +93 -0
  217. package/references/skill-tree/scraper/test/fixtures/valid-skill/_meta.json +22 -0
  218. package/references/skill-tree/scraper/tsup.config.ts +14 -0
  219. package/references/skill-tree/scraper/vitest.config.ts +17 -0
  220. package/references/skill-tree/scripts/convert-to-vitest.ts +166 -0
  221. package/references/skill-tree/skills/skill-writer/SKILL.md +339 -0
  222. package/references/skill-tree/skills/skill-writer/references/examples.md +326 -0
  223. package/references/skill-tree/skills/skill-writer/references/patterns.md +210 -0
  224. package/references/skill-tree/skills/skill-writer/references/quality-checklist.md +123 -0
  225. package/references/skill-tree/test/run-all.ts +106 -0
  226. package/references/skill-tree/test/utils.ts +128 -0
  227. package/references/skill-tree/vitest.config.ts +16 -0
  228. package/references/swarmkit/src/commands/init/phases/configure.ts +0 -22
  229. package/references/swarmkit/src/commands/init/phases/global-setup.ts +5 -3
  230. package/references/swarmkit/src/commands/init/wizard.ts +2 -2
  231. package/references/swarmkit/src/packages/setup.test.ts +53 -7
  232. package/references/swarmkit/src/packages/setup.ts +37 -1
  233. package/scripts/bootstrap.mjs +26 -1
  234. package/scripts/generate-agents.mjs +5 -1
  235. package/scripts/map-hook.mjs +97 -64
  236. package/scripts/map-sidecar.mjs +179 -25
  237. package/scripts/team-loader.mjs +12 -41
  238. package/skills/swarm/SKILL.md +89 -25
  239. package/src/__tests__/agent-generator.test.mjs +6 -13
  240. package/src/__tests__/bootstrap.test.mjs +124 -1
  241. package/src/__tests__/config.test.mjs +200 -27
  242. package/src/__tests__/e2e-live-map.test.mjs +536 -0
  243. package/src/__tests__/e2e-mesh-sidecar.test.mjs +570 -0
  244. package/src/__tests__/e2e-native-task-hooks.test.mjs +376 -0
  245. package/src/__tests__/e2e-sidecar-bridge.test.mjs +477 -0
  246. package/src/__tests__/helpers.mjs +13 -0
  247. package/src/__tests__/inbox.test.mjs +22 -89
  248. package/src/__tests__/index.test.mjs +35 -9
  249. package/src/__tests__/integration.test.mjs +513 -0
  250. package/src/__tests__/map-events.test.mjs +514 -150
  251. package/src/__tests__/mesh-connection.test.mjs +308 -0
  252. package/src/__tests__/opentasks-client.test.mjs +517 -0
  253. package/src/__tests__/paths.test.mjs +185 -41
  254. package/src/__tests__/sidecar-client.test.mjs +35 -0
  255. package/src/__tests__/sidecar-server.test.mjs +124 -0
  256. package/src/__tests__/skilltree-client.test.mjs +80 -0
  257. package/src/agent-generator.mjs +104 -33
  258. package/src/bootstrap.mjs +150 -10
  259. package/src/config.mjs +81 -17
  260. package/src/context-output.mjs +58 -8
  261. package/src/inbox.mjs +9 -54
  262. package/src/index.mjs +39 -8
  263. package/src/map-connection.mjs +4 -3
  264. package/src/map-events.mjs +350 -80
  265. package/src/mesh-connection.mjs +148 -0
  266. package/src/opentasks-client.mjs +269 -0
  267. package/src/paths.mjs +182 -27
  268. package/src/sessionlog.mjs +14 -9
  269. package/src/sidecar-client.mjs +81 -27
  270. package/src/sidecar-server.mjs +175 -16
  271. package/src/skilltree-client.mjs +173 -0
  272. package/src/template.mjs +68 -4
  273. package/vitest.config.mjs +1 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Session tracking for memory entries
3
+ *
4
+ * Captures context about the originating session (Claude Code, VS Code, etc.)
5
+ * and stores it as YAML frontmatter in memory files.
6
+ */
7
+
8
+ import * as os from "node:os";
9
+
10
+ /**
11
+ * Session metadata for memory entries
12
+ */
13
+ export type SessionContext = {
14
+ /** Session identifier (e.g., Claude Code session ID) */
15
+ id?: string;
16
+ /** Source application (claude-code, vscode, cursor, etc.) */
17
+ source?: string;
18
+ /** Project directory path */
19
+ project?: string;
20
+ /** Path to session transcript/log file */
21
+ transcript?: string;
22
+ };
23
+
24
+ /**
25
+ * Source provenance for a knowledge entry
26
+ */
27
+ export type KnowledgeSource = {
28
+ origin?: string;
29
+ trajectories?: string[];
30
+ agentId?: string;
31
+ };
32
+
33
+ /**
34
+ * A directional link from this entry to another knowledge node
35
+ */
36
+ export type KnowledgeLink = {
37
+ target: string;
38
+ relation: string;
39
+ layer?: string;
40
+ };
41
+
42
+ /**
43
+ * Frontmatter structure for memory files
44
+ */
45
+ export type MemoryFrontmatter = {
46
+ session?: SessionContext;
47
+ created?: string;
48
+ updated?: string;
49
+ tags?: string[];
50
+ /** Knowledge node identifier */
51
+ id?: string;
52
+ /** Knowledge entry type */
53
+ type?: "observation" | "entity" | "domain-summary" | string;
54
+ /** Domain tags for this knowledge entry */
55
+ domain?: string[];
56
+ /** Entity references in this knowledge entry */
57
+ entities?: string[];
58
+ /** Confidence score 0-1 */
59
+ confidence?: number;
60
+ /** Source provenance */
61
+ source?: KnowledgeSource;
62
+ /** Links to other knowledge nodes */
63
+ links?: KnowledgeLink[];
64
+ /** ID of the entry this supersedes */
65
+ supersedes?: string | null;
66
+ };
67
+
68
+ /**
69
+ * Parse YAML frontmatter from content
70
+ *
71
+ * Frontmatter is delimited by --- at the start and end:
72
+ * ```
73
+ * ---
74
+ * session:
75
+ * id: abc123
76
+ * source: claude-code
77
+ * created: 2024-01-27T14:30:00Z
78
+ * ---
79
+ * Actual content here...
80
+ * ```
81
+ */
82
+ export function parseFrontmatter(content: string): {
83
+ frontmatter: MemoryFrontmatter | undefined;
84
+ body: string;
85
+ } {
86
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
87
+ const match = content.match(frontmatterRegex);
88
+
89
+ if (!match) {
90
+ return { frontmatter: undefined, body: content };
91
+ }
92
+
93
+ const yamlContent = match[1];
94
+ const body = content.slice(match[0].length);
95
+
96
+ try {
97
+ const frontmatter = parseSimpleYaml(yamlContent);
98
+ return { frontmatter, body };
99
+ } catch {
100
+ // If parsing fails, treat as no frontmatter
101
+ return { frontmatter: undefined, body: content };
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Simple YAML parser for frontmatter.
107
+ *
108
+ * **Limitations** (by design — keeps the dependency count at zero):
109
+ * - Does not handle multi-line strings (block scalars `|` / `>`)
110
+ * - Does not preserve comments
111
+ * - Keys must be simple `\w+` identifiers (no quoted or special-char keys)
112
+ *
113
+ * Supports:
114
+ * - Multi-level nesting (objects within objects)
115
+ * - Inline arrays `[a, b]`
116
+ * - YAML list items with `- ` syntax (including `- {key: val}` objects)
117
+ * - Null values via `~` or `null`
118
+ */
119
+ function parseSimpleYaml(yaml: string): MemoryFrontmatter {
120
+ const lines = yaml.split("\n");
121
+ return parseYamlBlock(lines, 0, 0, lines.length).value as MemoryFrontmatter;
122
+ }
123
+
124
+ /**
125
+ * Parse a block of YAML lines at a given indentation level into an object.
126
+ * Returns the parsed value and the line index where parsing stopped.
127
+ */
128
+ function parseYamlBlock(
129
+ lines: string[],
130
+ indent: number,
131
+ startIdx: number,
132
+ endIdx: number,
133
+ ): { value: Record<string, unknown>; nextIdx: number } {
134
+ const result: Record<string, unknown> = {};
135
+ let i = startIdx;
136
+
137
+ while (i < endIdx) {
138
+ const line = lines[i];
139
+ if (!line || !line.trim()) {
140
+ i++;
141
+ continue;
142
+ }
143
+
144
+ const lineIndent = getIndent(line);
145
+ // If we've dedented back beyond our level, stop
146
+ if (lineIndent < indent) break;
147
+ // Skip lines indented deeper than expected (shouldn't happen at top of block)
148
+ if (lineIndent > indent) {
149
+ i++;
150
+ continue;
151
+ }
152
+
153
+ // Match a key: value line
154
+ const keyMatch = line.match(/^(\s*)([\w-]+):\s*(.*)?$/);
155
+ if (!keyMatch) {
156
+ i++;
157
+ continue;
158
+ }
159
+
160
+ const [, , key, rawValue] = keyMatch;
161
+ const value = rawValue?.trim() ?? "";
162
+
163
+ if (value === "" || value === undefined) {
164
+ // Could be an object or a list starting on next lines
165
+ const nextNonEmpty = findNextNonEmptyLine(lines, i + 1, endIdx);
166
+ if (nextNonEmpty < endIdx) {
167
+ const nextLine = lines[nextNonEmpty]!;
168
+ const nextIndent = getIndent(nextLine);
169
+ if (nextIndent > indent) {
170
+ // Check if it's a list (starts with "- ")
171
+ if (nextLine.trimStart().startsWith("- ")) {
172
+ const listResult = parseYamlList(lines, nextIndent, i + 1, endIdx);
173
+ result[key] = listResult.value;
174
+ i = listResult.nextIdx;
175
+ } else {
176
+ // Nested object
177
+ const blockResult = parseYamlBlock(lines, nextIndent, i + 1, endIdx);
178
+ result[key] = blockResult.value;
179
+ i = blockResult.nextIdx;
180
+ }
181
+ continue;
182
+ }
183
+ }
184
+ // Empty value, no nested content
185
+ result[key] = null;
186
+ i++;
187
+ } else {
188
+ result[key] = parseYamlValue(value);
189
+ i++;
190
+ }
191
+ }
192
+
193
+ return { value: result, nextIdx: i };
194
+ }
195
+
196
+ /**
197
+ * Parse a YAML list (lines starting with "- ") at a given indentation level.
198
+ */
199
+ function parseYamlList(
200
+ lines: string[],
201
+ indent: number,
202
+ startIdx: number,
203
+ endIdx: number,
204
+ ): { value: unknown[]; nextIdx: number } {
205
+ const result: unknown[] = [];
206
+ let i = startIdx;
207
+
208
+ while (i < endIdx) {
209
+ const line = lines[i];
210
+ if (!line || !line.trim()) {
211
+ i++;
212
+ continue;
213
+ }
214
+
215
+ const lineIndent = getIndent(line);
216
+ if (lineIndent < indent) break;
217
+ if (lineIndent > indent) {
218
+ i++;
219
+ continue;
220
+ }
221
+
222
+ const trimmed = line.trimStart();
223
+ if (!trimmed.startsWith("- ")) break;
224
+
225
+ // Get content after "- "
226
+ const itemContent = trimmed.slice(2).trim();
227
+
228
+ if (itemContent === "" || itemContent === undefined) {
229
+ // Sub-block under this list item
230
+ const nextNonEmpty = findNextNonEmptyLine(lines, i + 1, endIdx);
231
+ if (nextNonEmpty < endIdx) {
232
+ const nextIndent = getIndent(lines[nextNonEmpty]!);
233
+ if (nextIndent > indent) {
234
+ const blockResult = parseYamlBlock(lines, nextIndent, i + 1, endIdx);
235
+ result.push(blockResult.value);
236
+ i = blockResult.nextIdx;
237
+ continue;
238
+ }
239
+ }
240
+ result.push(null);
241
+ i++;
242
+ } else {
243
+ // Check for inline key: value (object item like "- target: foo")
244
+ const kvMatch = itemContent.match(/^([\w-]+):\s*(.*)$/);
245
+ if (kvMatch) {
246
+ // This list item is an object — collect all keys at this item's indent + 2
247
+ const obj: Record<string, unknown> = {};
248
+ const [, firstKey, firstVal] = kvMatch;
249
+ obj[firstKey] = parseYamlValue(firstVal?.trim() ?? "");
250
+
251
+ // Look for continuation keys indented further
252
+ const itemKeyIndent = indent + 2;
253
+ let j = i + 1;
254
+ while (j < endIdx) {
255
+ const nextLine = lines[j];
256
+ if (!nextLine || !nextLine.trim()) {
257
+ j++;
258
+ continue;
259
+ }
260
+ const nextLineIndent = getIndent(nextLine);
261
+ if (nextLineIndent < itemKeyIndent) break;
262
+ if (nextLineIndent === itemKeyIndent) {
263
+ const nextKv = nextLine.match(/^\s*([\w-]+):\s*(.*)$/);
264
+ if (nextKv) {
265
+ const [, nk, nv] = nextKv;
266
+ obj[nk] = parseYamlValue(nv?.trim() ?? "");
267
+ j++;
268
+ continue;
269
+ }
270
+ }
271
+ break;
272
+ }
273
+ result.push(obj);
274
+ i = j;
275
+ } else {
276
+ result.push(parseYamlValue(itemContent));
277
+ i++;
278
+ }
279
+ }
280
+ }
281
+
282
+ return { value: result, nextIdx: i };
283
+ }
284
+
285
+ function getIndent(line: string): number {
286
+ const match = line.match(/^(\s*)/);
287
+ return match ? match[1].length : 0;
288
+ }
289
+
290
+ function findNextNonEmptyLine(lines: string[], from: number, end: number): number {
291
+ for (let i = from; i < end; i++) {
292
+ if (lines[i]?.trim()) return i;
293
+ }
294
+ return end;
295
+ }
296
+
297
+ /**
298
+ * Parse a YAML value (handles strings, numbers, booleans, null, arrays)
299
+ */
300
+ function parseYamlValue(value: string): unknown {
301
+ // Empty string
302
+ if (value === "") return null;
303
+
304
+ // Remove quotes if present
305
+ if ((value.startsWith('"') && value.endsWith('"')) ||
306
+ (value.startsWith("'") && value.endsWith("'"))) {
307
+ return value.slice(1, -1);
308
+ }
309
+
310
+ // Null
311
+ if (value === "null" || value === "~") return null;
312
+
313
+ // Boolean
314
+ if (value === "true") return true;
315
+ if (value === "false") return false;
316
+
317
+ // Number
318
+ const num = Number(value);
319
+ if (!isNaN(num) && value !== "") return num;
320
+
321
+ // Array (simple inline format)
322
+ if (value.startsWith("[") && value.endsWith("]")) {
323
+ const inner = value.slice(1, -1);
324
+ if (inner.trim() === "") return [];
325
+ return inner.split(",").map((s) => parseYamlValue(s.trim()));
326
+ }
327
+
328
+ // String
329
+ return value;
330
+ }
331
+
332
+ /**
333
+ * Serialize frontmatter to YAML string
334
+ */
335
+ export function serializeFrontmatter(frontmatter: MemoryFrontmatter): string {
336
+ const lines: string[] = ["---"];
337
+
338
+ if (frontmatter.id) {
339
+ lines.push(`id: ${frontmatter.id}`);
340
+ }
341
+
342
+ if (frontmatter.type) {
343
+ lines.push(`type: ${frontmatter.type}`);
344
+ }
345
+
346
+ if (frontmatter.session) {
347
+ lines.push("session:");
348
+ const session = frontmatter.session;
349
+ if (session.id) lines.push(` id: ${session.id}`);
350
+ if (session.source) lines.push(` source: ${session.source}`);
351
+ if (session.project) lines.push(` project: ${formatPath(session.project)}`);
352
+ if (session.transcript) lines.push(` transcript: ${formatPath(session.transcript)}`);
353
+ }
354
+
355
+ if (frontmatter.created) {
356
+ lines.push(`created: ${frontmatter.created}`);
357
+ }
358
+
359
+ if (frontmatter.updated) {
360
+ lines.push(`updated: ${frontmatter.updated}`);
361
+ }
362
+
363
+ if (frontmatter.tags && frontmatter.tags.length > 0) {
364
+ lines.push(`tags: [${frontmatter.tags.join(", ")}]`);
365
+ }
366
+
367
+ if (frontmatter.domain && frontmatter.domain.length > 0) {
368
+ lines.push(`domain: [${frontmatter.domain.join(", ")}]`);
369
+ }
370
+
371
+ if (frontmatter.entities && frontmatter.entities.length > 0) {
372
+ lines.push(`entities: [${frontmatter.entities.join(", ")}]`);
373
+ }
374
+
375
+ if (frontmatter.confidence !== undefined) {
376
+ lines.push(`confidence: ${frontmatter.confidence}`);
377
+ }
378
+
379
+ if (frontmatter.source) {
380
+ lines.push("source:");
381
+ if (frontmatter.source.origin) lines.push(` origin: ${frontmatter.source.origin}`);
382
+ if (frontmatter.source.trajectories && frontmatter.source.trajectories.length > 0) {
383
+ lines.push(` trajectories: [${frontmatter.source.trajectories.join(", ")}]`);
384
+ }
385
+ if (frontmatter.source.agentId) lines.push(` agentId: ${frontmatter.source.agentId}`);
386
+ }
387
+
388
+ if (frontmatter.links && frontmatter.links.length > 0) {
389
+ lines.push("links:");
390
+ for (const link of frontmatter.links) {
391
+ lines.push(` - target: ${link.target}`);
392
+ lines.push(` relation: ${link.relation}`);
393
+ if (link.layer) lines.push(` layer: ${link.layer}`);
394
+ }
395
+ }
396
+
397
+ if (frontmatter.supersedes !== undefined) {
398
+ lines.push(`supersedes: ${frontmatter.supersedes === null ? "~" : frontmatter.supersedes}`);
399
+ }
400
+
401
+ lines.push("---");
402
+ return lines.join("\n") + "\n";
403
+ }
404
+
405
+ /**
406
+ * Add or update frontmatter in content
407
+ */
408
+ export function addFrontmatter(
409
+ content: string,
410
+ frontmatter: MemoryFrontmatter,
411
+ ): string {
412
+ const { frontmatter: existing, body } = parseFrontmatter(content);
413
+
414
+ // Merge with existing frontmatter
415
+ const merged: MemoryFrontmatter = {
416
+ ...existing,
417
+ ...frontmatter,
418
+ session: {
419
+ ...existing?.session,
420
+ ...frontmatter.session,
421
+ },
422
+ };
423
+
424
+ // Update timestamp
425
+ if (!merged.created) {
426
+ merged.created = new Date().toISOString();
427
+ }
428
+ merged.updated = new Date().toISOString();
429
+
430
+ return serializeFrontmatter(merged) + body;
431
+ }
432
+
433
+ /**
434
+ * Add session context as frontmatter to content
435
+ */
436
+ export function addSessionToContent(
437
+ content: string,
438
+ session: SessionContext,
439
+ ): string {
440
+ return addFrontmatter(content, { session });
441
+ }
442
+
443
+ /**
444
+ * Format path for display (use ~ for home directory)
445
+ */
446
+ function formatPath(filePath: string): string {
447
+ const home = os.homedir();
448
+ if (filePath.startsWith(home)) {
449
+ return "~" + filePath.slice(home.length);
450
+ }
451
+ return filePath;
452
+ }
453
+
454
+ /**
455
+ * Extract session context from file content
456
+ */
457
+ export function extractSession(content: string): SessionContext | undefined {
458
+ const { frontmatter } = parseFrontmatter(content);
459
+ return frontmatter?.session;
460
+ }
@@ -0,0 +1,177 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+
6
+ import {
7
+ loadManifest,
8
+ saveManifest,
9
+ loadStoreLinks,
10
+ saveStoreLinks,
11
+ resolveStore,
12
+ resolveStoreName,
13
+ getLinkedStoreNames,
14
+ type StoreManifest,
15
+ } from "../manifest.js";
16
+
17
+ describe("manifest", () => {
18
+ let tmpDir: string;
19
+
20
+ beforeEach(async () => {
21
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "minimem-manifest-test-"));
22
+ });
23
+
24
+ afterEach(async () => {
25
+ await fs.rm(tmpDir, { recursive: true, force: true });
26
+ });
27
+
28
+ describe("loadManifest / saveManifest", () => {
29
+ it("returns empty manifest when file does not exist", async () => {
30
+ const manifest = await loadManifest(path.join(tmpDir, "nonexistent.json"));
31
+ expect(manifest.stores).toEqual({});
32
+ });
33
+
34
+ it("round-trips a manifest", async () => {
35
+ const manifestPath = path.join(tmpDir, "stores.json");
36
+ const manifest: StoreManifest = {
37
+ stores: {
38
+ "project-a": { path: "/home/user/project-a" },
39
+ "project-b": {
40
+ path: "/home/user/project-b",
41
+ remote: "git@github.com:org/project-b.git",
42
+ description: "Project B memories",
43
+ },
44
+ },
45
+ };
46
+
47
+ await saveManifest(manifest, manifestPath);
48
+ const loaded = await loadManifest(manifestPath);
49
+
50
+ expect(loaded.stores["project-a"].path).toBe("/home/user/project-a");
51
+ expect(loaded.stores["project-b"].remote).toBe("git@github.com:org/project-b.git");
52
+ expect(loaded.stores["project-b"].description).toBe("Project B memories");
53
+ });
54
+
55
+ it("expands ~ in paths", async () => {
56
+ const manifestPath = path.join(tmpDir, "stores.json");
57
+ await fs.writeFile(
58
+ manifestPath,
59
+ JSON.stringify({ stores: { test: { path: "~/my-project" } } }),
60
+ );
61
+
62
+ const loaded = await loadManifest(manifestPath);
63
+ expect(loaded.stores["test"].path).toBe(
64
+ path.join(os.homedir(), "my-project"),
65
+ );
66
+ });
67
+ });
68
+
69
+ describe("loadStoreLinks / saveStoreLinks", () => {
70
+ it("returns empty links when no file exists", async () => {
71
+ const links = await loadStoreLinks(tmpDir);
72
+ expect(links.links).toEqual([]);
73
+ });
74
+
75
+ it("loads links from .minimem/links.json", async () => {
76
+ const linksDir = path.join(tmpDir, ".minimem");
77
+ await fs.mkdir(linksDir, { recursive: true });
78
+ await fs.writeFile(
79
+ path.join(linksDir, "links.json"),
80
+ JSON.stringify({ links: ["project-a", "project-b"] }),
81
+ );
82
+
83
+ const links = await loadStoreLinks(tmpDir);
84
+ expect(links.links).toEqual(["project-a", "project-b"]);
85
+ });
86
+
87
+ it("saves links to existing config dir", async () => {
88
+ const linksDir = path.join(tmpDir, ".minimem");
89
+ await fs.mkdir(linksDir, { recursive: true });
90
+
91
+ await saveStoreLinks(tmpDir, { links: ["store-x"] });
92
+
93
+ const content = await fs.readFile(
94
+ path.join(linksDir, "links.json"),
95
+ "utf-8",
96
+ );
97
+ expect(JSON.parse(content).links).toEqual(["store-x"]);
98
+ });
99
+ });
100
+
101
+ describe("resolveStore", () => {
102
+ it("returns store definition by name", () => {
103
+ const manifest: StoreManifest = {
104
+ stores: { mystore: { path: "/tmp/mystore" } },
105
+ };
106
+ const def = resolveStore(manifest, "mystore");
107
+ expect(def).not.toBeNull();
108
+ expect(def!.path).toBe("/tmp/mystore");
109
+ });
110
+
111
+ it("returns null for unknown store", () => {
112
+ const manifest: StoreManifest = { stores: {} };
113
+ expect(resolveStore(manifest, "missing")).toBeNull();
114
+ });
115
+ });
116
+
117
+ describe("resolveStoreName", () => {
118
+ it("finds store name by path", () => {
119
+ const manifest: StoreManifest = {
120
+ stores: { mystore: { path: "/tmp/mystore" } },
121
+ };
122
+ expect(resolveStoreName(manifest, "/tmp/mystore")).toBe("mystore");
123
+ });
124
+
125
+ it("returns null for unregistered path", () => {
126
+ const manifest: StoreManifest = { stores: {} };
127
+ expect(resolveStoreName(manifest, "/tmp/unknown")).toBeNull();
128
+ });
129
+ });
130
+
131
+ describe("getLinkedStoreNames", () => {
132
+ it("returns linked store names from per-store links", async () => {
133
+ // Create a store dir with links
134
+ const storeDir = path.join(tmpDir, "my-store");
135
+ const linksDir = path.join(storeDir, ".minimem");
136
+ await fs.mkdir(linksDir, { recursive: true });
137
+ await fs.writeFile(
138
+ path.join(linksDir, "links.json"),
139
+ JSON.stringify({ links: ["dep-a", "dep-b"] }),
140
+ );
141
+
142
+ const manifest: StoreManifest = {
143
+ stores: {
144
+ "my-store": { path: storeDir },
145
+ "dep-a": { path: "/tmp/dep-a" },
146
+ "dep-b": { path: "/tmp/dep-b" },
147
+ },
148
+ };
149
+
150
+ const linked = await getLinkedStoreNames(manifest, "my-store");
151
+ expect(linked).toEqual(["dep-a", "dep-b"]);
152
+ });
153
+
154
+ it("returns empty for store not in manifest", async () => {
155
+ const manifest: StoreManifest = { stores: {} };
156
+ const linked = await getLinkedStoreNames(manifest, "missing");
157
+ expect(linked).toEqual([]);
158
+ });
159
+
160
+ it("deduplicates linked stores", async () => {
161
+ const storeDir = path.join(tmpDir, "my-store");
162
+ const linksDir = path.join(storeDir, ".minimem");
163
+ await fs.mkdir(linksDir, { recursive: true });
164
+ await fs.writeFile(
165
+ path.join(linksDir, "links.json"),
166
+ JSON.stringify({ links: ["dep-a", "dep-a", "dep-b"] }),
167
+ );
168
+
169
+ const manifest: StoreManifest = {
170
+ stores: { "my-store": { path: storeDir } },
171
+ };
172
+
173
+ const linked = await getLinkedStoreNames(manifest, "my-store");
174
+ expect(linked).toEqual(["dep-a", "dep-b"]);
175
+ });
176
+ });
177
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import fsSync from "node:fs";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+
7
+ import { materializeStore, type MaterializeResult } from "../materialize.js";
8
+
9
+ describe("materializeStore", () => {
10
+ let tmpDir: string;
11
+
12
+ beforeEach(async () => {
13
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "minimem-materialize-test-"));
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await fs.rm(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it("returns symlink strategy for existing local store", async () => {
21
+ const storePath = path.join(tmpDir, "my-store");
22
+ await fs.mkdir(storePath, { recursive: true });
23
+ await fs.writeFile(path.join(storePath, "MEMORY.md"), "# Memory\n");
24
+
25
+ const result = await materializeStore("my-store", { path: storePath });
26
+
27
+ expect(result).not.toBeNull();
28
+ expect(result!.strategy).toBe("symlink");
29
+ expect(result!.path).toBe(storePath);
30
+
31
+ // Cleanup should work without error
32
+ await result!.cleanup();
33
+ });
34
+
35
+ it("returns null for nonexistent store with no remote", async () => {
36
+ const result = await materializeStore("missing", {
37
+ path: "/nonexistent/path",
38
+ });
39
+ expect(result).toBeNull();
40
+ });
41
+
42
+ it("symlink cleanup removes temp dir", async () => {
43
+ const storePath = path.join(tmpDir, "my-store");
44
+ await fs.mkdir(storePath, { recursive: true });
45
+
46
+ const result = await materializeStore("my-store", { path: storePath });
47
+ expect(result).not.toBeNull();
48
+
49
+ // The cleanup function should not throw
50
+ await result!.cleanup();
51
+ });
52
+ });