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,124 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later OR Commercial
2
+ import { readFile } from "fs/promises";
3
+ import { resolve } from "path";
4
+ import { execFile } from "child_process";
5
+ import { promisify } from "util";
6
+ const execFileAsync = promisify(execFile);
7
+ const CACHE_TTL_MS = 60_000; // 1 minute
8
+ let cache = null;
9
+ export function invalidateProjectMapCache() {
10
+ cache = null;
11
+ }
12
+ function setCache(entry) {
13
+ cache = entry;
14
+ }
15
+ function getCache(vaultPath) {
16
+ const now = Date.now();
17
+ if (cache && cache.vaultPath === vaultPath && now - cache.time < CACHE_TTL_MS) {
18
+ return cache.map;
19
+ }
20
+ return null;
21
+ }
22
+ /**
23
+ * Load the project map from the vault. Cache is keyed by vaultPath.
24
+ */
25
+ async function loadProjectMap(vaultPath) {
26
+ const cached = getCache(vaultPath);
27
+ if (cached)
28
+ return cached;
29
+ const mapPath = resolve(vaultPath, "project-map.json");
30
+ try {
31
+ const raw = await readFile(mapPath, "utf-8");
32
+ const parsed = JSON.parse(raw);
33
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
34
+ return {};
35
+ }
36
+ const map = {};
37
+ for (const [key, value] of Object.entries(parsed)) {
38
+ if (typeof value === "string") {
39
+ map[key] = value;
40
+ }
41
+ }
42
+ setCache({ map, time: Date.now(), vaultPath });
43
+ return map;
44
+ }
45
+ catch (e) {
46
+ if (e?.code === "EACCES") {
47
+ console.error(`Warning: permission denied reading ${mapPath}`);
48
+ }
49
+ else if (e?.code !== "ENOENT" && !(e instanceof SyntaxError)) {
50
+ throw e;
51
+ }
52
+ return {};
53
+ }
54
+ }
55
+ /**
56
+ * Strip worktree suffixes from a path.
57
+ */
58
+ function stripWorktreeSuffix(cwd) {
59
+ // Pattern: /path/to/project/.worktrees/feat-name
60
+ const worktreeMatch = cwd.match(/^(.+?)\/\.worktrees\/[^/]+/);
61
+ if (worktreeMatch)
62
+ return worktreeMatch[1];
63
+ // Pattern: /path/to/project/.claude/worktrees/feat/name
64
+ const claudeWorktreeMatch = cwd.match(/^(.+?)\/\.claude\/worktrees\/.+/);
65
+ if (claudeWorktreeMatch)
66
+ return claudeWorktreeMatch[1];
67
+ // Pattern: /path/to/project/.claude-worktrees/name
68
+ const oldWorktreeMatch = cwd.match(/^(.+?)\/\.claude-worktrees\/[^/]+/);
69
+ if (oldWorktreeMatch)
70
+ return oldWorktreeMatch[1];
71
+ return cwd;
72
+ }
73
+ /**
74
+ * Try to detect git root of current directory (async, non-blocking).
75
+ */
76
+ async function getGitRoot(cwd) {
77
+ try {
78
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
79
+ cwd,
80
+ timeout: 3000,
81
+ });
82
+ return stdout.trim();
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ /**
89
+ * Detect the project slug from a working directory.
90
+ *
91
+ * Strategy:
92
+ * 1. Strip worktree suffixes from cwd
93
+ * 2. Try exact match in project-map.json
94
+ * 3. Try matching cwd as a subdirectory of a mapped path
95
+ * 4. Try git root as a mapped path
96
+ * 5. Return null if no match
97
+ */
98
+ export async function detectProject(cwd, vaultPath) {
99
+ const map = await loadProjectMap(vaultPath);
100
+ const cleanCwd = stripWorktreeSuffix(cwd);
101
+ // Exact match
102
+ if (map[cleanCwd])
103
+ return map[cleanCwd];
104
+ // Subdirectory match
105
+ for (const [path, slug] of Object.entries(map)) {
106
+ if (cleanCwd.startsWith(path + "/") || cleanCwd === path) {
107
+ return slug;
108
+ }
109
+ }
110
+ // Git root match
111
+ const gitRoot = await getGitRoot(cwd);
112
+ if (gitRoot) {
113
+ const cleanGitRoot = stripWorktreeSuffix(gitRoot);
114
+ if (map[cleanGitRoot])
115
+ return map[cleanGitRoot];
116
+ for (const [path, slug] of Object.entries(map)) {
117
+ if (cleanGitRoot.startsWith(path + "/") || cleanGitRoot === path) {
118
+ return slug;
119
+ }
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ //# sourceMappingURL=project-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-detector.js","sourceRoot":"","sources":["../../src/lib/project-detector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
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 { detectProject, invalidateProjectMapCache } from "./project-detector.js";
6
+ describe("project-detector", () => {
7
+ let vaultRoot;
8
+ let originalCwd;
9
+ beforeEach(async () => {
10
+ vaultRoot = join(tmpdir(), `project-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
11
+ await mkdir(vaultRoot, { recursive: true });
12
+ originalCwd = process.cwd();
13
+ invalidateProjectMapCache();
14
+ });
15
+ afterEach(async () => {
16
+ process.chdir(originalCwd);
17
+ await rm(vaultRoot, { recursive: true, force: true });
18
+ invalidateProjectMapCache();
19
+ });
20
+ describe("detectProject", () => {
21
+ it("returns null when no project-map.json", async () => {
22
+ const result = await detectProject("/nonexistent/path", vaultRoot);
23
+ expect(result).toBeNull();
24
+ });
25
+ it("detects project from exact cwd match in project-map.json", async () => {
26
+ const projectMap = { "/Users/test/myproject": "myproject" };
27
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap));
28
+ invalidateProjectMapCache();
29
+ const result = await detectProject("/Users/test/myproject", vaultRoot);
30
+ expect(result).toBe("myproject");
31
+ });
32
+ it("detects project from subdirectory of mapped path", async () => {
33
+ const projectMap = { "/Users/test/myproject": "myproject" };
34
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap));
35
+ invalidateProjectMapCache();
36
+ const result = await detectProject("/Users/test/myproject/src/components", vaultRoot);
37
+ expect(result).toBe("myproject");
38
+ });
39
+ it("returns null for unmapped path", async () => {
40
+ const projectMap = { "/Users/test/other": "other" };
41
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap));
42
+ invalidateProjectMapCache();
43
+ const result = await detectProject("/Users/test/unmapped", vaultRoot);
44
+ expect(result).toBeNull();
45
+ });
46
+ it("handles invalid JSON in project-map.json", async () => {
47
+ await writeFile(join(vaultRoot, "project-map.json"), "not valid json {{{");
48
+ invalidateProjectMapCache();
49
+ const result = await detectProject("/any/path", vaultRoot);
50
+ expect(result).toBeNull();
51
+ });
52
+ it("handles non-object project-map.json", async () => {
53
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(["array", "not", "object"]));
54
+ invalidateProjectMapCache();
55
+ const result = await detectProject("/any/path", vaultRoot);
56
+ expect(result).toBeNull();
57
+ });
58
+ it("strips .worktrees suffix from cwd", async () => {
59
+ const projectMap = { "/Users/test/myproject": "myproject" };
60
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap));
61
+ invalidateProjectMapCache();
62
+ const result = await detectProject("/Users/test/myproject/.worktrees/feat-x", vaultRoot);
63
+ expect(result).toBe("myproject");
64
+ });
65
+ it("strips .claude/worktrees suffix from cwd", async () => {
66
+ const projectMap = { "/Users/test/myproject": "myproject" };
67
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap));
68
+ invalidateProjectMapCache();
69
+ const result = await detectProject("/Users/test/myproject/.claude/worktrees/feat/name", vaultRoot);
70
+ expect(result).toBe("myproject");
71
+ });
72
+ it("strips .claude-worktrees suffix from cwd", async () => {
73
+ const projectMap = { "/Users/test/myproject": "myproject" };
74
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap));
75
+ invalidateProjectMapCache();
76
+ const result = await detectProject("/Users/test/myproject/.claude-worktrees/feat-name", vaultRoot);
77
+ expect(result).toBe("myproject");
78
+ });
79
+ it("ignores non-string values in project-map.json", async () => {
80
+ const projectMap = {
81
+ "/Users/test/good": "good",
82
+ "/Users/test/bad": 123,
83
+ "/Users/test/also-bad": null
84
+ };
85
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap));
86
+ invalidateProjectMapCache();
87
+ const goodResult = await detectProject("/Users/test/good", vaultRoot);
88
+ expect(goodResult).toBe("good");
89
+ const badResult = await detectProject("/Users/test/bad", vaultRoot);
90
+ expect(badResult).toBeNull();
91
+ });
92
+ it("caches project-map.json", async () => {
93
+ const projectMap = { "/Users/test/cached": "cached" };
94
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap));
95
+ invalidateProjectMapCache();
96
+ await detectProject("/Users/test/cached", vaultRoot);
97
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify({ "/Users/test/other": "other" }));
98
+ const result = await detectProject("/Users/test/cached", vaultRoot);
99
+ expect(result).toBe("cached");
100
+ });
101
+ it("cache invalidates for different vault path", async () => {
102
+ const projectMap1 = { "/Users/test/path1": "project1" };
103
+ await writeFile(join(vaultRoot, "project-map.json"), JSON.stringify(projectMap1));
104
+ invalidateProjectMapCache();
105
+ const vaultRoot2 = join(tmpdir(), `project-test-2-${Date.now()}`);
106
+ await mkdir(vaultRoot2, { recursive: true });
107
+ const projectMap2 = { "/Users/test/path1": "project2" };
108
+ await writeFile(join(vaultRoot2, "project-map.json"), JSON.stringify(projectMap2));
109
+ const result1 = await detectProject("/Users/test/path1", vaultRoot);
110
+ const result2 = await detectProject("/Users/test/path1", vaultRoot2);
111
+ expect(result1).toBe("project1");
112
+ expect(result2).toBe("project2");
113
+ await rm(vaultRoot2, { recursive: true, force: true });
114
+ });
115
+ });
116
+ });
117
+ //# sourceMappingURL=project-detector.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-detector.test.js","sourceRoot":"","sources":["../../src/lib/project-detector.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Validate that a path is safe to read from outside the vault.
3
+ * Used only by vault_init to scan git repos.
4
+ *
5
+ * Enforces:
6
+ * - Must be under $HOME
7
+ * - Must not contain .. or symlinks escaping home
8
+ * - Must be a git repository
9
+ */
10
+ export declare function safeExternalPath(rawPath: string): Promise<string>;
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later OR Commercial
2
+ import { homedir } from "os";
3
+ import { resolve } from "path";
4
+ import { realpath } from "fs/promises";
5
+ import { execFile } from "child_process";
6
+ import { promisify } from "util";
7
+ const execFileAsync = promisify(execFile);
8
+ /**
9
+ * Validate that a path is safe to read from outside the vault.
10
+ * Used only by vault_init to scan git repos.
11
+ *
12
+ * Enforces:
13
+ * - Must be under $HOME
14
+ * - Must not contain .. or symlinks escaping home
15
+ * - Must be a git repository
16
+ */
17
+ export async function safeExternalPath(rawPath) {
18
+ const home = homedir();
19
+ const resolved = resolve(rawPath.startsWith("~") ? rawPath.replace("~", home) : rawPath);
20
+ // Must be under home directory (after resolution, this catches traversal attacks)
21
+ if (resolved !== home && !resolved.startsWith(home + "/")) {
22
+ throw new Error(`Path must be under home directory. Got: ${resolved}`);
23
+ }
24
+ // Verify real path doesn't escape home (symlink check)
25
+ try {
26
+ const real = await realpath(resolved);
27
+ if (real !== home && !real.startsWith(home + "/")) {
28
+ throw new Error(`Symlink escapes home directory: ${rawPath}`);
29
+ }
30
+ }
31
+ catch (e) {
32
+ if (e.code === "ENOENT")
33
+ throw new Error(`Path does not exist: ${rawPath}`);
34
+ if (e.message?.includes("Symlink"))
35
+ throw e;
36
+ throw e;
37
+ }
38
+ // Must be a git repo
39
+ try {
40
+ await execFileAsync("git", ["rev-parse", "--git-dir"], { cwd: resolved, timeout: 3000 });
41
+ }
42
+ catch {
43
+ throw new Error(`Not a git repository: ${rawPath}`);
44
+ }
45
+ return resolved;
46
+ }
47
+ //# sourceMappingURL=safe-external-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-external-path.js","sourceRoot":"","sources":["../../src/lib/safe-external-path.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAe;IACpD,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAEzF,kFAAkF;IAClF,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,2CAA2C,QAAQ,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,uDAAuD;IACvD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,mCAAmC,OAAO,EAAE,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,OAAO,EAAE,CAAC,CAAC;QAC5E,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC;YAAE,MAAM,CAAC,CAAC;QAC5C,MAAM,CAAC,CAAC;IACV,CAAC;IAED,qBAAqB;IACrB,IAAI,CAAC;QACH,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3F,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdir, rm, symlink } from "fs/promises";
3
+ import { tmpdir, homedir } from "os";
4
+ import { join } from "path";
5
+ import { safeExternalPath } from "./safe-external-path.js";
6
+ import { execFile } from "child_process";
7
+ import { promisify } from "util";
8
+ const execFileAsync = promisify(execFile);
9
+ describe("safeExternalPath", () => {
10
+ let testDir;
11
+ const home = homedir();
12
+ beforeEach(async () => {
13
+ testDir = join(tmpdir(), `safe-path-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
14
+ await mkdir(testDir, { recursive: true });
15
+ await execFileAsync("git", ["init"], { cwd: testDir });
16
+ });
17
+ afterEach(async () => {
18
+ await rm(testDir, { recursive: true, force: true });
19
+ });
20
+ describe("valid paths", () => {
21
+ it("accepts path under home directory", async () => {
22
+ const homeTestDir = join(home, `.safe-path-test-${Date.now()}`);
23
+ await mkdir(homeTestDir, { recursive: true });
24
+ await execFileAsync("git", ["init"], { cwd: homeTestDir });
25
+ const result = await safeExternalPath(homeTestDir);
26
+ expect(result).toBe(homeTestDir);
27
+ await rm(homeTestDir, { recursive: true, force: true });
28
+ });
29
+ it("accepts path with ~ prefix", async () => {
30
+ const homeTestDir = join(home, `.safe-path-test-tilde-${Date.now()}`);
31
+ await mkdir(homeTestDir, { recursive: true });
32
+ await execFileAsync("git", ["init"], { cwd: homeTestDir });
33
+ const result = await safeExternalPath(homeTestDir.replace(home, "~"));
34
+ expect(result).toBe(homeTestDir);
35
+ await rm(homeTestDir, { recursive: true, force: true });
36
+ });
37
+ it("resolves relative path segments", async () => {
38
+ const homeTestDir = join(home, `.safe-path-test-rel-${Date.now()}`);
39
+ await mkdir(homeTestDir, { recursive: true });
40
+ await execFileAsync("git", ["init"], { cwd: homeTestDir });
41
+ const result = await safeExternalPath(`${homeTestDir}/./subdir/..`);
42
+ expect(result).toBe(homeTestDir);
43
+ await rm(homeTestDir, { recursive: true, force: true });
44
+ });
45
+ });
46
+ describe("traversal attack prevention", () => {
47
+ it("rejects path with .. traversal", async () => {
48
+ await expect(safeExternalPath("/Users/../../../etc/passwd")).rejects.toThrow("Path must be under home directory");
49
+ });
50
+ });
51
+ describe("symlink escape prevention", () => {
52
+ it("rejects symlink escaping home directory", async () => {
53
+ const outsideDir = join(tmpdir(), `outside-safe-${Date.now()}`);
54
+ await mkdir(outsideDir, { recursive: true });
55
+ const linkPath = join(testDir, "escape-link");
56
+ try {
57
+ await symlink(outsideDir, linkPath);
58
+ await expect(safeExternalPath(linkPath)).rejects.toThrow();
59
+ }
60
+ finally {
61
+ await rm(outsideDir, { recursive: true, force: true });
62
+ }
63
+ });
64
+ it("accepts symlink within home directory", async () => {
65
+ const homeTestDir = join(home, `.safe-path-test-link-${Date.now()}`);
66
+ await mkdir(homeTestDir, { recursive: true });
67
+ await execFileAsync("git", ["init"], { cwd: homeTestDir });
68
+ const linkPath = join(home, `.safe-path-link-${Date.now()}`);
69
+ await symlink(homeTestDir, linkPath);
70
+ const result = await safeExternalPath(linkPath);
71
+ expect(result).toBeDefined();
72
+ await rm(linkPath, { force: true });
73
+ await rm(homeTestDir, { recursive: true, force: true });
74
+ });
75
+ });
76
+ describe("non-home directory rejection", () => {
77
+ it("rejects path outside home directory", async () => {
78
+ await expect(safeExternalPath("/etc/passwd")).rejects.toThrow("Path must be under home directory");
79
+ });
80
+ it("rejects /tmp directly (outside home)", async () => {
81
+ await expect(safeExternalPath("/tmp")).rejects.toThrow("Path must be under home directory");
82
+ });
83
+ });
84
+ describe("git repository check", () => {
85
+ it("rejects non-git directory", async () => {
86
+ const homeNonGit = join(home, `.safe-path-non-git-${Date.now()}`);
87
+ await mkdir(homeNonGit, { recursive: true });
88
+ await expect(safeExternalPath(homeNonGit)).rejects.toThrow("Not a git repository");
89
+ await rm(homeNonGit, { recursive: true, force: true });
90
+ });
91
+ });
92
+ describe("nonexistent path handling", () => {
93
+ it("rejects nonexistent path", async () => {
94
+ const nonexistent = join(home, `.nonexistent-path-${Date.now()}`);
95
+ await expect(safeExternalPath(nonexistent)).rejects.toThrow("Path does not exist");
96
+ });
97
+ });
98
+ });
99
+ //# sourceMappingURL=safe-external-path.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-external-path.test.js","sourceRoot":"","sources":["../../src/lib/safe-external-path.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export interface SearchResult {
2
+ path: string;
3
+ snippet: string;
4
+ line: number;
5
+ }
6
+ /**
7
+ * Full-text search using ripgrep.
8
+ */
9
+ export declare function searchText(vaultPath: string, query: string, options?: {
10
+ pathFilter?: string;
11
+ limit?: number;
12
+ }): Promise<SearchResult[]>;
13
+ /**
14
+ * Structured search by frontmatter properties.
15
+ * Scans files and filters by frontmatter fields.
16
+ */
17
+ export declare function searchStructured(vaultPath: string, filters: Record<string, string>, options?: {
18
+ limit?: number;
19
+ }): Promise<SearchResult[]>;
@@ -0,0 +1,164 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later OR Commercial
2
+ import { execFile } from "child_process";
3
+ import { promisify } from "util";
4
+ import { resolve, relative } from "path";
5
+ import { readFile, stat } from "fs/promises";
6
+ import { parseFrontmatter } from "./frontmatter.js";
7
+ import { escapeRegex } from "./escape-regex.js";
8
+ const execFileAsync = promisify(execFile);
9
+ /**
10
+ * Validate that a search path stays within the vault root.
11
+ * Throws if the path escapes the vault boundary.
12
+ */
13
+ function validateSearchPath(vaultPath, searchPath) {
14
+ const resolved = resolve(vaultPath, searchPath);
15
+ const rel = relative(vaultPath, resolved);
16
+ if (rel.startsWith("..") || rel.startsWith("/")) {
17
+ throw new Error(`Search path escapes vault: ${searchPath}`);
18
+ }
19
+ return resolved;
20
+ }
21
+ /**
22
+ * Convert an absolute path from rg output to a vault-relative path.
23
+ * Uses path.relative for correctness instead of string replacement.
24
+ */
25
+ function toRelativePath(vaultPath, absPath) {
26
+ return relative(vaultPath, absPath);
27
+ }
28
+ /**
29
+ * Full-text search using ripgrep.
30
+ */
31
+ export async function searchText(vaultPath, query, options = {}) {
32
+ const { pathFilter, limit = 10 } = options;
33
+ const args = [
34
+ "--json",
35
+ "--max-count", "1", // one match per file
36
+ "--type", "md",
37
+ "--ignore-case",
38
+ "--fixed-strings",
39
+ query,
40
+ ];
41
+ // Validate pathFilter stays within vault
42
+ const searchPath = pathFilter
43
+ ? validateSearchPath(vaultPath, pathFilter)
44
+ : vaultPath;
45
+ try {
46
+ const { stdout } = await execFileAsync("rg", [...args, searchPath], {
47
+ timeout: 10_000,
48
+ maxBuffer: 1024 * 1024,
49
+ });
50
+ const results = [];
51
+ for (const line of stdout.split("\n").filter(Boolean)) {
52
+ try {
53
+ const parsed = JSON.parse(line);
54
+ if (parsed.type === "match") {
55
+ const absPath = parsed.data.path.text;
56
+ const relPath = toRelativePath(vaultPath, absPath);
57
+ const snippet = parsed.data.lines.text.trim().slice(0, 200);
58
+ const lineNum = parsed.data.line_number;
59
+ // Skip hidden dirs and paths escaping vault
60
+ if (relPath.startsWith(".") || relPath.startsWith(".."))
61
+ continue;
62
+ results.push({ path: relPath, snippet, line: lineNum });
63
+ if (results.length >= limit)
64
+ break;
65
+ }
66
+ }
67
+ catch (e) {
68
+ if (e.code !== "ENOENT") {
69
+ console.error("[search] Skipping malformed rg line:", e.message);
70
+ }
71
+ }
72
+ }
73
+ return results;
74
+ }
75
+ catch (e) {
76
+ if (e.code === "ENOENT" && e.message.includes("rg")) {
77
+ console.error("[search] ripgrep (rg) not found. Install it or search will be unavailable.");
78
+ throw new Error("ripgrep (rg) is required for text search. Install it: https://github.com/BurntSushi/ripgrep");
79
+ }
80
+ if (e.code === 1 || e.status === 1)
81
+ return [];
82
+ throw e;
83
+ }
84
+ }
85
+ /**
86
+ * Structured search by frontmatter properties.
87
+ * Scans files and filters by frontmatter fields.
88
+ */
89
+ export async function searchStructured(vaultPath, filters, options = {}) {
90
+ const { limit = 10 } = options;
91
+ const results = [];
92
+ // Build grep patterns to narrow candidates (search for both key and value)
93
+ // SECURITY: escape both key and value to prevent regex injection
94
+ // Note: We search separately because YAML arrays span multiple lines
95
+ const firstKey = Object.keys(filters)[0];
96
+ const firstValue = filters[firstKey];
97
+ const keyPattern = escapeRegex(firstKey) + ":";
98
+ const valuePattern = escapeRegex(firstValue);
99
+ let candidates;
100
+ try {
101
+ const { stdout } = await execFileAsync("rg", [
102
+ "--files-with-matches",
103
+ "--type", "md",
104
+ "--regexp", keyPattern,
105
+ "--regexp", valuePattern,
106
+ vaultPath,
107
+ ], { timeout: 10_000, maxBuffer: 1024 * 1024 });
108
+ candidates = stdout.trim().split("\n").filter(Boolean);
109
+ }
110
+ catch (e) {
111
+ if (e.code === "ENOENT" && e.message.includes("rg")) {
112
+ throw new Error("ripgrep (rg) is required for structured search. Install it: https://github.com/BurntSushi/ripgrep");
113
+ }
114
+ if (e.code === 1 || e.status === 1)
115
+ return [];
116
+ throw e;
117
+ }
118
+ const MAX_FILE_SIZE = 100_000; // 100 KB — skip large files in structured search
119
+ for (const absPath of candidates) {
120
+ if (results.length >= limit)
121
+ break;
122
+ const relPath = toRelativePath(vaultPath, absPath);
123
+ if (relPath.startsWith(".") || relPath.startsWith(".."))
124
+ continue;
125
+ try {
126
+ const fileStat = await stat(absPath);
127
+ if (fileStat.size > MAX_FILE_SIZE)
128
+ continue;
129
+ const content = await readFile(absPath, "utf-8");
130
+ const { data } = parseFrontmatter(content);
131
+ // Check all filters match
132
+ let match = true;
133
+ for (const [key, value] of Object.entries(filters)) {
134
+ const fieldValue = data[key];
135
+ if (Array.isArray(fieldValue)) {
136
+ if (!fieldValue.includes(value)) {
137
+ match = false;
138
+ break;
139
+ }
140
+ }
141
+ else if (String(fieldValue) !== value) {
142
+ match = false;
143
+ break;
144
+ }
145
+ }
146
+ if (match) {
147
+ const lines = content.split("\n");
148
+ const firstContentLine = lines.find((l) => l.startsWith("# ") || (l.trim() && !l.startsWith("---") && !l.match(/^\w+:\s/)));
149
+ results.push({
150
+ path: relPath,
151
+ snippet: firstContentLine?.trim().slice(0, 200) ?? "",
152
+ line: 1,
153
+ });
154
+ }
155
+ }
156
+ catch (e) {
157
+ if (e.code !== "ENOENT") {
158
+ console.error(`[search] Skipping unreadable file ${absPath}:`, e.message);
159
+ }
160
+ }
161
+ }
162
+ return results;
163
+ }
164
+ //# sourceMappingURL=search-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-engine.js","sourceRoot":"","sources":["../../src/lib/search-engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};