@kb-labs/adapters 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/.cursorrules +32 -0
  2. package/.github/workflows/ci.yml +13 -0
  3. package/.github/workflows/deploy.yml +28 -0
  4. package/.github/workflows/docker-build.yml +25 -0
  5. package/.github/workflows/drift-check.yml +10 -0
  6. package/.github/workflows/profiles-validate.yml +16 -0
  7. package/.github/workflows/release.yml +8 -0
  8. package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
  9. package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
  10. package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
  11. package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
  12. package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
  13. package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
  14. package/.kb/devkit/agents/release-manager/context.globs +7 -0
  15. package/.kb/devkit/agents/release-manager/prompt.md +27 -0
  16. package/.kb/devkit/agents/release-manager/runbook.md +17 -0
  17. package/.kb/devkit/agents/test-generator/context.globs +7 -0
  18. package/.kb/devkit/agents/test-generator/prompt.md +27 -0
  19. package/.kb/devkit/agents/test-generator/runbook.md +18 -0
  20. package/CONTRIBUTING.md +90 -0
  21. package/IMPLEMENTATION_COMPLETE.md +416 -0
  22. package/LICENSE +186 -0
  23. package/README-TEMPLATE.md +179 -0
  24. package/README.md +306 -0
  25. package/docs/DOCUMENTATION.md +74 -0
  26. package/docs/adr/0000-template.md +49 -0
  27. package/docs/adr/0001-architecture-and-repository-layout.md +33 -0
  28. package/docs/adr/0002-plugins-and-extensibility.md +46 -0
  29. package/docs/adr/0003-package-and-module-boundaries.md +37 -0
  30. package/docs/adr/0004-versioning-and-release-policy.md +38 -0
  31. package/docs/adr/0005-use-devkit-for-shared-tooling.md +48 -0
  32. package/docs/adr/0006-adopt-devkit-sync.md +47 -0
  33. package/docs/adr/0007-drift-kit-check.md +72 -0
  34. package/docs/adr/0008-devkit-sync-wrapper-strategy.md +67 -0
  35. package/docs/naming-convention.md +272 -0
  36. package/eslint.config.js +27 -0
  37. package/kb-labs.config.json +5 -0
  38. package/package.json +84 -0
  39. package/package.json.bin +25 -0
  40. package/package.json.lib +30 -0
  41. package/packages/adapters-analytics-duckdb/package.json +54 -0
  42. package/packages/adapters-analytics-duckdb/scripts/migrate-from-jsonl.mjs +253 -0
  43. package/packages/adapters-analytics-duckdb/src/index.ts +380 -0
  44. package/packages/adapters-analytics-duckdb/src/manifest.ts +36 -0
  45. package/packages/adapters-analytics-duckdb/src/schema.ts +161 -0
  46. package/packages/adapters-analytics-duckdb/tsconfig.build.json +15 -0
  47. package/packages/adapters-analytics-duckdb/tsconfig.json +9 -0
  48. package/packages/adapters-analytics-duckdb/tsup.config.ts +9 -0
  49. package/packages/adapters-analytics-file/README.md +32 -0
  50. package/packages/adapters-analytics-file/eslint.config.js +27 -0
  51. package/packages/adapters-analytics-file/package.json +50 -0
  52. package/packages/adapters-analytics-file/src/__tests__/daily-stats.spec.ts +287 -0
  53. package/packages/adapters-analytics-file/src/__tests__/scoped-analytics.test.ts +233 -0
  54. package/packages/adapters-analytics-file/src/index.test.ts +214 -0
  55. package/packages/adapters-analytics-file/src/index.ts +830 -0
  56. package/packages/adapters-analytics-file/src/manifest.ts +45 -0
  57. package/packages/adapters-analytics-file/tsconfig.build.json +15 -0
  58. package/packages/adapters-analytics-file/tsconfig.json +9 -0
  59. package/packages/adapters-analytics-file/tsup.config.ts +9 -0
  60. package/packages/adapters-analytics-sqlite/package.json +55 -0
  61. package/packages/adapters-analytics-sqlite/scripts/migrate-from-jsonl.mjs +194 -0
  62. package/packages/adapters-analytics-sqlite/src/index.ts +460 -0
  63. package/packages/adapters-analytics-sqlite/src/manifest.ts +41 -0
  64. package/packages/adapters-analytics-sqlite/tsconfig.build.json +15 -0
  65. package/packages/adapters-analytics-sqlite/tsconfig.json +9 -0
  66. package/packages/adapters-analytics-sqlite/tsup.config.ts +9 -0
  67. package/packages/adapters-environment-docker/README.md +28 -0
  68. package/packages/adapters-environment-docker/eslint.config.js +5 -0
  69. package/packages/adapters-environment-docker/package.json +49 -0
  70. package/packages/adapters-environment-docker/src/index.test.ts +138 -0
  71. package/packages/adapters-environment-docker/src/index.ts +439 -0
  72. package/packages/adapters-environment-docker/src/manifest.ts +65 -0
  73. package/packages/adapters-environment-docker/tsconfig.build.json +15 -0
  74. package/packages/adapters-environment-docker/tsconfig.json +16 -0
  75. package/packages/adapters-environment-docker/tsup.config.ts +9 -0
  76. package/packages/adapters-eventbus-cache/README.md +242 -0
  77. package/packages/adapters-eventbus-cache/eslint.config.js +27 -0
  78. package/packages/adapters-eventbus-cache/package.json +46 -0
  79. package/packages/adapters-eventbus-cache/src/index.test.ts +235 -0
  80. package/packages/adapters-eventbus-cache/src/index.ts +215 -0
  81. package/packages/adapters-eventbus-cache/src/manifest.ts +50 -0
  82. package/packages/adapters-eventbus-cache/src/types.ts +58 -0
  83. package/packages/adapters-eventbus-cache/tsconfig.build.json +15 -0
  84. package/packages/adapters-eventbus-cache/tsconfig.json +9 -0
  85. package/packages/adapters-eventbus-cache/tsup.config.ts +9 -0
  86. package/packages/adapters-fs/README.md +171 -0
  87. package/packages/adapters-fs/allowed.txt +1 -0
  88. package/packages/adapters-fs/conflict.txt +1 -0
  89. package/packages/adapters-fs/dest.txt +1 -0
  90. package/packages/adapters-fs/eslint.config.js +27 -0
  91. package/packages/adapters-fs/exists.txt +1 -0
  92. package/packages/adapters-fs/not-allowed.txt +1 -0
  93. package/packages/adapters-fs/other.txt +1 -0
  94. package/packages/adapters-fs/package.json +55 -0
  95. package/packages/adapters-fs/public/file1.txt +1 -0
  96. package/packages/adapters-fs/public/file2.txt +1 -0
  97. package/packages/adapters-fs/secret.txt +1 -0
  98. package/packages/adapters-fs/secrets/key.txt +1 -0
  99. package/packages/adapters-fs/src/index.test.ts +243 -0
  100. package/packages/adapters-fs/src/index.ts +258 -0
  101. package/packages/adapters-fs/src/manifest.ts +35 -0
  102. package/packages/adapters-fs/src/secure-storage.test.ts +380 -0
  103. package/packages/adapters-fs/src/secure-storage.ts +268 -0
  104. package/packages/adapters-fs/test.json +1 -0
  105. package/packages/adapters-fs/test.txt +1 -0
  106. package/packages/adapters-fs/test.xyz +1 -0
  107. package/packages/adapters-fs/test1.txt +1 -0
  108. package/packages/adapters-fs/test2.txt +1 -0
  109. package/packages/adapters-fs/tsconfig.build.json +15 -0
  110. package/packages/adapters-fs/tsconfig.json +9 -0
  111. package/packages/adapters-fs/tsup.config.ts +8 -0
  112. package/packages/adapters-fs/vitest.config.ts +19 -0
  113. package/packages/adapters-log-ringbuffer/README.md +228 -0
  114. package/packages/adapters-log-ringbuffer/eslint.config.js +27 -0
  115. package/packages/adapters-log-ringbuffer/package.json +47 -0
  116. package/packages/adapters-log-ringbuffer/src/__tests__/ring-buffer.test.ts +450 -0
  117. package/packages/adapters-log-ringbuffer/src/index.ts +212 -0
  118. package/packages/adapters-log-ringbuffer/src/manifest.ts +30 -0
  119. package/packages/adapters-log-ringbuffer/tsconfig.build.json +15 -0
  120. package/packages/adapters-log-ringbuffer/tsconfig.json +9 -0
  121. package/packages/adapters-log-ringbuffer/tsup.config.ts +9 -0
  122. package/packages/adapters-log-ringbuffer/vitest.config.ts +14 -0
  123. package/packages/adapters-log-sqlite/README.md +396 -0
  124. package/packages/adapters-log-sqlite/eslint.config.js +27 -0
  125. package/packages/adapters-log-sqlite/package.json +49 -0
  126. package/packages/adapters-log-sqlite/src/__tests__/log-persistence.test.ts +718 -0
  127. package/packages/adapters-log-sqlite/src/index.ts +1068 -0
  128. package/packages/adapters-log-sqlite/src/manifest.ts +36 -0
  129. package/packages/adapters-log-sqlite/src/schema.sql +46 -0
  130. package/packages/adapters-log-sqlite/tsconfig.build.json +15 -0
  131. package/packages/adapters-log-sqlite/tsconfig.json +9 -0
  132. package/packages/adapters-log-sqlite/tsup.config.ts +9 -0
  133. package/packages/adapters-log-sqlite/vitest.config.ts +15 -0
  134. package/packages/adapters-mongodb/README.md +147 -0
  135. package/packages/adapters-mongodb/eslint.config.js +27 -0
  136. package/packages/adapters-mongodb/package.json +53 -0
  137. package/packages/adapters-mongodb/src/index.ts +428 -0
  138. package/packages/adapters-mongodb/src/manifest.ts +45 -0
  139. package/packages/adapters-mongodb/src/secure-document.ts +231 -0
  140. package/packages/adapters-mongodb/tsconfig.build.json +15 -0
  141. package/packages/adapters-mongodb/tsconfig.json +9 -0
  142. package/packages/adapters-mongodb/tsup.config.ts +8 -0
  143. package/packages/adapters-openai/README.md +151 -0
  144. package/packages/adapters-openai/embeddings.ts +37 -0
  145. package/packages/adapters-openai/eslint.config.js +26 -0
  146. package/packages/adapters-openai/index.ts +22 -0
  147. package/packages/adapters-openai/package.json +57 -0
  148. package/packages/adapters-openai/src/embeddings-manifest.ts +45 -0
  149. package/packages/adapters-openai/src/embeddings.ts +104 -0
  150. package/packages/adapters-openai/src/index.ts +13 -0
  151. package/packages/adapters-openai/src/llm.ts +304 -0
  152. package/packages/adapters-openai/src/manifest.ts +47 -0
  153. package/packages/adapters-openai/tsconfig.build.json +15 -0
  154. package/packages/adapters-openai/tsconfig.json +9 -0
  155. package/packages/adapters-openai/tsup.config.ts +8 -0
  156. package/packages/adapters-pino/README.md +152 -0
  157. package/packages/adapters-pino/eslint.config.js +27 -0
  158. package/packages/adapters-pino/package.json +49 -0
  159. package/packages/adapters-pino/src/index.test.ts +44 -0
  160. package/packages/adapters-pino/src/index.ts +322 -0
  161. package/packages/adapters-pino/src/log-ring-buffer.ts +142 -0
  162. package/packages/adapters-pino/src/manifest.ts +49 -0
  163. package/packages/adapters-pino/tsconfig.build.json +15 -0
  164. package/packages/adapters-pino/tsconfig.json +9 -0
  165. package/packages/adapters-pino/tsup.config.ts +9 -0
  166. package/packages/adapters-pino-http/README.md +141 -0
  167. package/packages/adapters-pino-http/eslint.config.js +27 -0
  168. package/packages/adapters-pino-http/package.json +46 -0
  169. package/packages/adapters-pino-http/src/index.ts +229 -0
  170. package/packages/adapters-pino-http/tsconfig.build.json +15 -0
  171. package/packages/adapters-pino-http/tsconfig.json +9 -0
  172. package/packages/adapters-pino-http/tsup.config.ts +9 -0
  173. package/packages/adapters-qdrant/README.md +166 -0
  174. package/packages/adapters-qdrant/eslint.config.js +27 -0
  175. package/packages/adapters-qdrant/package.json +49 -0
  176. package/packages/adapters-qdrant/src/index.ts +490 -0
  177. package/packages/adapters-qdrant/src/manifest.ts +54 -0
  178. package/packages/adapters-qdrant/src/retry.ts +204 -0
  179. package/packages/adapters-qdrant/tsconfig.build.json +15 -0
  180. package/packages/adapters-qdrant/tsconfig.json +9 -0
  181. package/packages/adapters-qdrant/tsup.config.ts +9 -0
  182. package/packages/adapters-redis/README.md +159 -0
  183. package/packages/adapters-redis/eslint.config.js +27 -0
  184. package/packages/adapters-redis/package.json +49 -0
  185. package/packages/adapters-redis/src/index.ts +164 -0
  186. package/packages/adapters-redis/src/manifest.ts +49 -0
  187. package/packages/adapters-redis/tsconfig.build.json +15 -0
  188. package/packages/adapters-redis/tsconfig.json +9 -0
  189. package/packages/adapters-redis/tsup.config.ts +9 -0
  190. package/packages/adapters-snapshot-localfs/README.md +10 -0
  191. package/packages/adapters-snapshot-localfs/eslint.config.js +2 -0
  192. package/packages/adapters-snapshot-localfs/package.json +46 -0
  193. package/packages/adapters-snapshot-localfs/src/index.test.ts +40 -0
  194. package/packages/adapters-snapshot-localfs/src/index.ts +292 -0
  195. package/packages/adapters-snapshot-localfs/src/manifest.ts +32 -0
  196. package/packages/adapters-snapshot-localfs/tsconfig.build.json +15 -0
  197. package/packages/adapters-snapshot-localfs/tsconfig.json +16 -0
  198. package/packages/adapters-snapshot-localfs/tsup.config.ts +11 -0
  199. package/packages/adapters-sqlite/README.md +163 -0
  200. package/packages/adapters-sqlite/eslint.config.js +27 -0
  201. package/packages/adapters-sqlite/package.json +54 -0
  202. package/packages/adapters-sqlite/src/index.test.ts +245 -0
  203. package/packages/adapters-sqlite/src/index.ts +382 -0
  204. package/packages/adapters-sqlite/src/manifest.ts +47 -0
  205. package/packages/adapters-sqlite/src/secure-sql.test.ts +290 -0
  206. package/packages/adapters-sqlite/src/secure-sql.ts +281 -0
  207. package/packages/adapters-sqlite/tsconfig.build.json +15 -0
  208. package/packages/adapters-sqlite/tsconfig.json +9 -0
  209. package/packages/adapters-sqlite/tsup.config.ts +8 -0
  210. package/packages/adapters-sqlite/vitest.config.ts +19 -0
  211. package/packages/adapters-transport/README.md +170 -0
  212. package/packages/adapters-transport/eslint.config.js +27 -0
  213. package/packages/adapters-transport/package.json +49 -0
  214. package/packages/adapters-transport/src/__tests__/unix-socket-server.test.ts +550 -0
  215. package/packages/adapters-transport/src/index.ts +101 -0
  216. package/packages/adapters-transport/src/ipc-transport.ts +228 -0
  217. package/packages/adapters-transport/src/transport.ts +224 -0
  218. package/packages/adapters-transport/src/types.ts +92 -0
  219. package/packages/adapters-transport/src/unix-socket-server.ts +193 -0
  220. package/packages/adapters-transport/src/unix-socket-transport.ts +280 -0
  221. package/packages/adapters-transport/tsconfig.build.json +15 -0
  222. package/packages/adapters-transport/tsconfig.json +9 -0
  223. package/packages/adapters-transport/tsup.config.ts +9 -0
  224. package/packages/adapters-vibeproxy/README.md +159 -0
  225. package/packages/adapters-vibeproxy/eslint.config.js +27 -0
  226. package/packages/adapters-vibeproxy/package.json +51 -0
  227. package/packages/adapters-vibeproxy/src/index.ts +13 -0
  228. package/packages/adapters-vibeproxy/src/llm.ts +437 -0
  229. package/packages/adapters-vibeproxy/src/manifest.ts +51 -0
  230. package/packages/adapters-vibeproxy/tsconfig.build.json +15 -0
  231. package/packages/adapters-vibeproxy/tsconfig.json +9 -0
  232. package/packages/adapters-vibeproxy/tsup.config.ts +8 -0
  233. package/packages/adapters-workspace-agent/package.json +46 -0
  234. package/packages/adapters-workspace-agent/src/__tests__/adapter.test.ts +212 -0
  235. package/packages/adapters-workspace-agent/src/index.ts +220 -0
  236. package/packages/adapters-workspace-agent/src/manifest.ts +36 -0
  237. package/packages/adapters-workspace-agent/tsconfig.build.json +15 -0
  238. package/packages/adapters-workspace-agent/tsconfig.json +16 -0
  239. package/packages/adapters-workspace-agent/tsup.config.ts +11 -0
  240. package/packages/adapters-workspace-localfs/README.md +9 -0
  241. package/packages/adapters-workspace-localfs/eslint.config.js +2 -0
  242. package/packages/adapters-workspace-localfs/package.json +46 -0
  243. package/packages/adapters-workspace-localfs/src/index.test.ts +27 -0
  244. package/packages/adapters-workspace-localfs/src/index.ts +172 -0
  245. package/packages/adapters-workspace-localfs/src/manifest.ts +32 -0
  246. package/packages/adapters-workspace-localfs/tsconfig.build.json +15 -0
  247. package/packages/adapters-workspace-localfs/tsconfig.json +16 -0
  248. package/packages/adapters-workspace-localfs/tsup.config.ts +11 -0
  249. package/packages/adapters-workspace-worktree/README.md +9 -0
  250. package/packages/adapters-workspace-worktree/eslint.config.js +2 -0
  251. package/packages/adapters-workspace-worktree/package.json +46 -0
  252. package/packages/adapters-workspace-worktree/src/index.test.ts +38 -0
  253. package/packages/adapters-workspace-worktree/src/index.ts +245 -0
  254. package/packages/adapters-workspace-worktree/src/manifest.ts +38 -0
  255. package/packages/adapters-workspace-worktree/tsconfig.build.json +15 -0
  256. package/packages/adapters-workspace-worktree/tsconfig.json +16 -0
  257. package/packages/adapters-workspace-worktree/tsup.config.ts +11 -0
  258. package/pnpm-workspace.yaml +2800 -0
  259. package/prettierrc.json +1 -0
  260. package/scripts/devkit-sync.mjs +37 -0
  261. package/scripts/hooks/post-push +9 -0
  262. package/scripts/hooks/pre-commit +9 -0
  263. package/scripts/hooks/pre-push +9 -0
  264. package/test-integration.ts +242 -0
  265. package/test.txt +1 -0
  266. package/tsconfig.base.json +6 -0
  267. package/tsconfig.build.json +15 -0
  268. package/tsconfig.json +9 -0
  269. package/tsconfig.paths.json +26 -0
  270. package/tsconfig.tools.json +17 -0
  271. package/tsup.config.bin.ts +34 -0
  272. package/tsup.config.cli.ts +41 -0
  273. package/tsup.config.dual.ts +46 -0
  274. package/tsup.config.ts +36 -0
  275. package/tsup.external.json +103 -0
  276. package/vitest.config.ts +2 -0
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @module @kb-labs/adapters-fs/__tests__
3
+ * Unit tests for FilesystemStorageAdapter
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { mkdtemp, rm } from "node:fs/promises";
10
+ import { createAdapter } from "./index.js";
11
+
12
+ describe("FilesystemStorageAdapter", () => {
13
+ let tmpDir: string;
14
+ let storage: ReturnType<typeof createAdapter>;
15
+
16
+ beforeEach(async () => {
17
+ tmpDir = await mkdtemp(join(tmpdir(), "kb-test-fs-"));
18
+ storage = createAdapter({ baseDir: tmpDir });
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await rm(tmpDir, { recursive: true, force: true });
23
+ });
24
+
25
+ describe("Basic Operations", () => {
26
+ it("should write and read a file", async () => {
27
+ const content = Buffer.from("Hello, World!");
28
+
29
+ await storage.write("test.txt", content);
30
+ const result = await storage.read("test.txt");
31
+
32
+ expect(result).toEqual(content);
33
+ expect(result?.toString()).toBe("Hello, World!");
34
+ });
35
+
36
+ it("should return null when reading nonexistent file", async () => {
37
+ const result = await storage.read("nonexistent.txt");
38
+
39
+ expect(result).toBeNull();
40
+ });
41
+
42
+ it("should write file in nested directory", async () => {
43
+ const content = Buffer.from("Nested content");
44
+
45
+ await storage.write("docs/nested/file.txt", content);
46
+ const result = await storage.read("docs/nested/file.txt");
47
+
48
+ expect(result).toEqual(content);
49
+ });
50
+
51
+ it("should check if file exists", async () => {
52
+ await storage.write("exists.txt", Buffer.from("test"));
53
+
54
+ expect(await storage.exists("exists.txt")).toBe(true);
55
+ expect(await storage.exists("nonexistent.txt")).toBe(false);
56
+ });
57
+
58
+ it("should delete a file", async () => {
59
+ await storage.write("to-delete.txt", Buffer.from("test"));
60
+ expect(await storage.exists("to-delete.txt")).toBe(true);
61
+
62
+ await storage.delete("to-delete.txt");
63
+
64
+ expect(await storage.exists("to-delete.txt")).toBe(false);
65
+ });
66
+
67
+ it("should not throw when deleting nonexistent file", async () => {
68
+ await expect(storage.delete("nonexistent.txt")).resolves.not.toThrow();
69
+ });
70
+ });
71
+
72
+ describe("List Operations", () => {
73
+ it("should list files with prefix", async () => {
74
+ await storage.write("docs/file1.txt", Buffer.from("1"));
75
+ await storage.write("docs/file2.txt", Buffer.from("2"));
76
+ await storage.write("other/file3.txt", Buffer.from("3"));
77
+
78
+ const files = await storage.list("docs/");
79
+
80
+ expect(files).toHaveLength(2);
81
+ expect(files).toContain("docs/file1.txt");
82
+ expect(files).toContain("docs/file2.txt");
83
+ });
84
+
85
+ it("should return empty array when no files match prefix", async () => {
86
+ const files = await storage.list("nonexistent/");
87
+
88
+ expect(files).toEqual([]);
89
+ });
90
+ });
91
+
92
+ describe("Security", () => {
93
+ it("should prevent directory traversal attacks", async () => {
94
+ await expect(
95
+ storage.write("../outside.txt", Buffer.from("bad")),
96
+ ).rejects.toThrow("Path traversal detected");
97
+ });
98
+
99
+ it("should prevent absolute path escaping baseDir", async () => {
100
+ await expect(
101
+ storage.write("/etc/passwd", Buffer.from("bad")),
102
+ ).rejects.toThrow("Path traversal detected");
103
+ });
104
+ });
105
+
106
+ describe("Extended Methods - stat()", () => {
107
+ it("should return file metadata", async () => {
108
+ const content = Buffer.from("Test content");
109
+ await storage.write("test.txt", content);
110
+
111
+ const metadata = await storage.stat("test.txt");
112
+
113
+ expect(metadata).not.toBeNull();
114
+ expect(metadata?.path).toBe("test.txt");
115
+ expect(metadata?.size).toBe(content.length);
116
+ expect(metadata?.contentType).toBe("text/plain");
117
+ expect(metadata?.lastModified).toBeDefined();
118
+ });
119
+
120
+ it("should return null for nonexistent file", async () => {
121
+ const metadata = await storage.stat("nonexistent.txt");
122
+
123
+ expect(metadata).toBeNull();
124
+ });
125
+
126
+ it("should detect content types correctly", async () => {
127
+ await storage.write("test.json", Buffer.from("{}"));
128
+ await storage.write("test.md", Buffer.from("# Title"));
129
+ await storage.write("test.png", Buffer.from("fake-png"));
130
+
131
+ expect((await storage.stat("test.json"))?.contentType).toBe(
132
+ "application/json",
133
+ );
134
+ expect((await storage.stat("test.md"))?.contentType).toBe(
135
+ "text/markdown",
136
+ );
137
+ expect((await storage.stat("test.png"))?.contentType).toBe("image/png");
138
+ });
139
+
140
+ it("should return octet-stream for unknown extensions", async () => {
141
+ await storage.write("test.unknown", Buffer.from("data"));
142
+
143
+ const metadata = await storage.stat("test.unknown");
144
+
145
+ expect(metadata?.contentType).toBe("application/octet-stream");
146
+ });
147
+ });
148
+
149
+ describe("Extended Methods - copy()", () => {
150
+ it("should copy file", async () => {
151
+ const content = Buffer.from("Original content");
152
+ await storage.write("source.txt", content);
153
+
154
+ await storage.copy("source.txt", "destination.txt");
155
+
156
+ const sourceStat = await storage.stat("source.txt");
157
+ const destStat = await storage.stat("destination.txt");
158
+
159
+ expect(sourceStat).not.toBeNull();
160
+ expect(destStat).not.toBeNull();
161
+ expect(destStat?.size).toBe(sourceStat?.size);
162
+
163
+ const destContent = await storage.read("destination.txt");
164
+ expect(destContent).toEqual(content);
165
+ });
166
+
167
+ it("should copy to nested directory", async () => {
168
+ await storage.write("source.txt", Buffer.from("test"));
169
+
170
+ await storage.copy("source.txt", "nested/dir/copy.txt");
171
+
172
+ expect(await storage.exists("nested/dir/copy.txt")).toBe(true);
173
+ });
174
+ });
175
+
176
+ describe("Extended Methods - move()", () => {
177
+ it("should move file", async () => {
178
+ const content = Buffer.from("Content to move");
179
+ await storage.write("source.txt", content);
180
+
181
+ await storage.move("source.txt", "destination.txt");
182
+
183
+ expect(await storage.exists("source.txt")).toBe(false);
184
+ expect(await storage.exists("destination.txt")).toBe(true);
185
+
186
+ const movedContent = await storage.read("destination.txt");
187
+ expect(movedContent).toEqual(content);
188
+ });
189
+
190
+ it("should move to nested directory", async () => {
191
+ await storage.write("source.txt", Buffer.from("test"));
192
+
193
+ await storage.move("source.txt", "nested/dir/moved.txt");
194
+
195
+ expect(await storage.exists("source.txt")).toBe(false);
196
+ expect(await storage.exists("nested/dir/moved.txt")).toBe(true);
197
+ });
198
+
199
+ it("should overwrite existing file when moving", async () => {
200
+ await storage.write("source.txt", Buffer.from("new content"));
201
+ await storage.write("destination.txt", Buffer.from("old content"));
202
+
203
+ await storage.move("source.txt", "destination.txt");
204
+
205
+ const content = await storage.read("destination.txt");
206
+ expect(content?.toString()).toBe("new content");
207
+ });
208
+ });
209
+
210
+ describe("Extended Methods - listWithMetadata()", () => {
211
+ it("should list files with metadata", async () => {
212
+ await storage.write("docs/file1.txt", Buffer.from("content1"));
213
+ await storage.write("docs/file2.md", Buffer.from("content2"));
214
+
215
+ const files = await storage.listWithMetadata("docs/");
216
+
217
+ expect(files).toHaveLength(2);
218
+ expect(files[0]!.path).toBeDefined();
219
+ expect(files[0]!.size).toBeGreaterThan(0);
220
+ expect(files[0]!.lastModified).toBeDefined();
221
+ expect(files[0]!.contentType).toBeDefined();
222
+ });
223
+
224
+ it("should return empty array when no files match", async () => {
225
+ const files = await storage.listWithMetadata("nonexistent/");
226
+
227
+ expect(files).toEqual([]);
228
+ });
229
+
230
+ it("should include correct content types", async () => {
231
+ await storage.write("docs/file.json", Buffer.from("{}"));
232
+ await storage.write("docs/file.txt", Buffer.from("text"));
233
+
234
+ const files = await storage.listWithMetadata("docs/");
235
+
236
+ const jsonFile = files.find((f) => f.path.endsWith(".json"));
237
+ const txtFile = files.find((f) => f.path.endsWith(".txt"));
238
+
239
+ expect(jsonFile?.contentType).toBe("application/json");
240
+ expect(txtFile?.contentType).toBe("text/plain");
241
+ });
242
+ });
243
+ });
@@ -0,0 +1,258 @@
1
+ /**
2
+ * @module @kb-labs/adapters-fs
3
+ * Filesystem adapter implementing IStorage interface.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { createAdapter } from '@kb-labs/adapters-fs';
8
+ *
9
+ * const storage = createAdapter({
10
+ * baseDir: '/var/data',
11
+ * });
12
+ *
13
+ * await storage.write('docs/readme.md', Buffer.from('# Hello'));
14
+ * const content = await storage.read('docs/readme.md');
15
+ * const files = await storage.list('docs/');
16
+ * await storage.delete('docs/readme.md');
17
+ * ```
18
+ */
19
+
20
+ import fs from "fs-extra";
21
+ import path from "node:path";
22
+ import fg from "fast-glob";
23
+ import type {
24
+ IStorage,
25
+ StorageMetadata,
26
+ } from "@kb-labs/core-platform/adapters";
27
+
28
+ // Re-export manifest
29
+ export { manifest } from "./manifest.js";
30
+
31
+ /**
32
+ * Configuration for filesystem storage adapter.
33
+ */
34
+ export interface FilesystemStorageConfig {
35
+ /** Base directory for all file operations (default: process.cwd()) */
36
+ baseDir?: string;
37
+ }
38
+
39
+ /**
40
+ * Filesystem implementation of IStorage interface.
41
+ */
42
+ export class FilesystemStorageAdapter implements IStorage {
43
+ private baseDir: string;
44
+
45
+ constructor(config: FilesystemStorageConfig = {}) {
46
+ this.baseDir = config.baseDir ?? process.cwd();
47
+ }
48
+
49
+ /**
50
+ * Resolve relative path to absolute path within baseDir.
51
+ */
52
+ private resolvePath(relativePath: string): string {
53
+ // Normalize path and ensure it's within baseDir (security)
54
+ const normalized = path.normalize(relativePath);
55
+ const absolute = path.isAbsolute(normalized)
56
+ ? normalized
57
+ : path.join(this.baseDir, normalized);
58
+
59
+ // Ensure path is within baseDir (prevent directory traversal)
60
+ if (!absolute.startsWith(this.baseDir)) {
61
+ throw new Error(`Path traversal detected: ${relativePath}`);
62
+ }
63
+
64
+ return absolute;
65
+ }
66
+
67
+ async read(filepath: string): Promise<Buffer | null> {
68
+ const absolutePath = this.resolvePath(filepath);
69
+
70
+ try {
71
+ return await fs.readFile(absolutePath);
72
+ } catch (error) {
73
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
74
+ return null;
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ async write(filepath: string, data: Buffer): Promise<void> {
81
+ const absolutePath = this.resolvePath(filepath);
82
+
83
+ // Ensure directory exists
84
+ await fs.ensureDir(path.dirname(absolutePath));
85
+
86
+ await fs.writeFile(absolutePath, data);
87
+ }
88
+
89
+ async delete(filepath: string): Promise<void> {
90
+ const absolutePath = this.resolvePath(filepath);
91
+
92
+ try {
93
+ await fs.unlink(absolutePath);
94
+ } catch (error) {
95
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
96
+ // File doesn't exist - that's okay
97
+ return;
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ async list(prefix: string): Promise<string[]> {
104
+ // List files with a given prefix in their name
105
+ const pattern = `${prefix}*`;
106
+ return fg(pattern, {
107
+ onlyFiles: true,
108
+ absolute: false,
109
+ cwd: this.baseDir,
110
+ });
111
+ }
112
+
113
+ async exists(filepath: string): Promise<boolean> {
114
+ const absolutePath = this.resolvePath(filepath);
115
+
116
+ try {
117
+ await fs.access(absolutePath);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ // ============================================================================
125
+ // EXTENDED METHODS (optional - implements IStorage extended interface)
126
+ // ============================================================================
127
+
128
+ /**
129
+ * Get file metadata (size, mtime, etc).
130
+ * Optional method - implements IStorage.stat().
131
+ */
132
+ async stat(filepath: string): Promise<StorageMetadata | null> {
133
+ const absolutePath = this.resolvePath(filepath);
134
+
135
+ try {
136
+ const stats = await fs.stat(absolutePath);
137
+
138
+ return {
139
+ path: filepath,
140
+ size: stats.size,
141
+ lastModified: stats.mtime.toISOString(),
142
+ contentType: this.guessContentType(filepath),
143
+ };
144
+ } catch (error) {
145
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
146
+ return null;
147
+ }
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Copy file within storage.
154
+ * Optional method - implements IStorage.copy().
155
+ */
156
+ async copy(sourcePath: string, destPath: string): Promise<void> {
157
+ const absoluteSource = this.resolvePath(sourcePath);
158
+ const absoluteDest = this.resolvePath(destPath);
159
+
160
+ // Ensure destination directory exists
161
+ await fs.ensureDir(path.dirname(absoluteDest));
162
+
163
+ await fs.copyFile(absoluteSource, absoluteDest);
164
+ }
165
+
166
+ /**
167
+ * Move file within storage.
168
+ * Optional method - implements IStorage.move().
169
+ */
170
+ async move(sourcePath: string, destPath: string): Promise<void> {
171
+ const absoluteSource = this.resolvePath(sourcePath);
172
+ const absoluteDest = this.resolvePath(destPath);
173
+
174
+ // Ensure destination directory exists
175
+ await fs.ensureDir(path.dirname(absoluteDest));
176
+
177
+ await fs.move(absoluteSource, absoluteDest, { overwrite: true });
178
+ }
179
+
180
+ /**
181
+ * List files with metadata.
182
+ * Optional method - implements IStorage.listWithMetadata().
183
+ */
184
+ async listWithMetadata(prefix: string): Promise<StorageMetadata[]> {
185
+ // List files with a given prefix in their name
186
+ const pattern = `${prefix}*`;
187
+ const files = await fg(pattern, {
188
+ onlyFiles: true,
189
+ absolute: true,
190
+ cwd: this.baseDir,
191
+ stats: true,
192
+ });
193
+
194
+ const results: StorageMetadata[] = [];
195
+
196
+ for (const entry of files) {
197
+ if (typeof entry === "string") {
198
+ continue;
199
+ } // Skip if no stats
200
+
201
+ const stats = entry.stats;
202
+ if (!stats) {
203
+ continue;
204
+ }
205
+
206
+ // Convert absolute path back to relative
207
+ const relativePath = path.relative(this.baseDir, entry.path);
208
+
209
+ results.push({
210
+ path: relativePath,
211
+ size: stats.size,
212
+ lastModified: stats.mtime.toISOString(),
213
+ contentType: this.guessContentType(relativePath),
214
+ });
215
+ }
216
+
217
+ return results;
218
+ }
219
+
220
+ /**
221
+ * Guess content type from file extension.
222
+ * Simple implementation - can be extended with mime-types library.
223
+ */
224
+ private guessContentType(filepath: string): string {
225
+ const ext = path.extname(filepath).toLowerCase();
226
+ const mimeTypes: Record<string, string> = {
227
+ ".txt": "text/plain",
228
+ ".md": "text/markdown",
229
+ ".json": "application/json",
230
+ ".js": "application/javascript",
231
+ ".ts": "application/typescript",
232
+ ".html": "text/html",
233
+ ".css": "text/css",
234
+ ".png": "image/png",
235
+ ".jpg": "image/jpeg",
236
+ ".jpeg": "image/jpeg",
237
+ ".gif": "image/gif",
238
+ ".svg": "image/svg+xml",
239
+ ".pdf": "application/pdf",
240
+ ".zip": "application/zip",
241
+ };
242
+
243
+ return mimeTypes[ext] ?? "application/octet-stream";
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Create filesystem storage adapter.
249
+ * This is the factory function called by initPlatform() when loading adapters.
250
+ */
251
+ export function createAdapter(
252
+ config?: FilesystemStorageConfig,
253
+ ): FilesystemStorageAdapter {
254
+ return new FilesystemStorageAdapter(config);
255
+ }
256
+
257
+ // Default export for direct import
258
+ export default createAdapter;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @module @kb-labs/adapters-fs/manifest
3
+ * Adapter manifest for filesystem storage.
4
+ */
5
+
6
+ import type { AdapterManifest } from "@kb-labs/core-platform";
7
+
8
+ /**
9
+ * Adapter manifest for filesystem storage.
10
+ */
11
+ export const manifest: AdapterManifest = {
12
+ manifestVersion: "1.0.0",
13
+ id: "fs-storage",
14
+ name: "Filesystem Storage",
15
+ version: "1.0.0",
16
+ description: "Local filesystem storage adapter with path security",
17
+ author: "KB Labs Team",
18
+ license: "KBPL-1.1",
19
+ type: "core",
20
+ implements: "IStorage",
21
+ capabilities: {
22
+ streaming: true,
23
+ custom: {
24
+ glob: true,
25
+ metadata: true,
26
+ },
27
+ },
28
+ configSchema: {
29
+ baseDir: {
30
+ type: "string",
31
+ default: "process.cwd()",
32
+ description: "Base directory for all file operations",
33
+ },
34
+ },
35
+ };