@sriinnu/kosha-discovery 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 (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +286 -0
  3. package/dist/aliases.d.ts +51 -0
  4. package/dist/aliases.d.ts.map +1 -0
  5. package/dist/aliases.js +115 -0
  6. package/dist/aliases.js.map +1 -0
  7. package/dist/cache.d.ts +57 -0
  8. package/dist/cache.d.ts.map +1 -0
  9. package/dist/cache.js +123 -0
  10. package/dist/cache.js.map +1 -0
  11. package/dist/cli-commands.d.ts +142 -0
  12. package/dist/cli-commands.d.ts.map +1 -0
  13. package/dist/cli-commands.js +746 -0
  14. package/dist/cli-commands.js.map +1 -0
  15. package/dist/cli-format.d.ts +119 -0
  16. package/dist/cli-format.d.ts.map +1 -0
  17. package/dist/cli-format.js +171 -0
  18. package/dist/cli-format.js.map +1 -0
  19. package/dist/cli.d.ts +15 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +143 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/credentials/index.d.ts +2 -0
  24. package/dist/credentials/index.d.ts.map +1 -0
  25. package/dist/credentials/index.js +2 -0
  26. package/dist/credentials/index.js.map +1 -0
  27. package/dist/credentials/resolver.d.ts +170 -0
  28. package/dist/credentials/resolver.d.ts.map +1 -0
  29. package/dist/credentials/resolver.js +597 -0
  30. package/dist/credentials/resolver.js.map +1 -0
  31. package/dist/discovery/anthropic.d.ts +49 -0
  32. package/dist/discovery/anthropic.d.ts.map +1 -0
  33. package/dist/discovery/anthropic.js +101 -0
  34. package/dist/discovery/anthropic.js.map +1 -0
  35. package/dist/discovery/base.d.ts +59 -0
  36. package/dist/discovery/base.d.ts.map +1 -0
  37. package/dist/discovery/base.js +123 -0
  38. package/dist/discovery/base.js.map +1 -0
  39. package/dist/discovery/bedrock.d.ts +122 -0
  40. package/dist/discovery/bedrock.d.ts.map +1 -0
  41. package/dist/discovery/bedrock.js +356 -0
  42. package/dist/discovery/bedrock.js.map +1 -0
  43. package/dist/discovery/cerebras.d.ts +43 -0
  44. package/dist/discovery/cerebras.d.ts.map +1 -0
  45. package/dist/discovery/cerebras.js +65 -0
  46. package/dist/discovery/cerebras.js.map +1 -0
  47. package/dist/discovery/cohere.d.ts +57 -0
  48. package/dist/discovery/cohere.d.ts.map +1 -0
  49. package/dist/discovery/cohere.js +82 -0
  50. package/dist/discovery/cohere.js.map +1 -0
  51. package/dist/discovery/deepinfra.d.ts +41 -0
  52. package/dist/discovery/deepinfra.d.ts.map +1 -0
  53. package/dist/discovery/deepinfra.js +86 -0
  54. package/dist/discovery/deepinfra.js.map +1 -0
  55. package/dist/discovery/deepseek.d.ts +17 -0
  56. package/dist/discovery/deepseek.d.ts.map +1 -0
  57. package/dist/discovery/deepseek.js +45 -0
  58. package/dist/discovery/deepseek.js.map +1 -0
  59. package/dist/discovery/fireworks.d.ts +48 -0
  60. package/dist/discovery/fireworks.d.ts.map +1 -0
  61. package/dist/discovery/fireworks.js +101 -0
  62. package/dist/discovery/fireworks.js.map +1 -0
  63. package/dist/discovery/glm.d.ts +17 -0
  64. package/dist/discovery/glm.d.ts.map +1 -0
  65. package/dist/discovery/glm.js +52 -0
  66. package/dist/discovery/glm.js.map +1 -0
  67. package/dist/discovery/google.d.ts +53 -0
  68. package/dist/discovery/google.d.ts.map +1 -0
  69. package/dist/discovery/google.js +122 -0
  70. package/dist/discovery/google.js.map +1 -0
  71. package/dist/discovery/groq.d.ts +44 -0
  72. package/dist/discovery/groq.d.ts.map +1 -0
  73. package/dist/discovery/groq.js +89 -0
  74. package/dist/discovery/groq.js.map +1 -0
  75. package/dist/discovery/index.d.ts +48 -0
  76. package/dist/discovery/index.d.ts.map +1 -0
  77. package/dist/discovery/index.js +116 -0
  78. package/dist/discovery/index.js.map +1 -0
  79. package/dist/discovery/llama-cpp.d.ts +45 -0
  80. package/dist/discovery/llama-cpp.d.ts.map +1 -0
  81. package/dist/discovery/llama-cpp.js +137 -0
  82. package/dist/discovery/llama-cpp.js.map +1 -0
  83. package/dist/discovery/minimax.d.ts +16 -0
  84. package/dist/discovery/minimax.d.ts.map +1 -0
  85. package/dist/discovery/minimax.js +56 -0
  86. package/dist/discovery/minimax.js.map +1 -0
  87. package/dist/discovery/mistral.d.ts +45 -0
  88. package/dist/discovery/mistral.d.ts.map +1 -0
  89. package/dist/discovery/mistral.js +82 -0
  90. package/dist/discovery/mistral.js.map +1 -0
  91. package/dist/discovery/moonshot.d.ts +16 -0
  92. package/dist/discovery/moonshot.d.ts.map +1 -0
  93. package/dist/discovery/moonshot.js +46 -0
  94. package/dist/discovery/moonshot.js.map +1 -0
  95. package/dist/discovery/nvidia.d.ts +60 -0
  96. package/dist/discovery/nvidia.d.ts.map +1 -0
  97. package/dist/discovery/nvidia.js +145 -0
  98. package/dist/discovery/nvidia.js.map +1 -0
  99. package/dist/discovery/ollama.d.ts +57 -0
  100. package/dist/discovery/ollama.d.ts.map +1 -0
  101. package/dist/discovery/ollama.js +151 -0
  102. package/dist/discovery/ollama.js.map +1 -0
  103. package/dist/discovery/openai-compatible.d.ts +126 -0
  104. package/dist/discovery/openai-compatible.d.ts.map +1 -0
  105. package/dist/discovery/openai-compatible.js +161 -0
  106. package/dist/discovery/openai-compatible.js.map +1 -0
  107. package/dist/discovery/openai.d.ts +62 -0
  108. package/dist/discovery/openai.d.ts.map +1 -0
  109. package/dist/discovery/openai.js +157 -0
  110. package/dist/discovery/openai.js.map +1 -0
  111. package/dist/discovery/openrouter.d.ts +78 -0
  112. package/dist/discovery/openrouter.d.ts.map +1 -0
  113. package/dist/discovery/openrouter.js +184 -0
  114. package/dist/discovery/openrouter.js.map +1 -0
  115. package/dist/discovery/perplexity.d.ts +66 -0
  116. package/dist/discovery/perplexity.d.ts.map +1 -0
  117. package/dist/discovery/perplexity.js +86 -0
  118. package/dist/discovery/perplexity.js.map +1 -0
  119. package/dist/discovery/together.d.ts +41 -0
  120. package/dist/discovery/together.d.ts.map +1 -0
  121. package/dist/discovery/together.js +88 -0
  122. package/dist/discovery/together.js.map +1 -0
  123. package/dist/discovery/vertex.d.ts +146 -0
  124. package/dist/discovery/vertex.d.ts.map +1 -0
  125. package/dist/discovery/vertex.js +371 -0
  126. package/dist/discovery/vertex.js.map +1 -0
  127. package/dist/discovery/zai.d.ts +17 -0
  128. package/dist/discovery/zai.d.ts.map +1 -0
  129. package/dist/discovery/zai.js +51 -0
  130. package/dist/discovery/zai.js.map +1 -0
  131. package/dist/discovery-contract.d.ts +186 -0
  132. package/dist/discovery-contract.d.ts.map +1 -0
  133. package/dist/discovery-contract.js +115 -0
  134. package/dist/discovery-contract.js.map +1 -0
  135. package/dist/discovery-routes.d.ts +14 -0
  136. package/dist/discovery-routes.d.ts.map +1 -0
  137. package/dist/discovery-routes.js +98 -0
  138. package/dist/discovery-routes.js.map +1 -0
  139. package/dist/enrichment/index.d.ts +2 -0
  140. package/dist/enrichment/index.d.ts.map +1 -0
  141. package/dist/enrichment/index.js +2 -0
  142. package/dist/enrichment/index.js.map +1 -0
  143. package/dist/enrichment/litellm.d.ts +52 -0
  144. package/dist/enrichment/litellm.d.ts.map +1 -0
  145. package/dist/enrichment/litellm.js +205 -0
  146. package/dist/enrichment/litellm.js.map +1 -0
  147. package/dist/index.d.ts +30 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +33 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/normalize.d.ts +64 -0
  152. package/dist/normalize.d.ts.map +1 -0
  153. package/dist/normalize.js +166 -0
  154. package/dist/normalize.js.map +1 -0
  155. package/dist/provider-catalog.d.ts +62 -0
  156. package/dist/provider-catalog.d.ts.map +1 -0
  157. package/dist/provider-catalog.js +331 -0
  158. package/dist/provider-catalog.js.map +1 -0
  159. package/dist/registry-delta.d.ts +17 -0
  160. package/dist/registry-delta.d.ts.map +1 -0
  161. package/dist/registry-delta.js +74 -0
  162. package/dist/registry-delta.js.map +1 -0
  163. package/dist/registry-discovery.d.ts +66 -0
  164. package/dist/registry-discovery.d.ts.map +1 -0
  165. package/dist/registry-discovery.js +339 -0
  166. package/dist/registry-discovery.js.map +1 -0
  167. package/dist/registry-query.d.ts +74 -0
  168. package/dist/registry-query.d.ts.map +1 -0
  169. package/dist/registry-query.js +310 -0
  170. package/dist/registry-query.js.map +1 -0
  171. package/dist/registry-runtime.d.ts +48 -0
  172. package/dist/registry-runtime.d.ts.map +1 -0
  173. package/dist/registry-runtime.js +255 -0
  174. package/dist/registry-runtime.js.map +1 -0
  175. package/dist/registry-selection.d.ts +31 -0
  176. package/dist/registry-selection.d.ts.map +1 -0
  177. package/dist/registry-selection.js +167 -0
  178. package/dist/registry-selection.js.map +1 -0
  179. package/dist/registry-state.d.ts +92 -0
  180. package/dist/registry-state.d.ts.map +1 -0
  181. package/dist/registry-state.js +35 -0
  182. package/dist/registry-state.js.map +1 -0
  183. package/dist/registry.d.ts +127 -0
  184. package/dist/registry.d.ts.map +1 -0
  185. package/dist/registry.js +248 -0
  186. package/dist/registry.js.map +1 -0
  187. package/dist/resilience.d.ts +212 -0
  188. package/dist/resilience.d.ts.map +1 -0
  189. package/dist/resilience.js +292 -0
  190. package/dist/resilience.js.map +1 -0
  191. package/dist/security.d.ts +44 -0
  192. package/dist/security.d.ts.map +1 -0
  193. package/dist/security.js +184 -0
  194. package/dist/security.js.map +1 -0
  195. package/dist/server.d.ts +41 -0
  196. package/dist/server.d.ts.map +1 -0
  197. package/dist/server.js +320 -0
  198. package/dist/server.js.map +1 -0
  199. package/dist/shell-safe.d.ts +15 -0
  200. package/dist/shell-safe.d.ts.map +1 -0
  201. package/dist/shell-safe.js +20 -0
  202. package/dist/shell-safe.js.map +1 -0
  203. package/dist/types.d.ts +365 -0
  204. package/dist/types.d.ts.map +1 -0
  205. package/dist/types.js +10 -0
  206. package/dist/types.js.map +1 -0
  207. package/logo.svg +81 -0
  208. package/package.json +85 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Srinivas Pendela
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,286 @@
1
+ <p align="center">
2
+ <img src="logo.svg" alt="Kosha — AI Model Discovery" width="140" />
3
+ </p>
4
+
5
+ <h1 align="center">kosha-discovery — कोश</h1>
6
+
7
+ <p align="center"><strong>AI Model & Provider Discovery Registry</strong></p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@sriinnu/kosha-discovery"><img src="https://img.shields.io/npm/v/%40sriinnu%2Fkosha-discovery?color=7C3AED&label=npm" alt="npm version" /></a>
11
+ <a href="https://www.npmjs.com/package/@sriinnu/kosha-discovery"><img src="https://img.shields.io/npm/dm/%40sriinnu%2Fkosha-discovery?color=0EA5E9&label=downloads" alt="npm downloads" /></a>
12
+ <a href="https://github.com/sriinnu/kosha-discovery/blob/main/LICENSE"><img src="https://img.shields.io/github/license/sriinnu/kosha-discovery?color=F59E0B" alt="license" /></a>
13
+ <a href="https://www.npmjs.com/package/@sriinnu/kosha-discovery"><img src="https://img.shields.io/node/v/%40sriinnu%2Fkosha-discovery?color=5B21B6" alt="node version" /></a>
14
+ <a href="https://github.com/sriinnu/kosha-discovery/actions/workflows/release-npm.yml"><img src="https://img.shields.io/github/actions/workflow/status/sriinnu/kosha-discovery/release-npm.yml?label=release%20workflow" alt="release workflow status" /></a>
15
+ <a href="https://github.com/sriinnu/kosha-discovery/actions/workflows/update-kosha-snapshot.yml"><img src="https://img.shields.io/github/actions/workflow/status/sriinnu/kosha-discovery/update-kosha-snapshot.yml?label=snapshot%20workflow" alt="snapshot workflow status" /></a>
16
+ <a href="https://github.com/sriinnu/kosha-discovery/actions/workflows/provider-smoke.yml"><img src="https://img.shields.io/github/actions/workflow/status/sriinnu/kosha-discovery/provider-smoke.yml?label=provider%20smoke" alt="provider smoke status" /></a>
17
+ <a href="https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json"><img src="https://img.shields.io/badge/pricing%20source-LiteLLM-2563EB" alt="LiteLLM pricing source" /></a>
18
+ </p>
19
+
20
+ Kosha (कोश — treasury/repository) automatically discovers AI models across providers, resolves credentials from CLI tools and environment variables, enriches models with pricing data, and exposes the catalog via library, CLI, and HTTP API.
21
+
22
+ ## Why
23
+
24
+ AI applications hardcode model IDs, pricing, and provider configs. When providers add models or change pricing, every app breaks. Kosha solves this:
25
+
26
+ - **Dynamic discovery** — fetches real model lists from provider APIs
27
+ - **Smart credentials** — finds API keys from env vars, CLI tools (Claude, Copilot, Gemini CLI), and config files
28
+ - **Pricing enrichment** — fills in costs and context windows from litellm's community-maintained dataset
29
+ - **Model aliases** — `sonnet` → `claude-sonnet-4-20250514`, updated as models evolve
30
+ - **Role matrix** — query provider -> model -> roles (`chat`, `embedding`, `image_generation`, etc.)
31
+ - **Cheapest routing** — rank cheapest eligible models for tasks like embeddings or image generation
32
+ - **Local LLM scanning** — detects Ollama models alongside cloud providers
33
+ - **Three access patterns** — use as a library, CLI tool, or HTTP API
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pnpm add kosha-discovery
39
+ ```
40
+
41
+ ## Development (pnpm)
42
+
43
+ ```bash
44
+ pnpm install
45
+ pnpm run build
46
+ pnpm run test
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ### Library
52
+
53
+ ```typescript
54
+ import { createKosha } from "kosha-discovery";
55
+
56
+ const kosha = await createKosha();
57
+
58
+ const models = kosha.models(); // all models
59
+ const embeddings = kosha.models({ mode: "embedding" }); // filter by mode
60
+ const model = kosha.model("sonnet"); // resolve alias
61
+ const cheapest = kosha.cheapestModels({ role: "image", limit: 3 });
62
+
63
+ console.log(model.pricing); // { inputPerMillion: 3, outputPerMillion: 15, ... }
64
+ ```
65
+
66
+ ### CLI
67
+
68
+ ```bash
69
+ kosha discover # discover all providers
70
+ kosha list # list models
71
+ kosha list --provider anthropic # filter by provider
72
+ kosha search gemini # fuzzy search
73
+ kosha model sonnet # model details
74
+ kosha cheapest --role embeddings # cheapest for a task
75
+ kosha routes gpt-4o # all provider routes
76
+ kosha providers # provider status
77
+ kosha latest # force-fetch latest provider/model details
78
+ kosha latest --provider openai # latest for one provider
79
+ kosha serve --port 3000 # start HTTP API
80
+ ```
81
+
82
+ ### Auto-Fetch JSON Snapshot
83
+
84
+ ```bash
85
+ # one-shot latest snapshot
86
+ pnpm run autofetch:once
87
+
88
+ # custom output/provider
89
+ pnpm run autofetch:once -- --provider openai --output ./data/openai-latest.json
90
+
91
+ # continuous loop (default 3600s)
92
+ pnpm run autofetch -- --interval-seconds 900
93
+ ```
94
+
95
+ By default this writes JSON to `./data/kosha-latest.json`.
96
+
97
+ ### CI / Smoke Checks
98
+
99
+ Workflow files:
100
+
101
+ - `.github/workflows/update-kosha-snapshot.yml`
102
+ - `.github/workflows/provider-smoke.yml`
103
+
104
+ Snapshot workflow:
105
+
106
+ - Scheduled: weekly (Monday 06:00 UTC)
107
+ - Manual: GitHub UI -> Actions -> `Update Kosha Snapshot` -> `Run workflow`
108
+ - Optional manual inputs:
109
+ - `provider` (empty = all providers)
110
+ - `output` (default `data/kosha-latest.json`)
111
+ - Disable scheduled runs without removing the workflow by setting `KOSHA_SNAPSHOT_SCHEDULE_ENABLED=false`
112
+ - Manual `Run workflow` still works even when schedule is disabled
113
+
114
+ Provider smoke workflow:
115
+
116
+ - Scheduled: nightly (03:00 UTC)
117
+ - Manual: GitHub UI -> Actions -> `Provider Smoke Checks` -> `Run workflow`
118
+ - Runs only when repository variable `KOSHA_PROVIDER_SMOKE_ENABLED=true`
119
+ - Manual dispatch can override that gate with `force=true`
120
+ - Installs with pnpm, builds, then runs a node inline smoke script against real provider endpoints
121
+ - Providers without the required secrets are skipped instead of failing the job
122
+ - Always uploads `artifacts/provider-smoke-report.json`
123
+
124
+ Provider smoke secrets:
125
+
126
+ - OpenAI: `OPENAI_API_KEY`
127
+ - Google/Gemini: `GOOGLE_API_KEY` or `GEMINI_API_KEY`
128
+ - Mistral: `MISTRAL_API_KEY`
129
+ - DeepSeek: `DEEPSEEK_API_KEY`
130
+ - Moonshot: `MOONSHOT_API_KEY` or `KIMI_API_KEY`
131
+ - GLM: `GLM_API_KEY` or `ZHIPUAI_API_KEY`
132
+ - Z.AI: `ZAI_API_KEY`
133
+ - MiniMax: `MINIMAX_API_KEY`
134
+ - OpenRouter: optional `OPENROUTER_API_KEY`
135
+ - Bedrock: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`
136
+ - Vertex AI: `GOOGLE_APPLICATION_CREDENTIALS_JSON`, `GOOGLE_CLOUD_PROJECT`
137
+
138
+ Security controls in these workflows:
139
+
140
+ - Snapshot workflow commits only the configured snapshot file path plus its checksum file, never broad `git add -A`
141
+ - Snapshot workflow validates the generated snapshot against a local JSON schema before commit
142
+ - Snapshot workflow runs a high-signal secret-pattern scan on snapshot output before commit
143
+ - Snapshot workflow writes `data/kosha-latest.sha256` alongside the snapshot
144
+ - Snapshot workflow uploads an always-on artifact with run metadata, provider summaries, and failure details
145
+ - Provider smoke workflow never echoes secret values and records a machine-readable JSON report
146
+
147
+ ### Branch Protection Audit
148
+
149
+ Workflow file: `.github/workflows/branch-protection-check.yml`
150
+
151
+ - Runs on `main` and via `workflow_dispatch`
152
+ - Reads the `main` branch protection rule through the GitHub API
153
+ - Expects the required status checks list to include `Branch Protection Check / audit`
154
+ - Uploads a warning artifact instead of failing when the token cannot read branch protection settings
155
+
156
+ Keep that check name stable when you rename the workflow or job, and update the protection rule whenever you add more required checks.
157
+
158
+ ### Where Data Is Stored
159
+
160
+ - Git repo: model/provider discovery data is **not** committed by default.
161
+ - Runtime cache: `~/.kosha/cache/*.json` (machine-local, TTL-based).
162
+ - Exported snapshot: only if you run `autofetch`/`autofetch:once` with an output file and commit it yourself.
163
+
164
+ ### HTTP API
165
+
166
+ ```bash
167
+ kosha serve --port 3000
168
+ ```
169
+
170
+ ```
171
+ GET /api/models — All models (filterable)
172
+ GET /api/models/cheapest — Cheapest ranked models
173
+ GET /api/models/:idOrAlias — Single model
174
+ GET /api/models/:idOrAlias/routes — All provider routes
175
+ GET /api/roles — Provider → model → roles matrix
176
+ GET /api/providers — All providers
177
+ POST /api/refresh — Re-discover
178
+ GET /health — Health check
179
+ ```
180
+
181
+ ## Supported Providers
182
+
183
+ | Provider | Discovery | Credential Sources |
184
+ |----------|-----------|-------------------|
185
+ | Anthropic | API (`/v1/models`) | `ANTHROPIC_API_KEY`, Claude CLI, Codex CLI |
186
+ | OpenAI | API (`/v1/models`) | `OPENAI_API_KEY`, GitHub Copilot tokens |
187
+ | Google | API (`/v1beta/models`) | `GOOGLE_API_KEY`, `GEMINI_API_KEY`, Gemini CLI, gcloud |
188
+ | AWS Bedrock | SDK → CLI → static | `AWS_ACCESS_KEY_ID`, `~/.aws/credentials`, SSO, IAM |
189
+ | Vertex AI | API + gcloud | `GOOGLE_APPLICATION_CREDENTIALS`, gcloud ADC |
190
+ | Ollama | Local API | None needed (local) |
191
+ | OpenRouter | API | `OPENROUTER_API_KEY` (optional) |
192
+ | NVIDIA | API | `NVIDIA_API_KEY` |
193
+ | Together AI | API | `TOGETHER_API_KEY` |
194
+ | Fireworks AI | API | `FIREWORKS_API_KEY` |
195
+ | Groq | API | `GROQ_API_KEY` |
196
+ | Mistral AI | API | `MISTRAL_API_KEY` |
197
+ | DeepInfra | API | `DEEPINFRA_API_KEY` |
198
+ | Cohere | API | `CO_API_KEY` |
199
+ | Cerebras | API | `CEREBRAS_API_KEY` |
200
+ | Perplexity | API | `PERPLEXITY_API_KEY` |
201
+ | DeepSeek | API | `DEEPSEEK_API_KEY` |
202
+ | Moonshot (Kimi) | API | `MOONSHOT_API_KEY` / `KIMI_API_KEY` |
203
+ | GLM (Zhipu) | API | `GLM_API_KEY` / `ZHIPUAI_API_KEY` |
204
+ | Z.AI | API | `ZAI_API_KEY` |
205
+ | MiniMax | API | `MINIMAX_API_KEY` |
206
+
207
+ ## Security
208
+
209
+ All external data (API responses, CLI output, cache reads) is scanned for 9 threat types before use: credential leaks, base64 payloads, script/shell injection, data URIs, null bytes, prototype pollution, hex blobs, and oversized strings. A pre-commit hook blocks secrets at commit time.
210
+
211
+ See [docs/security.md](docs/security.md) for the full threat catalogue and architecture.
212
+
213
+ ## Architecture
214
+
215
+ <p align="center">
216
+ <img src="architecture.svg" alt="Kosha Architecture" width="720" />
217
+ </p>
218
+
219
+ ```
220
+ ┌─────────────────────────────────────────────────────┐
221
+ │ Your Application │
222
+ │ import { createKosha } from "kosha" │
223
+ └───────────────────────┬─────────────────────────────┘
224
+
225
+ ┌───────────────────────▼─────────────────────────────┐
226
+ │ ModelRegistry │
227
+ │ models() · providerRoles() · cheapestModels() │
228
+ └──┬──────────┬──────────────┬───────────────┬────────┘
229
+ │ │ │ │
230
+ ┌──▼───┐ ┌───▼────────┐ ┌───▼──────────┐ ┌──▼─────────┐
231
+ │Alias │ │ Discovery │ │ Enrichment │ │ Resilience │
232
+ │System│ │ Layer │ │ Layer │ │ Layer │
233
+ └──────┘ └───┬────────┘ └──────┬───────┘ └────────────┘
234
+ │ │ CircuitBreaker
235
+ ┌────────┼────────┐ │ HealthTracker
236
+ ▼ ▼ ▼ ▼ StaleCachePolicy
237
+ Direct OpenAI- Cloud litellm
238
+ API Compatible Proxies JSON
239
+ ```
240
+
241
+ ## Documentation
242
+
243
+ | Doc | What's in it |
244
+ |-----|-------------|
245
+ | [Credentials](docs/credentials.md) | Setup for all 21 providers (env vars, CLI tools, config files) |
246
+ | [CLI Reference](docs/cli.md) | All commands, flags, and example output |
247
+ | [HTTP API](docs/api.md) | All endpoints, parameters, and response schemas |
248
+ | [Configuration](docs/configuration.md) | Aliases, routing, pricing enrichment, programmatic config |
249
+ | [Architecture](docs/architecture.md) | Discovery flow, module map, data pipeline, adding providers |
250
+ | [Resilience](docs/resilience.md) | Circuit breakers, stale cache fallback, health monitoring |
251
+ | [Security](docs/security.md) | Threat catalogue, runtime scanning, pre-commit hook |
252
+ | [Discovery Plane v1](docs/discovery-plane-v1.md) | Stable daemon contract (deltas, SSE watch, binding hints) |
253
+
254
+ ## Release & Tagging
255
+
256
+ Package: `@sriinnu/kosha-discovery`
257
+
258
+ This repo uses a human-in-the-loop release flow:
259
+
260
+ 1. Update version in `package.json` and lockfiles locally.
261
+ 2. Create a signed tag and push it:
262
+ ```bash
263
+ git tag -s v0.6.0 -m "v0.6.0"
264
+ git push origin v0.6.0
265
+ ```
266
+ 3. In GitHub Actions, run `Manual Release (Tag + npm)` and provide `tag=v0.6.0`.
267
+ 4. Workflow verifies tag/version match, builds/tests, then publishes to npm (if enabled) and creates a GitHub Release.
268
+
269
+ Required secret for publish:
270
+ - `NPM_TOKEN` (publish rights for `@sriinnu` scope)
271
+
272
+ ## Credits
273
+
274
+ - **[litellm](https://github.com/BerriAI/litellm)** -- Community-maintained model pricing database
275
+ - **[openrouter](https://openrouter.ai)** -- Model aggregation API
276
+ - **[ollama](https://ollama.ai)** -- Local LLM runtime
277
+ - **[chitragupta](https://github.com/sriinnu/chitragupta)** -- Autonomous AI Agent Platform whose registry patterns inspired kosha
278
+ - **[takumi](https://github.com/sriinnu/takumi)** -- AI coding agent TUI whose routing needs drove kosha's creation
279
+
280
+ ## What "Kosha" Means
281
+
282
+ `Kosha` comes from Sanskrit -- a container, treasury, or layered sheath of knowledge. A standalone model-discovery utility for any AI system.
283
+
284
+ ## License
285
+
286
+ MIT
@@ -0,0 +1,51 @@
1
+ /**
2
+ * kosha-discovery — Model alias resolution system.
3
+ *
4
+ * Provides short, memorable names that resolve to canonical model IDs.
5
+ * Built-in aliases are kept in sync with the latest model releases.
6
+ * @module
7
+ */
8
+ /**
9
+ * Curated default aliases mapping short names to canonical model IDs.
10
+ *
11
+ * These are production-ready and cover the most commonly referenced
12
+ * models across major providers. Updated February 2026.
13
+ */
14
+ export declare const DEFAULT_ALIASES: Readonly<Record<string, string>>;
15
+ /**
16
+ * Resolves short alias names to canonical model IDs.
17
+ *
18
+ * Built-in aliases from {@link DEFAULT_ALIASES} are merged with optional
19
+ * user-provided overrides, where user overrides take precedence.
20
+ */
21
+ export declare class AliasResolver {
22
+ /** Internal map holding the merged alias -> canonical ID mappings. */
23
+ private aliases;
24
+ /**
25
+ * @param customAliases - Optional user overrides; these are merged on top
26
+ * of the built-in {@link DEFAULT_ALIASES} map.
27
+ */
28
+ constructor(customAliases?: Record<string, string>);
29
+ /**
30
+ * Resolve an alias to its canonical model ID.
31
+ * Returns the input unchanged if no matching alias is found.
32
+ */
33
+ resolve(nameOrAlias: string): string;
34
+ /**
35
+ * Find all aliases that point to the given canonical model ID.
36
+ */
37
+ reverseAliases(modelId: string): string[];
38
+ /**
39
+ * Add or overwrite an alias mapping.
40
+ */
41
+ addAlias(alias: string, modelId: string): void;
42
+ /**
43
+ * Remove an alias mapping.
44
+ */
45
+ removeAlias(alias: string): void;
46
+ /**
47
+ * Return a snapshot of the full alias map (defaults + custom).
48
+ */
49
+ all(): Record<string, string>;
50
+ }
51
+ //# sourceMappingURL=aliases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aliases.d.ts","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgD5D,CAAC;AAEF;;;;;GAKG;AACH,qBAAa,aAAa;IACzB,sEAAsE;IACtE,OAAO,CAAC,OAAO,CAAsB;IAErC;;;OAGG;gBACS,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAUlD;;;OAGG;IACH,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAIpC;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAUzC;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9C;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIhC;;OAEG;IACH,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CAG7B"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * kosha-discovery — Model alias resolution system.
3
+ *
4
+ * Provides short, memorable names that resolve to canonical model IDs.
5
+ * Built-in aliases are kept in sync with the latest model releases.
6
+ * @module
7
+ */
8
+ /**
9
+ * Curated default aliases mapping short names to canonical model IDs.
10
+ *
11
+ * These are production-ready and cover the most commonly referenced
12
+ * models across major providers. Updated February 2026.
13
+ */
14
+ export const DEFAULT_ALIASES = {
15
+ // ── Anthropic — Claude 4.6 family (latest as of Feb 2026) ──
16
+ "opus": "claude-opus-4-6",
17
+ "opus-4": "claude-opus-4-6",
18
+ "sonnet": "claude-sonnet-4-6",
19
+ "sonnet-4": "claude-sonnet-4-6",
20
+ "haiku": "claude-haiku-4-5-20251001",
21
+ "haiku-4.5": "claude-haiku-4-5-20251001",
22
+ // ── OpenAI — latest reasoning and chat models ──
23
+ "gpt4o": "gpt-4o",
24
+ "gpt4o-mini": "gpt-4o-mini",
25
+ "o1": "o1",
26
+ "o3": "o3",
27
+ "o3-mini": "o3-mini",
28
+ "o4-mini": "o4-mini",
29
+ // ── Google — Gemini 2.5 series ──
30
+ "gemini-pro": "gemini-2.5-pro-preview-05-06",
31
+ "gemini-flash": "gemini-2.5-flash-preview-04-17",
32
+ "gemini-flash-lite": "gemini-2.0-flash-lite",
33
+ // ── Local — latest popular open-weight models for Ollama ──
34
+ "qwen": "qwen3:8b",
35
+ "llama": "llama3.3:latest",
36
+ "codestral": "codestral:latest",
37
+ "deepseek": "deepseek-r1:latest",
38
+ // ── NVIDIA — Nemotron family via build.nvidia.com ──
39
+ "nemotron-ultra": "nvidia/llama-3.1-nemotron-ultra-253b-v1",
40
+ "nemotron-super": "nvidia/llama-3.1-nemotron-super-49b-v1",
41
+ "nemotron-nano": "nvidia/llama-3.1-nemotron-nano-8b-v1",
42
+ // ── Mistral AI — latest models ──
43
+ "mistral-large": "mistral-large-latest",
44
+ "mistral-small": "mistral-small-latest",
45
+ "mistral-codestral": "codestral-latest",
46
+ "pixtral": "pixtral-large-latest",
47
+ // ── Groq — fast inference ──
48
+ "groq-llama": "llama-3.3-70b-versatile",
49
+ "groq-mixtral": "mixtral-8x7b-32768",
50
+ // ── Embeddings — OpenAI, Nomic, and Google embedding models ──
51
+ "embed-small": "text-embedding-3-small",
52
+ "embed-large": "text-embedding-3-large",
53
+ "nomic": "nomic-embed-text",
54
+ "gemini-embed": "gemini-embedding-001",
55
+ };
56
+ /**
57
+ * Resolves short alias names to canonical model IDs.
58
+ *
59
+ * Built-in aliases from {@link DEFAULT_ALIASES} are merged with optional
60
+ * user-provided overrides, where user overrides take precedence.
61
+ */
62
+ export class AliasResolver {
63
+ /** Internal map holding the merged alias -> canonical ID mappings. */
64
+ aliases;
65
+ /**
66
+ * @param customAliases - Optional user overrides; these are merged on top
67
+ * of the built-in {@link DEFAULT_ALIASES} map.
68
+ */
69
+ constructor(customAliases) {
70
+ this.aliases = new Map(Object.entries(DEFAULT_ALIASES));
71
+ if (customAliases) {
72
+ for (const [alias, modelId] of Object.entries(customAliases)) {
73
+ this.aliases.set(alias, modelId);
74
+ }
75
+ }
76
+ }
77
+ /**
78
+ * Resolve an alias to its canonical model ID.
79
+ * Returns the input unchanged if no matching alias is found.
80
+ */
81
+ resolve(nameOrAlias) {
82
+ return this.aliases.get(nameOrAlias) ?? nameOrAlias;
83
+ }
84
+ /**
85
+ * Find all aliases that point to the given canonical model ID.
86
+ */
87
+ reverseAliases(modelId) {
88
+ const result = [];
89
+ for (const [alias, target] of this.aliases) {
90
+ if (target === modelId) {
91
+ result.push(alias);
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+ /**
97
+ * Add or overwrite an alias mapping.
98
+ */
99
+ addAlias(alias, modelId) {
100
+ this.aliases.set(alias, modelId);
101
+ }
102
+ /**
103
+ * Remove an alias mapping.
104
+ */
105
+ removeAlias(alias) {
106
+ this.aliases.delete(alias);
107
+ }
108
+ /**
109
+ * Return a snapshot of the full alias map (defaults + custom).
110
+ */
111
+ all() {
112
+ return Object.fromEntries(this.aliases);
113
+ }
114
+ }
115
+ //# sourceMappingURL=aliases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aliases.js","sourceRoot":"","sources":["../src/aliases.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAqC;IAChE,8DAA8D;IAC9D,MAAM,EAAE,iBAAiB;IACzB,QAAQ,EAAE,iBAAiB;IAC3B,QAAQ,EAAE,mBAAmB;IAC7B,UAAU,EAAE,mBAAmB;IAC/B,OAAO,EAAE,2BAA2B;IACpC,WAAW,EAAE,2BAA2B;IAExC,kDAAkD;IAClD,OAAO,EAAE,QAAQ;IACjB,YAAY,EAAE,aAAa;IAC3B,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,SAAS,EAAE,SAAS;IACpB,SAAS,EAAE,SAAS;IAEpB,mCAAmC;IACnC,YAAY,EAAE,8BAA8B;IAC5C,cAAc,EAAE,gCAAgC;IAChD,mBAAmB,EAAE,uBAAuB;IAE5C,6DAA6D;IAC7D,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,iBAAiB;IAC1B,WAAW,EAAE,kBAAkB;IAC/B,UAAU,EAAE,oBAAoB;IAEhC,sDAAsD;IACtD,gBAAgB,EAAE,yCAAyC;IAC3D,gBAAgB,EAAE,wCAAwC;IAC1D,eAAe,EAAE,sCAAsC;IAEvD,mCAAmC;IACnC,eAAe,EAAE,sBAAsB;IACvC,eAAe,EAAE,sBAAsB;IACvC,mBAAmB,EAAE,kBAAkB;IACvC,SAAS,EAAE,sBAAsB;IAEjC,8BAA8B;IAC9B,YAAY,EAAE,yBAAyB;IACvC,cAAc,EAAE,oBAAoB;IAEpC,gEAAgE;IAChE,aAAa,EAAE,wBAAwB;IACvC,aAAa,EAAE,wBAAwB;IACvC,OAAO,EAAE,kBAAkB;IAC3B,cAAc,EAAE,sBAAsB;CACtC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,OAAO,aAAa;IACzB,sEAAsE;IAC9D,OAAO,CAAsB;IAErC;;;OAGG;IACH,YAAY,aAAsC;QACjD,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;QAExD,IAAI,aAAa,EAAE,CAAC;YACnB,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC9D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAClC,CAAC;QACF,CAAC;IACF,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,WAAmB;QAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,WAAW,CAAC;IACrD,CAAC;IAED;;OAEG;IACH,cAAc,CAAC,OAAe;QAC7B,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC5C,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;QACF,CAAC;QACD,OAAO,MAAM,CAAC;IACf,CAAC;IAED;;OAEG;IACH,QAAQ,CAAC,KAAa,EAAE,OAAe;QACtC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAClC,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,KAAa;QACxB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IAED;;OAEG;IACH,GAAG;QACF,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;CACD"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * kosha-discovery — File-based JSON cache with TTL.
3
+ *
4
+ * Stores discovered provider data as individual JSON files on disk
5
+ * to avoid re-fetching from provider APIs on every startup.
6
+ * Each cache key maps to a `{key}.json` file inside the cache directory.
7
+ * @module
8
+ */
9
+ interface CacheEntry<T> {
10
+ data: T;
11
+ timestamp: number;
12
+ }
13
+ /**
14
+ * Simple file-based JSON cache with TTL support.
15
+ * Stores discovered provider data to avoid re-fetching on every startup.
16
+ *
17
+ * Each cache key maps to a `{key}.json` file inside the cache directory.
18
+ */
19
+ export declare class KoshaCache {
20
+ private readonly cacheDir;
21
+ constructor(cacheDir?: string);
22
+ /**
23
+ * Retrieve cached data by key.
24
+ * Returns null if the entry is missing or the file cannot be read.
25
+ */
26
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
27
+ /**
28
+ * Write data to the cache under the given key.
29
+ *
30
+ * Uses atomic write (temp file + rename) so a crash mid-write
31
+ * cannot leave a corrupted cache file behind.
32
+ */
33
+ set<T>(key: string, data: T): Promise<void>;
34
+ /**
35
+ * Remove a single cached entry by key.
36
+ */
37
+ invalidate(key: string): Promise<void>;
38
+ /**
39
+ * Remove all cache files from the cache directory.
40
+ */
41
+ clear(): Promise<void>;
42
+ /**
43
+ * Check whether a cached timestamp has exceeded the given TTL.
44
+ */
45
+ isExpired(timestamp: number, ttlMs: number): boolean;
46
+ /**
47
+ * Sanitize a cache key into a safe filename and return the full path.
48
+ * Replaces any character that is not alphanumeric, dash, or underscore with an underscore.
49
+ */
50
+ private keyToPath;
51
+ /**
52
+ * Ensure the cache directory exists, creating it recursively if needed.
53
+ */
54
+ private ensureDir;
55
+ }
56
+ export {};
57
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,UAAU,UAAU,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC;IACR,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,QAAQ,CAAC,EAAE,MAAM;IAI7B;;;OAGG;IACG,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAmBxD;;;;;OAKG;IACG,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBjD;;OAEG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5C;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B;;OAEG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAQpD;;;OAGG;IACH,OAAO,CAAC,SAAS;IAKjB;;OAEG;YACW,SAAS;CAGvB"}
package/dist/cache.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * kosha-discovery — File-based JSON cache with TTL.
3
+ *
4
+ * Stores discovered provider data as individual JSON files on disk
5
+ * to avoid re-fetching from provider APIs on every startup.
6
+ * Each cache key maps to a `{key}.json` file inside the cache directory.
7
+ * @module
8
+ */
9
+ import { mkdir, readFile, readdir, rename, unlink, writeFile } from "fs/promises";
10
+ import { randomBytes } from "crypto";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ import { assertCleanPayload } from "./security.js";
14
+ /**
15
+ * Simple file-based JSON cache with TTL support.
16
+ * Stores discovered provider data to avoid re-fetching on every startup.
17
+ *
18
+ * Each cache key maps to a `{key}.json` file inside the cache directory.
19
+ */
20
+ export class KoshaCache {
21
+ cacheDir;
22
+ constructor(cacheDir) {
23
+ this.cacheDir = cacheDir ?? join(homedir(), ".kosha", "cache");
24
+ }
25
+ /**
26
+ * Retrieve cached data by key.
27
+ * Returns null if the entry is missing or the file cannot be read.
28
+ */
29
+ async get(key) {
30
+ try {
31
+ const filePath = this.keyToPath(key);
32
+ const raw = await readFile(filePath, "utf-8");
33
+ const entry = JSON.parse(raw);
34
+ assertCleanPayload(entry, `cache/${key}`);
35
+ return entry;
36
+ }
37
+ catch (err) {
38
+ if (err instanceof Error && err.message.startsWith("Rejected")) {
39
+ // Security violation — poisoned cache file. Log loudly, invalidate, return miss.
40
+ console.error(`KoshaCache SECURITY: ${err.message}`);
41
+ await this.invalidate(key);
42
+ }
43
+ else if (err instanceof SyntaxError) {
44
+ console.warn(`KoshaCache: corrupted cache file for key "${key}"`);
45
+ }
46
+ return null;
47
+ }
48
+ }
49
+ /**
50
+ * Write data to the cache under the given key.
51
+ *
52
+ * Uses atomic write (temp file + rename) so a crash mid-write
53
+ * cannot leave a corrupted cache file behind.
54
+ */
55
+ async set(key, data) {
56
+ await this.ensureDir();
57
+ const entry = {
58
+ data,
59
+ timestamp: Date.now(),
60
+ };
61
+ const filePath = this.keyToPath(key);
62
+ const tmpPath = `${filePath}.${randomBytes(4).toString("hex")}.tmp`;
63
+ try {
64
+ await writeFile(tmpPath, JSON.stringify(entry, null, "\t"), "utf-8");
65
+ await rename(tmpPath, filePath);
66
+ }
67
+ catch (err) {
68
+ await unlink(tmpPath).catch(() => { });
69
+ throw err;
70
+ }
71
+ }
72
+ /**
73
+ * Remove a single cached entry by key.
74
+ */
75
+ async invalidate(key) {
76
+ try {
77
+ const filePath = this.keyToPath(key);
78
+ await unlink(filePath);
79
+ }
80
+ catch {
81
+ // Ignore if file doesn't exist
82
+ }
83
+ }
84
+ /**
85
+ * Remove all cache files from the cache directory.
86
+ */
87
+ async clear() {
88
+ try {
89
+ const files = await readdir(this.cacheDir);
90
+ const removals = files
91
+ .filter((f) => f.endsWith(".json"))
92
+ .map((f) => unlink(join(this.cacheDir, f)).catch(() => { }));
93
+ await Promise.all(removals);
94
+ }
95
+ catch {
96
+ // Directory may not exist yet
97
+ }
98
+ }
99
+ /**
100
+ * Check whether a cached timestamp has exceeded the given TTL.
101
+ */
102
+ isExpired(timestamp, ttlMs) {
103
+ return Date.now() - timestamp > ttlMs;
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // Private helpers
107
+ // ---------------------------------------------------------------------------
108
+ /**
109
+ * Sanitize a cache key into a safe filename and return the full path.
110
+ * Replaces any character that is not alphanumeric, dash, or underscore with an underscore.
111
+ */
112
+ keyToPath(key) {
113
+ const safeKey = key.replace(/[^a-zA-Z0-9\-_]/g, "_");
114
+ return join(this.cacheDir, `${safeKey}.json`);
115
+ }
116
+ /**
117
+ * Ensure the cache directory exists, creating it recursively if needed.
118
+ */
119
+ async ensureDir() {
120
+ await mkdir(this.cacheDir, { recursive: true });
121
+ }
122
+ }
123
+ //# sourceMappingURL=cache.js.map