superskill 0.2.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 (301) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +235 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +1158 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/brainstorm.d.ts +9 -0
  8. package/dist/commands/brainstorm.js +37 -0
  9. package/dist/commands/brainstorm.js.map +1 -0
  10. package/dist/commands/brainstorm.test.d.ts +1 -0
  11. package/dist/commands/brainstorm.test.js +110 -0
  12. package/dist/commands/brainstorm.test.js.map +1 -0
  13. package/dist/commands/context.d.ts +18 -0
  14. package/dist/commands/context.js +88 -0
  15. package/dist/commands/context.js.map +1 -0
  16. package/dist/commands/context.test.d.ts +1 -0
  17. package/dist/commands/context.test.js +230 -0
  18. package/dist/commands/context.test.js.map +1 -0
  19. package/dist/commands/decide.d.ts +12 -0
  20. package/dist/commands/decide.js +40 -0
  21. package/dist/commands/decide.js.map +1 -0
  22. package/dist/commands/decide.test.d.ts +1 -0
  23. package/dist/commands/decide.test.js +109 -0
  24. package/dist/commands/decide.test.js.map +1 -0
  25. package/dist/commands/graph.d.ts +15 -0
  26. package/dist/commands/graph.js +89 -0
  27. package/dist/commands/graph.js.map +1 -0
  28. package/dist/commands/graph.test.d.ts +1 -0
  29. package/dist/commands/graph.test.js +215 -0
  30. package/dist/commands/graph.test.js.map +1 -0
  31. package/dist/commands/init.d.ts +11 -0
  32. package/dist/commands/init.js +257 -0
  33. package/dist/commands/init.js.map +1 -0
  34. package/dist/commands/learn.d.ts +25 -0
  35. package/dist/commands/learn.js +92 -0
  36. package/dist/commands/learn.js.map +1 -0
  37. package/dist/commands/learn.test.d.ts +1 -0
  38. package/dist/commands/learn.test.js +241 -0
  39. package/dist/commands/learn.test.js.map +1 -0
  40. package/dist/commands/prune.d.ts +58 -0
  41. package/dist/commands/prune.js +246 -0
  42. package/dist/commands/prune.js.map +1 -0
  43. package/dist/commands/read.d.ts +8 -0
  44. package/dist/commands/read.js +7 -0
  45. package/dist/commands/read.js.map +1 -0
  46. package/dist/commands/read.test.d.ts +1 -0
  47. package/dist/commands/read.test.js +48 -0
  48. package/dist/commands/read.test.js.map +1 -0
  49. package/dist/commands/resume.d.ts +20 -0
  50. package/dist/commands/resume.js +141 -0
  51. package/dist/commands/resume.js.map +1 -0
  52. package/dist/commands/resume.test.d.ts +1 -0
  53. package/dist/commands/resume.test.js +243 -0
  54. package/dist/commands/resume.test.js.map +1 -0
  55. package/dist/commands/search.d.ts +8 -0
  56. package/dist/commands/search.js +25 -0
  57. package/dist/commands/search.js.map +1 -0
  58. package/dist/commands/search.test.d.ts +1 -0
  59. package/dist/commands/search.test.js +61 -0
  60. package/dist/commands/search.test.js.map +1 -0
  61. package/dist/commands/session.d.ts +22 -0
  62. package/dist/commands/session.js +79 -0
  63. package/dist/commands/session.js.map +1 -0
  64. package/dist/commands/session.test.d.ts +1 -0
  65. package/dist/commands/session.test.js +231 -0
  66. package/dist/commands/session.test.js.map +1 -0
  67. package/dist/commands/skill/catalog.d.ts +39 -0
  68. package/dist/commands/skill/catalog.js +260 -0
  69. package/dist/commands/skill/catalog.js.map +1 -0
  70. package/dist/commands/skill/index.d.ts +55 -0
  71. package/dist/commands/skill/index.js +82 -0
  72. package/dist/commands/skill/index.js.map +1 -0
  73. package/dist/commands/skill/index.test.d.ts +1 -0
  74. package/dist/commands/skill/index.test.js +137 -0
  75. package/dist/commands/skill/index.test.js.map +1 -0
  76. package/dist/commands/skill/install.d.ts +14 -0
  77. package/dist/commands/skill/install.js +108 -0
  78. package/dist/commands/skill/install.js.map +1 -0
  79. package/dist/commands/skill/install.test.d.ts +1 -0
  80. package/dist/commands/skill/install.test.js +225 -0
  81. package/dist/commands/skill/install.test.js.map +1 -0
  82. package/dist/commands/skill/list.d.ts +6 -0
  83. package/dist/commands/skill/list.js +5 -0
  84. package/dist/commands/skill/list.js.map +1 -0
  85. package/dist/commands/skill/list.test.d.ts +1 -0
  86. package/dist/commands/skill/list.test.js +101 -0
  87. package/dist/commands/skill/list.test.js.map +1 -0
  88. package/dist/commands/skill/marketplace.d.ts +121 -0
  89. package/dist/commands/skill/marketplace.js +548 -0
  90. package/dist/commands/skill/marketplace.js.map +1 -0
  91. package/dist/commands/skill/schema.d.ts +27 -0
  92. package/dist/commands/skill/schema.js +55 -0
  93. package/dist/commands/skill/schema.js.map +1 -0
  94. package/dist/commands/skill/schema.test.d.ts +1 -0
  95. package/dist/commands/skill/schema.test.js +142 -0
  96. package/dist/commands/skill/schema.test.js.map +1 -0
  97. package/dist/commands/skill/validate.d.ts +10 -0
  98. package/dist/commands/skill/validate.js +40 -0
  99. package/dist/commands/skill/validate.js.map +1 -0
  100. package/dist/commands/skill/validate.test.d.ts +1 -0
  101. package/dist/commands/skill/validate.test.js +171 -0
  102. package/dist/commands/skill/validate.test.js.map +1 -0
  103. package/dist/commands/task.d.ts +34 -0
  104. package/dist/commands/task.js +160 -0
  105. package/dist/commands/task.js.map +1 -0
  106. package/dist/commands/task.test.d.ts +1 -0
  107. package/dist/commands/task.test.js +395 -0
  108. package/dist/commands/task.test.js.map +1 -0
  109. package/dist/commands/todo.d.ts +15 -0
  110. package/dist/commands/todo.js +99 -0
  111. package/dist/commands/todo.js.map +1 -0
  112. package/dist/commands/todo.test.d.ts +1 -0
  113. package/dist/commands/todo.test.js +324 -0
  114. package/dist/commands/todo.test.js.map +1 -0
  115. package/dist/commands/write.d.ts +12 -0
  116. package/dist/commands/write.js +40 -0
  117. package/dist/commands/write.js.map +1 -0
  118. package/dist/commands/write.test.d.ts +1 -0
  119. package/dist/commands/write.test.js +79 -0
  120. package/dist/commands/write.test.js.map +1 -0
  121. package/dist/config.d.ts +15 -0
  122. package/dist/config.js +49 -0
  123. package/dist/config.js.map +1 -0
  124. package/dist/config.test.d.ts +1 -0
  125. package/dist/config.test.js +147 -0
  126. package/dist/config.test.js.map +1 -0
  127. package/dist/core/executor.d.ts +11 -0
  128. package/dist/core/executor.js +42 -0
  129. package/dist/core/executor.js.map +1 -0
  130. package/dist/core/executor.test.d.ts +1 -0
  131. package/dist/core/executor.test.js +206 -0
  132. package/dist/core/executor.test.js.map +1 -0
  133. package/dist/core/middleware/error-handler.d.ts +2 -0
  134. package/dist/core/middleware/error-handler.js +29 -0
  135. package/dist/core/middleware/error-handler.js.map +1 -0
  136. package/dist/core/middleware/index.d.ts +2 -0
  137. package/dist/core/middleware/index.js +3 -0
  138. package/dist/core/middleware/index.js.map +1 -0
  139. package/dist/core/middleware/logging.d.ts +2 -0
  140. package/dist/core/middleware/logging.js +18 -0
  141. package/dist/core/middleware/logging.js.map +1 -0
  142. package/dist/core/registry.d.ts +12 -0
  143. package/dist/core/registry.js +552 -0
  144. package/dist/core/registry.js.map +1 -0
  145. package/dist/core/registry.test.d.ts +1 -0
  146. package/dist/core/registry.test.js +213 -0
  147. package/dist/core/registry.test.js.map +1 -0
  148. package/dist/core/types.d.ts +38 -0
  149. package/dist/core/types.js +2 -0
  150. package/dist/core/types.js.map +1 -0
  151. package/dist/integration/coordination.test.d.ts +1 -0
  152. package/dist/integration/coordination.test.js +286 -0
  153. package/dist/integration/coordination.test.js.map +1 -0
  154. package/dist/integration/project-artifacts.test.d.ts +1 -0
  155. package/dist/integration/project-artifacts.test.js +275 -0
  156. package/dist/integration/project-artifacts.test.js.map +1 -0
  157. package/dist/integration/registry-graph.test.d.ts +1 -0
  158. package/dist/integration/registry-graph.test.js +157 -0
  159. package/dist/integration/registry-graph.test.js.map +1 -0
  160. package/dist/integration/vault-lifecycle.test.d.ts +1 -0
  161. package/dist/integration/vault-lifecycle.test.js +183 -0
  162. package/dist/integration/vault-lifecycle.test.js.map +1 -0
  163. package/dist/lib/auto-number.d.ts +10 -0
  164. package/dist/lib/auto-number.js +33 -0
  165. package/dist/lib/auto-number.js.map +1 -0
  166. package/dist/lib/auto-number.test.d.ts +1 -0
  167. package/dist/lib/auto-number.test.js +88 -0
  168. package/dist/lib/auto-number.test.js.map +1 -0
  169. package/dist/lib/auto-profile.d.ts +11 -0
  170. package/dist/lib/auto-profile.js +123 -0
  171. package/dist/lib/auto-profile.js.map +1 -0
  172. package/dist/lib/auto-profile.test.d.ts +1 -0
  173. package/dist/lib/auto-profile.test.js +227 -0
  174. package/dist/lib/auto-profile.test.js.map +1 -0
  175. package/dist/lib/escape-regex.d.ts +4 -0
  176. package/dist/lib/escape-regex.js +8 -0
  177. package/dist/lib/escape-regex.js.map +1 -0
  178. package/dist/lib/escape-regex.test.d.ts +1 -0
  179. package/dist/lib/escape-regex.test.js +27 -0
  180. package/dist/lib/escape-regex.test.js.map +1 -0
  181. package/dist/lib/frontmatter.d.ts +34 -0
  182. package/dist/lib/frontmatter.js +75 -0
  183. package/dist/lib/frontmatter.js.map +1 -0
  184. package/dist/lib/frontmatter.test.d.ts +1 -0
  185. package/dist/lib/frontmatter.test.js +192 -0
  186. package/dist/lib/frontmatter.test.js.map +1 -0
  187. package/dist/lib/project-detector.d.ts +12 -0
  188. package/dist/lib/project-detector.js +124 -0
  189. package/dist/lib/project-detector.js.map +1 -0
  190. package/dist/lib/project-detector.test.d.ts +1 -0
  191. package/dist/lib/project-detector.test.js +117 -0
  192. package/dist/lib/project-detector.test.js.map +1 -0
  193. package/dist/lib/safe-external-path.d.ts +10 -0
  194. package/dist/lib/safe-external-path.js +47 -0
  195. package/dist/lib/safe-external-path.js.map +1 -0
  196. package/dist/lib/safe-external-path.test.d.ts +1 -0
  197. package/dist/lib/safe-external-path.test.js +99 -0
  198. package/dist/lib/safe-external-path.test.js.map +1 -0
  199. package/dist/lib/search-engine.d.ts +19 -0
  200. package/dist/lib/search-engine.js +164 -0
  201. package/dist/lib/search-engine.js.map +1 -0
  202. package/dist/lib/search-engine.test.d.ts +1 -0
  203. package/dist/lib/search-engine.test.js +120 -0
  204. package/dist/lib/search-engine.test.js.map +1 -0
  205. package/dist/lib/session-registry.d.ts +59 -0
  206. package/dist/lib/session-registry.js +231 -0
  207. package/dist/lib/session-registry.js.map +1 -0
  208. package/dist/lib/session-registry.test.d.ts +1 -0
  209. package/dist/lib/session-registry.test.js +199 -0
  210. package/dist/lib/session-registry.test.js.map +1 -0
  211. package/dist/lib/skill-registry.d.ts +13 -0
  212. package/dist/lib/skill-registry.js +77 -0
  213. package/dist/lib/skill-registry.js.map +1 -0
  214. package/dist/lib/stack-detector.d.ts +7 -0
  215. package/dist/lib/stack-detector.js +184 -0
  216. package/dist/lib/stack-detector.js.map +1 -0
  217. package/dist/lib/stack-detector.test.d.ts +1 -0
  218. package/dist/lib/stack-detector.test.js +110 -0
  219. package/dist/lib/stack-detector.test.js.map +1 -0
  220. package/dist/lib/token-estimator.d.ts +19 -0
  221. package/dist/lib/token-estimator.js +59 -0
  222. package/dist/lib/token-estimator.js.map +1 -0
  223. package/dist/lib/token-estimator.test.d.ts +1 -0
  224. package/dist/lib/token-estimator.test.js +65 -0
  225. package/dist/lib/token-estimator.test.js.map +1 -0
  226. package/dist/lib/tool-detector.d.ts +8 -0
  227. package/dist/lib/tool-detector.js +76 -0
  228. package/dist/lib/tool-detector.js.map +1 -0
  229. package/dist/lib/tool-detector.test.d.ts +1 -0
  230. package/dist/lib/tool-detector.test.js +170 -0
  231. package/dist/lib/tool-detector.test.js.map +1 -0
  232. package/dist/lib/vault-fs.d.ts +39 -0
  233. package/dist/lib/vault-fs.js +184 -0
  234. package/dist/lib/vault-fs.js.map +1 -0
  235. package/dist/lib/vault-fs.test.d.ts +1 -0
  236. package/dist/lib/vault-fs.test.js +210 -0
  237. package/dist/lib/vault-fs.test.js.map +1 -0
  238. package/dist/mcp-server.d.ts +2 -0
  239. package/dist/mcp-server.js +387 -0
  240. package/dist/mcp-server.js.map +1 -0
  241. package/dist/setup/clients.d.ts +2 -0
  242. package/dist/setup/clients.js +109 -0
  243. package/dist/setup/clients.js.map +1 -0
  244. package/dist/setup/configure.d.ts +6 -0
  245. package/dist/setup/configure.js +126 -0
  246. package/dist/setup/configure.js.map +1 -0
  247. package/dist/setup/configure.test.d.ts +1 -0
  248. package/dist/setup/configure.test.js +112 -0
  249. package/dist/setup/configure.test.js.map +1 -0
  250. package/dist/setup/detect.d.ts +3 -0
  251. package/dist/setup/detect.js +24 -0
  252. package/dist/setup/detect.js.map +1 -0
  253. package/dist/setup/detect.test.d.ts +1 -0
  254. package/dist/setup/detect.test.js +44 -0
  255. package/dist/setup/detect.test.js.map +1 -0
  256. package/dist/setup/index.d.ts +2 -0
  257. package/dist/setup/index.js +147 -0
  258. package/dist/setup/index.js.map +1 -0
  259. package/dist/setup/instructions.d.ts +4 -0
  260. package/dist/setup/instructions.js +62 -0
  261. package/dist/setup/instructions.js.map +1 -0
  262. package/dist/setup/instructions.test.d.ts +1 -0
  263. package/dist/setup/instructions.test.js +84 -0
  264. package/dist/setup/instructions.test.js.map +1 -0
  265. package/dist/setup/json-config.d.ts +10 -0
  266. package/dist/setup/json-config.js +43 -0
  267. package/dist/setup/json-config.js.map +1 -0
  268. package/dist/setup/json-config.test.d.ts +1 -0
  269. package/dist/setup/json-config.test.js +92 -0
  270. package/dist/setup/json-config.test.js.map +1 -0
  271. package/dist/setup/postinstall.d.ts +2 -0
  272. package/dist/setup/postinstall.js +22 -0
  273. package/dist/setup/postinstall.js.map +1 -0
  274. package/dist/setup/preuninstall.d.ts +2 -0
  275. package/dist/setup/preuninstall.js +12 -0
  276. package/dist/setup/preuninstall.js.map +1 -0
  277. package/dist/setup/setup-integration.test.d.ts +1 -0
  278. package/dist/setup/setup-integration.test.js +72 -0
  279. package/dist/setup/setup-integration.test.js.map +1 -0
  280. package/dist/setup/teardown.d.ts +5 -0
  281. package/dist/setup/teardown.js +94 -0
  282. package/dist/setup/teardown.js.map +1 -0
  283. package/dist/setup/teardown.test.d.ts +1 -0
  284. package/dist/setup/teardown.test.js +105 -0
  285. package/dist/setup/teardown.test.js.map +1 -0
  286. package/dist/setup/toml-config.d.ts +5 -0
  287. package/dist/setup/toml-config.js +26 -0
  288. package/dist/setup/toml-config.js.map +1 -0
  289. package/dist/setup/toml-config.test.d.ts +1 -0
  290. package/dist/setup/toml-config.test.js +49 -0
  291. package/dist/setup/toml-config.test.js.map +1 -0
  292. package/dist/setup/types.d.ts +52 -0
  293. package/dist/setup/types.js +23 -0
  294. package/dist/setup/types.js.map +1 -0
  295. package/dist/test-helpers.d.ts +20 -0
  296. package/dist/test-helpers.js +47 -0
  297. package/dist/test-helpers.js.map +1 -0
  298. package/dist/test-helpers.test.d.ts +1 -0
  299. package/dist/test-helpers.test.js +90 -0
  300. package/dist/test-helpers.test.js.map +1 -0
  301. package/package.json +74 -0
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdir, writeFile, rm } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { searchText, searchStructured } from "./search-engine.js";
6
+ describe("search-engine", () => {
7
+ let vaultRoot;
8
+ beforeEach(async () => {
9
+ vaultRoot = join(tmpdir(), `search-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
10
+ await mkdir(vaultRoot, { recursive: true });
11
+ });
12
+ afterEach(async () => {
13
+ await rm(vaultRoot, { recursive: true, force: true });
14
+ });
15
+ describe("searchText", () => {
16
+ beforeEach(async () => {
17
+ await writeFile(join(vaultRoot, "doc1.md"), "# Hello World\n\nThis is a test document.");
18
+ await writeFile(join(vaultRoot, "doc2.md"), "# Another Doc\n\nNo matching content here.");
19
+ await mkdir(join(vaultRoot, "subdir"), { recursive: true });
20
+ await writeFile(join(vaultRoot, "subdir/nested.md"), "# Nested Hello\n\nHello from nested path.");
21
+ await mkdir(join(vaultRoot, ".obsidian"), { recursive: true });
22
+ await writeFile(join(vaultRoot, ".obsidian/config"), "hello in hidden");
23
+ });
24
+ it("finds matching documents", async () => {
25
+ const results = await searchText(vaultRoot, "Hello");
26
+ expect(results.length).toBeGreaterThan(0);
27
+ expect(results.some(r => r.path === "doc1.md")).toBe(true);
28
+ });
29
+ it("is case insensitive", async () => {
30
+ const results = await searchText(vaultRoot, "HELLO");
31
+ expect(results.length).toBeGreaterThan(0);
32
+ });
33
+ it("respects limit parameter", async () => {
34
+ const results = await searchText(vaultRoot, "Hello", { limit: 1 });
35
+ expect(results.length).toBeLessThanOrEqual(1);
36
+ });
37
+ it("filters by path", async () => {
38
+ const results = await searchText(vaultRoot, "Hello", { pathFilter: "subdir" });
39
+ expect(results.every(r => r.path.startsWith("subdir"))).toBe(true);
40
+ });
41
+ it("excludes hidden directories", async () => {
42
+ const results = await searchText(vaultRoot, "hello");
43
+ expect(results.every(r => !r.path.startsWith(".obsidian"))).toBe(true);
44
+ });
45
+ it("returns empty array for no matches", async () => {
46
+ const results = await searchText(vaultRoot, "zzzznomatchxxxx");
47
+ expect(results).toEqual([]);
48
+ });
49
+ it("rejects traversal attack in pathFilter", async () => {
50
+ await expect(searchText(vaultRoot, "test", { pathFilter: "../outside" })).rejects.toThrow();
51
+ });
52
+ it("rejects absolute path in pathFilter", async () => {
53
+ await expect(searchText(vaultRoot, "test", { pathFilter: "/etc/passwd" })).rejects.toThrow();
54
+ });
55
+ it("returns path, snippet, and line number", async () => {
56
+ const results = await searchText(vaultRoot, "World");
57
+ expect(results.length).toBeGreaterThan(0);
58
+ expect(results[0].path).toBeDefined();
59
+ expect(results[0].snippet).toBeDefined();
60
+ expect(results[0].line).toBeGreaterThan(0);
61
+ });
62
+ it("handles special regex characters safely", async () => {
63
+ await writeFile(join(vaultRoot, "special.md"), "Content with $pecial ch@rs");
64
+ const results = await searchText(vaultRoot, "$pecial");
65
+ expect(results.length).toBeGreaterThan(0);
66
+ });
67
+ });
68
+ describe("searchStructured", () => {
69
+ beforeEach(async () => {
70
+ await writeFile(join(vaultRoot, "task1.md"), "---\ntype: task\nstatus: in-progress\nproject: test-project\ntags:\n - urgent\n - backend\n---\n\n# Task 1\n\nTask one content.");
71
+ await writeFile(join(vaultRoot, "task2.md"), "---\ntype: task\nstatus: done\nproject: other-project\ntags:\n - frontend\n---\n\n# Task 2\n\nTask two content.");
72
+ await writeFile(join(vaultRoot, "note.md"), "---\ntype: index\n---\n\n# Note\n\nJust a note.");
73
+ });
74
+ it("filters by single frontmatter field", async () => {
75
+ const results = await searchStructured(vaultRoot, { type: "task" });
76
+ expect(results.length).toBe(2);
77
+ });
78
+ it("filters by multiple fields", async () => {
79
+ const results = await searchStructured(vaultRoot, { type: "task", status: "done" });
80
+ expect(results.length).toBe(1);
81
+ expect(results[0].path).toBe("task2.md");
82
+ });
83
+ it("filters by array field value", async () => {
84
+ const results = await searchStructured(vaultRoot, { tags: "urgent" });
85
+ expect(results.length).toBe(1);
86
+ expect(results[0].path).toBe("task1.md");
87
+ });
88
+ it("respects limit parameter", async () => {
89
+ const results = await searchStructured(vaultRoot, { type: "task" }, { limit: 1 });
90
+ expect(results.length).toBeLessThanOrEqual(1);
91
+ });
92
+ it("returns empty array for no matches", async () => {
93
+ const results = await searchStructured(vaultRoot, { type: "nonexistent" });
94
+ expect(results).toEqual([]);
95
+ });
96
+ it("excludes hidden directories", async () => {
97
+ await mkdir(join(vaultRoot, ".hidden"), { recursive: true });
98
+ await writeFile(join(vaultRoot, ".hidden/secret.md"), "---\ntype: secret\n---\n\n# Secret");
99
+ const results = await searchStructured(vaultRoot, { type: "secret" });
100
+ expect(results.every(r => !r.path.startsWith("."))).toBe(true);
101
+ });
102
+ it("returns path and snippet", async () => {
103
+ const results = await searchStructured(vaultRoot, { type: "task" });
104
+ expect(results.length).toBeGreaterThan(0);
105
+ expect(results[0].path).toBeDefined();
106
+ expect(results[0].snippet).toBeDefined();
107
+ });
108
+ it("handles files without frontmatter", async () => {
109
+ await writeFile(join(vaultRoot, "plain.md"), "Just plain text");
110
+ const results = await searchStructured(vaultRoot, { type: "task" });
111
+ expect(results.every(r => r.path !== "plain.md")).toBe(true);
112
+ });
113
+ it("handles malformed YAML gracefully", async () => {
114
+ await writeFile(join(vaultRoot, "bad.md"), "---\ninvalid: yaml: content\n---\n\n# Bad");
115
+ const results = await searchStructured(vaultRoot, { type: "task" });
116
+ expect(results.every(r => r.path !== "bad.md")).toBe(true);
117
+ });
118
+ });
119
+ });
120
+ //# sourceMappingURL=search-engine.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-engine.test.js","sourceRoot":"","sources":["../../src/lib/search-engine.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAElE,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,IAAI,SAAiB,CAAC;IAEtB,UAAU,CAAC,KAAK,IAAI,EAAE;QACpB,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,eAAe,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/F,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;QAC1B,UAAU,CAAC,KAAK,IAAI,EAAE;YACpB,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,2CAA2C,CAAC,CAAC;YACzF,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,4CAA4C,CAAC,CAAC;YAC1F,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC5D,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,EAAE,2CAA2C,CAAC,CAAC;YAClG,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/D,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,EAAE,iBAAiB,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;YACxC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACrD,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE;YACnC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACrD,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;YACxC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;YACnE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iBAAiB,EAAE,KAAK,IAAI,EAAE;YAC/B,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC/E,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;YAC3C,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACrD,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;YAClD,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;YAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;YACtD,MAAM,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAC9F,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;YACnD,MAAM,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAC/F,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;YACtD,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACrD,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;YACzC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;YACvD,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,4BAA4B,CAAC,CAAC;YAC7E,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACvD,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChC,UAAU,CAAC,KAAK,IAAI,EAAE;YACpB,MAAM,SAAS,CACb,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAC3B,mIAAmI,CACpI,CAAC;YACF,MAAM,SAAS,CACb,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAC3B,kHAAkH,CACnH,CAAC;YACF,MAAM,SAAS,CACb,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,iDAAiD,CAClD,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;YACnD,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACpE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;YAC1C,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;YACpF,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;YAC5C,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YACtE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;YACxC,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;YAClF,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;YAClD,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;YAC3E,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;YAC3C,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7D,MAAM,SAAS,CACb,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,EACpC,oCAAoC,CACrC,CAAC;YACF,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YACtE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;YACxC,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACpE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;QAC3C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;YACjD,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,CAAC;YAChE,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACpE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;YACjD,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,2CAA2C,CAAC,CAAC;YACxF,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACpE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,59 @@
1
+ export interface Session {
2
+ id: string;
3
+ tool: string;
4
+ project: string | null;
5
+ task_summary: string | null;
6
+ files_touched: string[];
7
+ started_at: string;
8
+ last_heartbeat: string;
9
+ completed_at: string | null;
10
+ status: "active" | "stale" | "completed";
11
+ }
12
+ export interface SessionRegistry {
13
+ sessions: Session[];
14
+ }
15
+ interface Conflict {
16
+ session_id: string;
17
+ tool: string;
18
+ overlapping_files: string[];
19
+ task_summary: string | null;
20
+ }
21
+ /**
22
+ * Manages the session registry for multi-agent coordination.
23
+ * Uses PID lockfiles for safe concurrent access.
24
+ */
25
+ export declare class SessionRegistryManager {
26
+ private readonly ttlHours;
27
+ private readonly registryPath;
28
+ private readonly locksDir;
29
+ constructor(vaultPath: string, ttlHours: number);
30
+ /**
31
+ * Register a new session. Returns session ID and any conflicts.
32
+ */
33
+ register(tool: string, project: string | null, taskSummary?: string | null, filesTouched?: string[]): Promise<{
34
+ session_id: string;
35
+ conflicts: Conflict[];
36
+ }>;
37
+ /**
38
+ * Update heartbeat for a session. Returns false if session not found.
39
+ */
40
+ heartbeat(sessionId: string): Promise<boolean>;
41
+ /**
42
+ * Mark a session as completed.
43
+ */
44
+ complete(sessionId: string, summary?: string): Promise<void>;
45
+ /**
46
+ * List active sessions. Persists stale status changes.
47
+ */
48
+ listActive(): Promise<Session[]>;
49
+ private cleanOldCompleted;
50
+ private cleanStale;
51
+ private readRegistry;
52
+ private writeRegistry;
53
+ /**
54
+ * PID-based lockfile for safe concurrent writes.
55
+ * Times out after 2 seconds with retry.
56
+ */
57
+ private withLock;
58
+ }
59
+ export {};
@@ -0,0 +1,231 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later OR Commercial
2
+ import { readFile, writeFile, mkdir, unlink, stat, open } from "fs/promises";
3
+ import { resolve, dirname } from "path";
4
+ import { randomBytes } from "crypto";
5
+ /**
6
+ * Manages the session registry for multi-agent coordination.
7
+ * Uses PID lockfiles for safe concurrent access.
8
+ */
9
+ export class SessionRegistryManager {
10
+ ttlHours;
11
+ registryPath;
12
+ locksDir;
13
+ constructor(vaultPath, ttlHours) {
14
+ this.ttlHours = ttlHours;
15
+ this.registryPath = resolve(vaultPath, "coordination/session-registry.json");
16
+ this.locksDir = resolve(vaultPath, "coordination/locks");
17
+ }
18
+ /**
19
+ * Register a new session. Returns session ID and any conflicts.
20
+ */
21
+ async register(tool, project, taskSummary = null, filesTouched = []) {
22
+ const sessionId = `${tool}-${randomBytes(4).toString("hex")}`;
23
+ const now = new Date().toISOString();
24
+ const session = {
25
+ id: sessionId,
26
+ tool,
27
+ project,
28
+ task_summary: taskSummary,
29
+ files_touched: filesTouched,
30
+ started_at: now,
31
+ last_heartbeat: now,
32
+ completed_at: null,
33
+ status: "active",
34
+ };
35
+ return await this.withLock(async () => {
36
+ const registry = await this.readRegistry();
37
+ this.cleanStale(registry);
38
+ this.cleanOldCompleted(registry);
39
+ // Detect conflicts (use Set for O(n+m) intersection)
40
+ const conflicts = [];
41
+ if (filesTouched.length > 0) {
42
+ const touchedSet = new Set(filesTouched);
43
+ for (const existing of registry.sessions) {
44
+ if (existing.status !== "active")
45
+ continue;
46
+ const overlap = existing.files_touched.filter((f) => touchedSet.has(f));
47
+ if (overlap.length > 0) {
48
+ conflicts.push({
49
+ session_id: existing.id,
50
+ tool: existing.tool,
51
+ overlapping_files: overlap,
52
+ task_summary: existing.task_summary,
53
+ });
54
+ }
55
+ }
56
+ }
57
+ registry.sessions.push(session);
58
+ await this.writeRegistry(registry);
59
+ return { session_id: sessionId, conflicts };
60
+ });
61
+ }
62
+ /**
63
+ * Update heartbeat for a session. Returns false if session not found.
64
+ */
65
+ async heartbeat(sessionId) {
66
+ return await this.withLock(async () => {
67
+ const registry = await this.readRegistry();
68
+ const session = registry.sessions.find((s) => s.id === sessionId);
69
+ if (!session)
70
+ return false;
71
+ session.last_heartbeat = new Date().toISOString();
72
+ await this.writeRegistry(registry);
73
+ return true;
74
+ });
75
+ }
76
+ /**
77
+ * Mark a session as completed.
78
+ */
79
+ async complete(sessionId, summary) {
80
+ await this.withLock(async () => {
81
+ const registry = await this.readRegistry();
82
+ const session = registry.sessions.find((s) => s.id === sessionId);
83
+ if (session) {
84
+ session.status = "completed";
85
+ session.completed_at = new Date().toISOString();
86
+ if (summary)
87
+ session.task_summary = summary;
88
+ }
89
+ this.cleanOldCompleted(registry);
90
+ await this.writeRegistry(registry);
91
+ });
92
+ }
93
+ /**
94
+ * List active sessions. Persists stale status changes.
95
+ */
96
+ async listActive() {
97
+ return await this.withLock(async () => {
98
+ const registry = await this.readRegistry();
99
+ const hadStale = this.cleanStale(registry);
100
+ if (hadStale) {
101
+ await this.writeRegistry(registry);
102
+ }
103
+ return registry.sessions.filter((s) => s.status === "active");
104
+ });
105
+ }
106
+ cleanOldCompleted(registry) {
107
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
108
+ registry.sessions = registry.sessions.filter((s) => {
109
+ if (s.status !== "completed")
110
+ return true;
111
+ const ts = s.completed_at ?? s.started_at;
112
+ if (typeof ts !== "string")
113
+ return false;
114
+ const time = new Date(ts).getTime();
115
+ return !isNaN(time) && time > cutoff;
116
+ });
117
+ }
118
+ cleanStale(registry) {
119
+ let changed = false;
120
+ const cutoff = Date.now() - this.ttlHours * 60 * 60 * 1000;
121
+ for (const session of registry.sessions) {
122
+ if (session.status === "active" &&
123
+ new Date(session.last_heartbeat).getTime() < cutoff) {
124
+ session.status = "stale";
125
+ changed = true;
126
+ }
127
+ }
128
+ return changed;
129
+ }
130
+ async readRegistry() {
131
+ try {
132
+ const raw = await readFile(this.registryPath, "utf-8");
133
+ const parsed = JSON.parse(raw);
134
+ // Schema validation: ensure sessions is an array of valid objects
135
+ if (!parsed || !Array.isArray(parsed.sessions)) {
136
+ return { sessions: [] };
137
+ }
138
+ // Filter out malformed entries
139
+ const validSessions = parsed.sessions.filter((s) => typeof s === "object" &&
140
+ s !== null &&
141
+ typeof s.id === "string" &&
142
+ typeof s.tool === "string" &&
143
+ typeof s.status === "string" &&
144
+ typeof s.started_at === "string" &&
145
+ typeof s.last_heartbeat === "string" &&
146
+ !isNaN(new Date(s.last_heartbeat).getTime()) &&
147
+ Array.isArray(s.files_touched));
148
+ return { sessions: validSessions };
149
+ }
150
+ catch (e) {
151
+ if (e?.code !== "ENOENT") {
152
+ console.error("[session-registry] Error reading registry:", e.message);
153
+ }
154
+ return { sessions: [] };
155
+ }
156
+ }
157
+ async writeRegistry(registry) {
158
+ await mkdir(dirname(this.registryPath), { recursive: true });
159
+ await writeFile(this.registryPath, JSON.stringify(registry, null, 2), "utf-8");
160
+ }
161
+ /**
162
+ * PID-based lockfile for safe concurrent writes.
163
+ * Times out after 2 seconds with retry.
164
+ */
165
+ async withLock(fn) {
166
+ const lockPath = resolve(this.locksDir, "session-registry.lock");
167
+ await mkdir(this.locksDir, { recursive: true });
168
+ const maxRetries = 8;
169
+ const retryDelay = 100;
170
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
171
+ try {
172
+ const lockContent = JSON.stringify({
173
+ pid: process.pid,
174
+ tool: "superskill",
175
+ timestamp: new Date().toISOString(),
176
+ });
177
+ const fd = await open(lockPath, "wx");
178
+ try {
179
+ await fd.write(Buffer.from(lockContent, "utf-8"));
180
+ }
181
+ finally {
182
+ await fd.close();
183
+ }
184
+ // Lock acquired — run the function
185
+ try {
186
+ return await fn();
187
+ }
188
+ finally {
189
+ await unlink(lockPath).catch(() => { });
190
+ }
191
+ }
192
+ catch (e) {
193
+ if (e.code === "EEXIST") {
194
+ // Lock exists — check if it's stale
195
+ try {
196
+ const lockStat = await stat(lockPath);
197
+ const lockAge = Date.now() - lockStat.mtimeMs;
198
+ if (lockAge > 5000) {
199
+ // Lock is stale (>5s), force remove
200
+ await unlink(lockPath).catch(() => { });
201
+ continue;
202
+ }
203
+ // Lock is fresh — check if PID is alive
204
+ const lockData = JSON.parse(await readFile(lockPath, "utf-8"));
205
+ try {
206
+ process.kill(lockData.pid, 0);
207
+ // PID is alive — wait and retry
208
+ }
209
+ catch {
210
+ // PID is dead — remove stale lock
211
+ await unlink(lockPath).catch(() => { });
212
+ continue;
213
+ }
214
+ }
215
+ catch {
216
+ // Can't read lock — try to remove it
217
+ await unlink(lockPath).catch(() => { });
218
+ continue;
219
+ }
220
+ // Wait before retrying
221
+ await new Promise((r) => setTimeout(r, retryDelay));
222
+ }
223
+ else {
224
+ throw e;
225
+ }
226
+ }
227
+ }
228
+ throw new Error("Failed to acquire lock after retries");
229
+ }
230
+ }
231
+ //# sourceMappingURL=session-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../src/lib/session-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdir, writeFile as fspWriteFile, rm, readFile as fspReadFile } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { SessionRegistryManager } from "./session-registry.js";
6
+ describe("SessionRegistryManager", () => {
7
+ let vaultRoot;
8
+ let manager;
9
+ beforeEach(async () => {
10
+ vaultRoot = join(tmpdir(), `session-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
11
+ await mkdir(vaultRoot, { recursive: true });
12
+ manager = new SessionRegistryManager(vaultRoot, 1);
13
+ });
14
+ afterEach(async () => {
15
+ await rm(vaultRoot, { recursive: true, force: true });
16
+ });
17
+ describe("register", () => {
18
+ it("creates session with generated ID", async () => {
19
+ const result = await manager.register("claude-code", "test-project");
20
+ expect(result.session_id).toMatch(/^claude-code-[a-f0-9]{8}$/);
21
+ });
22
+ it("creates session with all fields", async () => {
23
+ const result = await manager.register("opencode", "my-project", "Doing work", ["file1.ts", "file2.ts"]);
24
+ expect(result.session_id).toMatch(/^opencode-/);
25
+ });
26
+ it("detects file conflicts", async () => {
27
+ await manager.register("claude-code", "proj", "Task 1", ["a.ts", "b.ts"]);
28
+ const result = await manager.register("opencode", "proj", "Task 2", ["b.ts", "c.ts"]);
29
+ expect(result.conflicts.length).toBe(1);
30
+ expect(result.conflicts[0].overlapping_files).toContain("b.ts");
31
+ });
32
+ it("returns empty conflicts when no overlap", async () => {
33
+ await manager.register("claude-code", "proj", "Task 1", ["a.ts"]);
34
+ const result = await manager.register("opencode", "proj", "Task 2", ["b.ts"]);
35
+ expect(result.conflicts).toEqual([]);
36
+ });
37
+ it("returns empty conflicts when first session has no files", async () => {
38
+ await manager.register("claude-code", "proj", "Task 1", []);
39
+ const result = await manager.register("opencode", "proj", "Task 2", ["b.ts"]);
40
+ expect(result.conflicts).toEqual([]);
41
+ });
42
+ it("cleans stale sessions on register", async () => {
43
+ const oldManager = new SessionRegistryManager(vaultRoot, 0);
44
+ await oldManager.register("claude-code", "proj", "Old session");
45
+ await new Promise(r => setTimeout(r, 10));
46
+ const newManager = new SessionRegistryManager(vaultRoot, 0.000001);
47
+ await newManager.register("opencode", "proj", "New session");
48
+ const active = await newManager.listActive();
49
+ expect(active.every(s => s.status === "active")).toBe(true);
50
+ });
51
+ it("persists session to registry file", async () => {
52
+ await manager.register("claude-code", "test-project");
53
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
54
+ const raw = await fspReadFile(registryPath, "utf-8");
55
+ const parsed = JSON.parse(raw);
56
+ expect(parsed.sessions.length).toBe(1);
57
+ expect(parsed.sessions[0].tool).toBe("claude-code");
58
+ });
59
+ });
60
+ describe("heartbeat", () => {
61
+ it("updates heartbeat timestamp", async () => {
62
+ const { session_id } = await manager.register("claude-code", "proj");
63
+ await new Promise(r => setTimeout(r, 10));
64
+ const result = await manager.heartbeat(session_id);
65
+ expect(result).toBe(true);
66
+ });
67
+ it("returns false for missing session", async () => {
68
+ const result = await manager.heartbeat("nonexistent-id");
69
+ expect(result).toBe(false);
70
+ });
71
+ it("persists heartbeat to file", async () => {
72
+ const { session_id } = await manager.register("claude-code", "proj");
73
+ await manager.heartbeat(session_id);
74
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
75
+ const raw = await fspReadFile(registryPath, "utf-8");
76
+ const parsed = JSON.parse(raw);
77
+ const session = parsed.sessions.find((s) => s.id === session_id);
78
+ expect(session.last_heartbeat).toBeDefined();
79
+ });
80
+ });
81
+ describe("complete", () => {
82
+ it("marks session as completed", async () => {
83
+ const { session_id } = await manager.register("claude-code", "proj");
84
+ await manager.complete(session_id);
85
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
86
+ const raw = await fspReadFile(registryPath, "utf-8");
87
+ const parsed = JSON.parse(raw);
88
+ const session = parsed.sessions.find((s) => s.id === session_id);
89
+ expect(session.status).toBe("completed");
90
+ expect(session.completed_at).toBeDefined();
91
+ });
92
+ it("updates task summary when provided", async () => {
93
+ const { session_id } = await manager.register("claude-code", "proj", "Old summary");
94
+ await manager.complete(session_id, "New summary");
95
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
96
+ const raw = await fspReadFile(registryPath, "utf-8");
97
+ const parsed = JSON.parse(raw);
98
+ const session = parsed.sessions.find((s) => s.id === session_id);
99
+ expect(session.task_summary).toBe("New summary");
100
+ });
101
+ it("removes old completed sessions", async () => {
102
+ const { session_id } = await manager.register("claude-code", "proj");
103
+ await manager.complete(session_id);
104
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
105
+ let raw = await fspReadFile(registryPath, "utf-8");
106
+ let parsed = JSON.parse(raw);
107
+ const session = parsed.sessions.find((s) => s.id === session_id);
108
+ session.completed_at = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
109
+ await fspWriteFile(registryPath, JSON.stringify(parsed));
110
+ await manager.register("opencode", "proj");
111
+ raw = await fspReadFile(registryPath, "utf-8");
112
+ parsed = JSON.parse(raw);
113
+ expect(parsed.sessions.find((s) => s.id === session_id)).toBeUndefined();
114
+ });
115
+ });
116
+ describe("listActive", () => {
117
+ it("returns only active sessions", async () => {
118
+ const { session_id } = await manager.register("claude-code", "proj");
119
+ await manager.register("opencode", "proj2");
120
+ await manager.complete(session_id);
121
+ const active = await manager.listActive();
122
+ expect(active.length).toBe(1);
123
+ expect(active[0].tool).toBe("opencode");
124
+ });
125
+ it("returns empty array when no active sessions", async () => {
126
+ const active = await manager.listActive();
127
+ expect(active).toEqual([]);
128
+ });
129
+ it("persists stale status changes", async () => {
130
+ const staleManager = new SessionRegistryManager(vaultRoot, 0.000001);
131
+ await staleManager.register("claude-code", "proj");
132
+ await new Promise(r => setTimeout(r, 20));
133
+ const active = await staleManager.listActive();
134
+ expect(active).toEqual([]);
135
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
136
+ const raw = await fspReadFile(registryPath, "utf-8");
137
+ const parsed = JSON.parse(raw);
138
+ expect(parsed.sessions[0].status).toBe("stale");
139
+ });
140
+ });
141
+ describe("withLock", () => {
142
+ it("prevents concurrent access", async () => {
143
+ const promises = [];
144
+ for (let i = 0; i < 5; i++) {
145
+ promises.push(manager.register(`tool-${i}`, "proj"));
146
+ }
147
+ const results = await Promise.all(promises);
148
+ expect(results.every(r => r.session_id)).toBe(true);
149
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
150
+ const raw = await fspReadFile(registryPath, "utf-8");
151
+ const parsed = JSON.parse(raw);
152
+ expect(parsed.sessions.length).toBe(5);
153
+ });
154
+ it("removes stale locks", async () => {
155
+ const locksDir = join(vaultRoot, "coordination/locks");
156
+ await mkdir(locksDir, { recursive: true });
157
+ const lockPath = join(locksDir, "session-registry.lock");
158
+ await fspWriteFile(lockPath, JSON.stringify({ pid: 999999999, timestamp: new Date().toISOString() }));
159
+ const { session_id } = await manager.register("claude-code", "proj");
160
+ expect(session_id).toBeDefined();
161
+ });
162
+ });
163
+ describe("readRegistry validation", () => {
164
+ it("handles malformed session entries", async () => {
165
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
166
+ await mkdir(join(vaultRoot, "coordination"), { recursive: true });
167
+ const now = new Date().toISOString();
168
+ const malformedData = {
169
+ sessions: [
170
+ { id: "valid-1", tool: "test", status: "active", started_at: now, last_heartbeat: now, files_touched: [] },
171
+ { id: 123, tool: "bad" },
172
+ null,
173
+ "string not object",
174
+ { id: "valid-2", tool: "test", status: "active", started_at: "bad-date", last_heartbeat: "2024-01-01T00:00:00Z", files_touched: [] },
175
+ ]
176
+ };
177
+ await fspWriteFile(registryPath, JSON.stringify(malformedData));
178
+ const active = await manager.listActive();
179
+ const validIds = active.map(s => s.id);
180
+ expect(validIds).toContain("valid-1");
181
+ expect(validIds).not.toContain("valid-2");
182
+ });
183
+ it("handles non-array sessions field", async () => {
184
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
185
+ await mkdir(join(vaultRoot, "coordination"), { recursive: true });
186
+ await fspWriteFile(registryPath, JSON.stringify({ sessions: "not-an-array" }));
187
+ const active = await manager.listActive();
188
+ expect(active).toEqual([]);
189
+ });
190
+ it("handles missing sessions field", async () => {
191
+ const registryPath = join(vaultRoot, "coordination/session-registry.json");
192
+ await mkdir(join(vaultRoot, "coordination"), { recursive: true });
193
+ await fspWriteFile(registryPath, JSON.stringify({}));
194
+ const active = await manager.listActive();
195
+ expect(active).toEqual([]);
196
+ });
197
+ });
198
+ });
199
+ //# sourceMappingURL=session-registry.test.js.map