waypoi 0.0.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 (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveStoragePaths = resolveStoragePaths;
7
+ exports.ensureStorageDir = ensureStorageDir;
8
+ exports.loadConfig = loadConfig;
9
+ exports.saveConfig = saveConfig;
10
+ exports.loadHealth = loadHealth;
11
+ exports.saveHealth = saveHealth;
12
+ exports.loadProviderHealth = loadProviderHealth;
13
+ exports.saveProviderHealth = saveProviderHealth;
14
+ exports.readJsonFile = readJsonFile;
15
+ exports.writeJsonFile = writeJsonFile;
16
+ exports.newEndpointId = newEndpointId;
17
+ exports.defaultHealth = defaultHealth;
18
+ exports.appendRequestLog = appendRequestLog;
19
+ exports.readRequestLogs = readRequestLogs;
20
+ const fs_1 = require("fs");
21
+ const path_1 = __importDefault(require("path"));
22
+ const os_1 = __importDefault(require("os"));
23
+ const crypto_1 = __importDefault(require("crypto"));
24
+ const yaml_1 = __importDefault(require("yaml"));
25
+ function resolveStoragePaths() {
26
+ const baseDir = process.env.WAYPOI_DIR ?? path_1.default.join(os_1.default.homedir(), ".config", "waypoi");
27
+ const configPath = process.env.WAYPOI_CONFIG ?? path_1.default.join(baseDir, "config.yaml");
28
+ return {
29
+ baseDir,
30
+ configPath,
31
+ healthPath: path_1.default.join(baseDir, "health.json"),
32
+ providerHealthPath: path_1.default.join(baseDir, "providers_health.json"),
33
+ requestLogPath: path_1.default.join(baseDir, "request_logs.jsonl"),
34
+ providersPath: path_1.default.join(baseDir, "providers.json"),
35
+ poolsPath: path_1.default.join(baseDir, "pools.json"),
36
+ poolStatePath: path_1.default.join(baseDir, "pool_state.json"),
37
+ };
38
+ }
39
+ async function ensureStorageDir(paths) {
40
+ await fs_1.promises.mkdir(paths.baseDir, { recursive: true });
41
+ }
42
+ async function loadConfig(paths) {
43
+ await ensureStorageDir(paths);
44
+ try {
45
+ const raw = await fs_1.promises.readFile(paths.configPath, "utf8");
46
+ const doc = yaml_1.default.parse(raw);
47
+ if (doc?.endpoints) {
48
+ return doc;
49
+ }
50
+ }
51
+ catch (error) {
52
+ if (error.code !== "ENOENT") {
53
+ throw error;
54
+ }
55
+ }
56
+ return { endpoints: [] };
57
+ }
58
+ async function saveConfig(paths, config) {
59
+ await ensureStorageDir(paths);
60
+ const yaml = yaml_1.default.stringify(config);
61
+ await writeAtomic(paths.configPath, yaml);
62
+ }
63
+ async function loadHealth(paths) {
64
+ await ensureStorageDir(paths);
65
+ try {
66
+ const raw = await fs_1.promises.readFile(paths.healthPath, "utf8");
67
+ const data = JSON.parse(raw);
68
+ if (data?.endpoints) {
69
+ return data;
70
+ }
71
+ }
72
+ catch (error) {
73
+ if (error.code !== "ENOENT") {
74
+ throw error;
75
+ }
76
+ }
77
+ return { endpoints: {} };
78
+ }
79
+ async function saveHealth(paths, health) {
80
+ await ensureStorageDir(paths);
81
+ await writeAtomic(paths.healthPath, JSON.stringify(health, null, 2));
82
+ }
83
+ async function loadProviderHealth(paths) {
84
+ await ensureStorageDir(paths);
85
+ try {
86
+ const raw = await fs_1.promises.readFile(paths.providerHealthPath, "utf8");
87
+ const data = JSON.parse(raw);
88
+ if (data?.models) {
89
+ return data;
90
+ }
91
+ }
92
+ catch (error) {
93
+ if (error.code !== "ENOENT") {
94
+ throw error;
95
+ }
96
+ }
97
+ return { models: {} };
98
+ }
99
+ async function saveProviderHealth(paths, health) {
100
+ await ensureStorageDir(paths);
101
+ await writeAtomic(paths.providerHealthPath, JSON.stringify(health, null, 2));
102
+ }
103
+ async function readJsonFile(filePath, fallback) {
104
+ try {
105
+ const raw = await fs_1.promises.readFile(filePath, "utf8");
106
+ return JSON.parse(raw);
107
+ }
108
+ catch (error) {
109
+ if (error.code === "ENOENT") {
110
+ return fallback;
111
+ }
112
+ throw error;
113
+ }
114
+ }
115
+ async function writeJsonFile(filePath, value) {
116
+ await ensureDir(path_1.default.dirname(filePath));
117
+ await writeAtomic(filePath, JSON.stringify(value, null, 2));
118
+ }
119
+ function newEndpointId() {
120
+ return crypto_1.default.randomUUID();
121
+ }
122
+ function defaultHealth() {
123
+ return {
124
+ status: "up",
125
+ consecutiveFailures: 0
126
+ };
127
+ }
128
+ async function appendRequestLog(paths, log) {
129
+ await ensureStorageDir(paths);
130
+ const line = `${JSON.stringify(log)}\n`;
131
+ await fs_1.promises.appendFile(paths.requestLogPath, line, "utf8");
132
+ }
133
+ async function readRequestLogs(paths) {
134
+ await ensureStorageDir(paths);
135
+ try {
136
+ const raw = await fs_1.promises.readFile(paths.requestLogPath, "utf8");
137
+ return raw
138
+ .split("\n")
139
+ .filter((line) => line.trim().length > 0)
140
+ .map((line) => JSON.parse(line));
141
+ }
142
+ catch (error) {
143
+ if (error.code === "ENOENT") {
144
+ return [];
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ async function writeAtomic(filePath, content) {
150
+ const dir = path_1.default.dirname(filePath);
151
+ const tmp = path_1.default.join(dir, `.${path_1.default.basename(filePath)}.${crypto_1.default.randomUUID()}`);
152
+ await fs_1.promises.writeFile(tmp, content, "utf8");
153
+ await fs_1.promises.rename(tmp, filePath);
154
+ }
155
+ async function ensureDir(dir) {
156
+ await fs_1.promises.mkdir(dir, { recursive: true });
157
+ }
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveMediaDir = resolveMediaDir;
7
+ exports.ensureMediaCacheReady = ensureMediaCacheReady;
8
+ exports.storeMedia = storeMedia;
9
+ exports.getMediaPath = getMediaPath;
10
+ exports.getMediaEntry = getMediaEntry;
11
+ exports.getCacheStats = getCacheStats;
12
+ exports.clearCache = clearCache;
13
+ exports.syncSessionMediaReferences = syncSessionMediaReferences;
14
+ exports.unmarkSessionMediaReferences = unmarkSessionMediaReferences;
15
+ exports.getMediaRefCount = getMediaRefCount;
16
+ exports.cleanOrphanedMedia = cleanOrphanedMedia;
17
+ const fs_1 = require("fs");
18
+ const path_1 = __importDefault(require("path"));
19
+ const crypto_1 = __importDefault(require("crypto"));
20
+ const files_1 = require("./files");
21
+ const DEFAULT_MAX_SIZE_BYTES = 1024 * 1024 * 1024; // 1GB
22
+ function resolveMediaDir(paths) {
23
+ return path_1.default.join(paths.baseDir, "media");
24
+ }
25
+ function cacheIndexPath(paths) {
26
+ return path_1.default.join(resolveMediaDir(paths), "index.json");
27
+ }
28
+ function refsIndexPath(paths) {
29
+ return path_1.default.join(resolveMediaDir(paths), "media_refs.json");
30
+ }
31
+ async function ensureMediaDir(paths) {
32
+ await (0, files_1.ensureStorageDir)(paths);
33
+ await fs_1.promises.mkdir(resolveMediaDir(paths), { recursive: true });
34
+ }
35
+ async function ensureMediaCacheReady(paths) {
36
+ await ensureMediaDir(paths);
37
+ const index = await loadCacheIndex(paths);
38
+ await saveCacheIndex(paths, index);
39
+ const refs = await loadRefsIndex(paths);
40
+ await saveRefsIndex(paths, refs);
41
+ }
42
+ async function loadCacheIndex(paths) {
43
+ const indexPath = cacheIndexPath(paths);
44
+ try {
45
+ const raw = await fs_1.promises.readFile(indexPath, "utf8");
46
+ const data = JSON.parse(raw);
47
+ data.entries = data.entries.map((entry) => ({
48
+ ...entry,
49
+ createdAt: new Date(entry.createdAt),
50
+ }));
51
+ data.evictionBlockedCount = typeof data.evictionBlockedCount === "number" ? data.evictionBlockedCount : 0;
52
+ return data;
53
+ }
54
+ catch (error) {
55
+ if (error.code === "ENOENT") {
56
+ return { entries: [], totalSize: 0, evictionBlockedCount: 0 };
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+ async function saveCacheIndex(paths, index) {
62
+ await fs_1.promises.writeFile(cacheIndexPath(paths), JSON.stringify(index, null, 2), "utf8");
63
+ }
64
+ async function loadRefsIndex(paths) {
65
+ const refsPath = refsIndexPath(paths);
66
+ try {
67
+ const raw = await fs_1.promises.readFile(refsPath, "utf8");
68
+ const parsed = JSON.parse(raw);
69
+ const refs = parsed?.refs && typeof parsed.refs === "object" ? parsed.refs : {};
70
+ const cleaned = {};
71
+ for (const [hash, sessionIds] of Object.entries(refs)) {
72
+ if (!Array.isArray(sessionIds))
73
+ continue;
74
+ const deduped = Array.from(new Set(sessionIds.filter((id) => typeof id === "string" && id.length > 0)));
75
+ if (deduped.length > 0) {
76
+ cleaned[hash] = deduped;
77
+ }
78
+ }
79
+ return { refs: cleaned };
80
+ }
81
+ catch (error) {
82
+ if (error.code === "ENOENT") {
83
+ return { refs: {} };
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+ async function saveRefsIndex(paths, index) {
89
+ await fs_1.promises.writeFile(refsIndexPath(paths), JSON.stringify(index, null, 2), "utf8");
90
+ }
91
+ async function storeMedia(paths, data, options) {
92
+ await ensureMediaDir(paths);
93
+ const normalized = normalizeMediaInput(data, options?.mimeType);
94
+ const buffer = normalized.buffer;
95
+ const mimeType = normalized.mimeType;
96
+ const extension = extensionFromMime(mimeType, buffer);
97
+ const hash = crypto_1.default.createHash("sha256").update(buffer).digest("hex").slice(0, 16);
98
+ const filename = `${hash}.${extension}`;
99
+ const filePath = path_1.default.join(resolveMediaDir(paths), filename);
100
+ const index = await loadCacheIndex(paths);
101
+ const refs = await loadRefsIndex(paths);
102
+ const existing = index.entries.find((entry) => entry.hash === hash);
103
+ if (existing) {
104
+ index.entries = index.entries.filter((entry) => entry.hash !== hash);
105
+ existing.createdAt = new Date();
106
+ index.entries.push(existing);
107
+ await saveCacheIndex(paths, index);
108
+ return { filePath, hash, mimeType: existing.mimeType, evicted: [] };
109
+ }
110
+ await fs_1.promises.writeFile(filePath, buffer);
111
+ const entry = {
112
+ hash,
113
+ filename,
114
+ size: buffer.length,
115
+ mimeType,
116
+ createdAt: new Date(),
117
+ model: options?.model,
118
+ };
119
+ index.entries.push(entry);
120
+ index.totalSize += buffer.length;
121
+ const maxSize = options?.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
122
+ const evicted = [];
123
+ while (index.totalSize > maxSize && index.entries.length > 1) {
124
+ const evictionTargetIndex = index.entries.findIndex((entry) => !isHashReferenced(refs, entry.hash));
125
+ if (evictionTargetIndex < 0) {
126
+ index.evictionBlockedCount = (index.evictionBlockedCount ?? 0) + 1;
127
+ break;
128
+ }
129
+ const [oldest] = index.entries.splice(evictionTargetIndex, 1);
130
+ if (!oldest) {
131
+ break;
132
+ }
133
+ try {
134
+ await fs_1.promises.unlink(path_1.default.join(resolveMediaDir(paths), oldest.filename));
135
+ evicted.push(oldest.hash);
136
+ }
137
+ catch {
138
+ // ignore missing file
139
+ }
140
+ index.totalSize -= oldest.size;
141
+ }
142
+ await saveCacheIndex(paths, index);
143
+ return { filePath, hash, mimeType, evicted };
144
+ }
145
+ async function getMediaPath(paths, hash) {
146
+ const index = await loadCacheIndex(paths);
147
+ const entry = index.entries.find((item) => item.hash === hash);
148
+ if (!entry) {
149
+ return null;
150
+ }
151
+ return path_1.default.join(resolveMediaDir(paths), entry.filename);
152
+ }
153
+ async function getMediaEntry(paths, hash) {
154
+ const index = await loadCacheIndex(paths);
155
+ return index.entries.find((item) => item.hash === hash) ?? null;
156
+ }
157
+ async function getCacheStats(paths) {
158
+ const index = await loadCacheIndex(paths);
159
+ const refs = await loadRefsIndex(paths);
160
+ if (index.entries.length === 0) {
161
+ return {
162
+ count: 0,
163
+ totalSizeBytes: 0,
164
+ referencedCount: 0,
165
+ unreferencedCount: 0,
166
+ evictionBlockedCount: index.evictionBlockedCount ?? 0,
167
+ };
168
+ }
169
+ const sorted = [...index.entries].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
170
+ const referencedCount = index.entries.filter((entry) => isHashReferenced(refs, entry.hash)).length;
171
+ return {
172
+ count: index.entries.length,
173
+ totalSizeBytes: index.totalSize,
174
+ referencedCount,
175
+ unreferencedCount: index.entries.length - referencedCount,
176
+ evictionBlockedCount: index.evictionBlockedCount ?? 0,
177
+ oldestEntry: sorted[0].createdAt,
178
+ newestEntry: sorted[sorted.length - 1].createdAt,
179
+ };
180
+ }
181
+ async function clearCache(paths) {
182
+ const index = await loadCacheIndex(paths);
183
+ let deleted = 0;
184
+ for (const entry of index.entries) {
185
+ try {
186
+ await fs_1.promises.unlink(path_1.default.join(resolveMediaDir(paths), entry.filename));
187
+ deleted += 1;
188
+ }
189
+ catch {
190
+ // ignore missing file
191
+ }
192
+ }
193
+ await saveCacheIndex(paths, { entries: [], totalSize: 0, evictionBlockedCount: 0 });
194
+ await saveRefsIndex(paths, { refs: {} });
195
+ return deleted;
196
+ }
197
+ async function syncSessionMediaReferences(paths, sessionId, hashes) {
198
+ await ensureMediaDir(paths);
199
+ const refs = await loadRefsIndex(paths);
200
+ const nextHashes = Array.from(new Set(hashes.filter((hash) => /^[a-f0-9]{16}$/i.test(hash))));
201
+ // Remove previous references for this session from all hashes.
202
+ for (const [hash, sessionIds] of Object.entries(refs.refs)) {
203
+ const filtered = sessionIds.filter((id) => id !== sessionId);
204
+ if (filtered.length > 0) {
205
+ refs.refs[hash] = filtered;
206
+ }
207
+ else {
208
+ delete refs.refs[hash];
209
+ }
210
+ }
211
+ for (const hash of nextHashes) {
212
+ const existing = refs.refs[hash] ?? [];
213
+ refs.refs[hash] = Array.from(new Set([...existing, sessionId]));
214
+ }
215
+ await saveRefsIndex(paths, refs);
216
+ }
217
+ async function unmarkSessionMediaReferences(paths, sessionId) {
218
+ await ensureMediaDir(paths);
219
+ const refs = await loadRefsIndex(paths);
220
+ for (const [hash, sessionIds] of Object.entries(refs.refs)) {
221
+ const filtered = sessionIds.filter((id) => id !== sessionId);
222
+ if (filtered.length > 0) {
223
+ refs.refs[hash] = filtered;
224
+ }
225
+ else {
226
+ delete refs.refs[hash];
227
+ }
228
+ }
229
+ await saveRefsIndex(paths, refs);
230
+ }
231
+ async function getMediaRefCount(paths, hash) {
232
+ const refs = await loadRefsIndex(paths);
233
+ return refs.refs[hash]?.length ?? 0;
234
+ }
235
+ /**
236
+ * Remove all media entries that are no longer referenced by any session.
237
+ * Call this after deleting a session to free up disk space.
238
+ * Returns the number of files deleted.
239
+ */
240
+ async function cleanOrphanedMedia(paths) {
241
+ const index = await loadCacheIndex(paths);
242
+ const refs = await loadRefsIndex(paths);
243
+ let deleted = 0;
244
+ const surviving = [];
245
+ let survivingSize = 0;
246
+ for (const entry of index.entries) {
247
+ if (isHashReferenced(refs, entry.hash)) {
248
+ surviving.push(entry);
249
+ survivingSize += entry.size;
250
+ }
251
+ else {
252
+ try {
253
+ await fs_1.promises.unlink(path_1.default.join(resolveMediaDir(paths), entry.filename));
254
+ deleted += 1;
255
+ }
256
+ catch {
257
+ // File already gone — still remove from index
258
+ deleted += 1;
259
+ }
260
+ }
261
+ }
262
+ if (deleted > 0) {
263
+ await saveCacheIndex(paths, { ...index, entries: surviving, totalSize: survivingSize });
264
+ }
265
+ return deleted;
266
+ }
267
+ function isHashReferenced(refs, hash) {
268
+ const sessionIds = refs.refs[hash];
269
+ return Array.isArray(sessionIds) && sessionIds.length > 0;
270
+ }
271
+ function normalizeMediaInput(data, hintedMime) {
272
+ if (Buffer.isBuffer(data)) {
273
+ const mimeType = hintedMime ?? detectMimeType(data);
274
+ return { buffer: data, mimeType };
275
+ }
276
+ const trimmed = data.trim();
277
+ const dataUrlMatch = trimmed.match(/^data:([^;]+);base64,(.+)$/i);
278
+ if (dataUrlMatch) {
279
+ const mimeType = dataUrlMatch[1].toLowerCase();
280
+ const base64Payload = dataUrlMatch[2].replace(/\s+/g, "");
281
+ return { buffer: Buffer.from(base64Payload, "base64"), mimeType };
282
+ }
283
+ const buffer = Buffer.from(trimmed.replace(/\s+/g, ""), "base64");
284
+ const mimeType = hintedMime ?? detectMimeType(buffer);
285
+ return { buffer, mimeType };
286
+ }
287
+ function extensionFromMime(mimeType, buffer) {
288
+ const map = {
289
+ "image/png": "png",
290
+ "image/jpeg": "jpg",
291
+ "image/jpg": "jpg",
292
+ "image/gif": "gif",
293
+ "image/webp": "webp",
294
+ "audio/wav": "wav",
295
+ "audio/x-wav": "wav",
296
+ "audio/mpeg": "mp3",
297
+ "audio/mp3": "mp3",
298
+ "audio/ogg": "ogg",
299
+ "audio/webm": "webm",
300
+ "audio/mp4": "m4a",
301
+ };
302
+ if (map[mimeType]) {
303
+ return map[mimeType];
304
+ }
305
+ // fallback by magic bytes
306
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
307
+ return "png";
308
+ if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
309
+ return "jpg";
310
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
311
+ return "gif";
312
+ if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46) {
313
+ if (buffer.length > 11) {
314
+ const chunk = buffer.subarray(8, 12).toString("ascii");
315
+ if (chunk === "WAVE")
316
+ return "wav";
317
+ if (chunk === "WEBP")
318
+ return "webp";
319
+ }
320
+ }
321
+ return "bin";
322
+ }
323
+ function detectMimeType(buffer) {
324
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
325
+ return "image/png";
326
+ }
327
+ if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
328
+ return "image/jpeg";
329
+ }
330
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
331
+ return "image/gif";
332
+ }
333
+ if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46) {
334
+ if (buffer.length > 11) {
335
+ const chunk = buffer.subarray(8, 12).toString("ascii");
336
+ if (chunk === "WAVE")
337
+ return "audio/wav";
338
+ if (chunk === "WEBP")
339
+ return "image/webp";
340
+ }
341
+ }
342
+ if (buffer.length > 3 && buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) {
343
+ return "audio/mpeg";
344
+ }
345
+ return "application/octet-stream";
346
+ }