stacksherpa 1.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.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/db/client.js +444 -0
- package/dist/db/schema.js +254 -0
- package/dist/decisions.js +232 -0
- package/dist/knowledge.js +1146 -0
- package/dist/patterns.js +208 -0
- package/dist/profile.js +533 -0
- package/dist/projects.js +180 -0
- package/dist/scoring.js +72 -0
- package/dist/server.js +604 -0
- package/dist/types.js +20 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alex Stein
|
|
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,107 @@
|
|
|
1
|
+
# stacksherpa
|
|
2
|
+
|
|
3
|
+
Intelligent API recommendation engine for Claude Code. Silently picks the best APIs for your stack based on your project profile, constraints, and past experiences.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
claude plugin add github:alexstein/stacksherpa
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's it. The skill loads automatically and the MCP server starts on first use.
|
|
12
|
+
|
|
13
|
+
## What happens
|
|
14
|
+
|
|
15
|
+
When you ask Claude to implement something that needs an external API (email, payments, auth, etc.), stacksherpa:
|
|
16
|
+
|
|
17
|
+
1. Checks the shared provider catalog (50+ providers across 13 categories)
|
|
18
|
+
2. Scores providers against your project profile (stack, scale, compliance, budget)
|
|
19
|
+
3. Applies taste learned from your past decisions across all projects
|
|
20
|
+
4. Returns the best match with confidence level
|
|
21
|
+
|
|
22
|
+
Claude uses the recommendation silently — you just get the right API without having to research it.
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
**Scoring factors:**
|
|
27
|
+
- Compliance match (SOC2, HIPAA, GDPR, PCI-DSS)
|
|
28
|
+
- Scale fit (hobby → enterprise)
|
|
29
|
+
- Strength alignment with your priorities (DX, reliability, cost, performance)
|
|
30
|
+
- Ecosystem affinity (prefers providers in ecosystems you already use)
|
|
31
|
+
- Past experience (positive/negative outcomes across projects)
|
|
32
|
+
- Pattern inference (learns your preferences over time)
|
|
33
|
+
|
|
34
|
+
**Categories:** email, payments, auth, sms, storage, database, analytics, search, monitoring, ai, push, financial-data, trading, prediction-markets
|
|
35
|
+
|
|
36
|
+
## Tools
|
|
37
|
+
|
|
38
|
+
The MCP server exposes these tools:
|
|
39
|
+
|
|
40
|
+
| Tool | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `recommend` | Get instant recommendation for a category |
|
|
43
|
+
| `get_profile` | View merged project profile + taste |
|
|
44
|
+
| `update_project_profile` | Surgical profile updates (set/append/remove) |
|
|
45
|
+
| `record_decision` | Record an API selection outcome |
|
|
46
|
+
| `get_provider` | Detailed provider info (pricing, issues, benchmarks) |
|
|
47
|
+
| `list_categories` | All categories with provider counts |
|
|
48
|
+
| `get_search_strategy` | Tailored search queries for research |
|
|
49
|
+
| `report_outcome` | Report integration success/failure |
|
|
50
|
+
|
|
51
|
+
## Data
|
|
52
|
+
|
|
53
|
+
- **Shared catalog**: Hosted on Turso (read-only from clients). Providers, pricing, issues, benchmarks.
|
|
54
|
+
- **Local data**: `~/.stacksherpa/` stores your profiles, decisions, and project registry. Never leaves your machine.
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
### Project profile
|
|
59
|
+
|
|
60
|
+
Create `.stacksherpa/profile.json` in your project:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"project": {
|
|
65
|
+
"name": "my-app",
|
|
66
|
+
"stack": { "language": "TypeScript", "framework": "Next.js" },
|
|
67
|
+
"scale": "startup"
|
|
68
|
+
},
|
|
69
|
+
"constraints": {
|
|
70
|
+
"compliance": ["SOC2"]
|
|
71
|
+
},
|
|
72
|
+
"preferences": {
|
|
73
|
+
"prioritize": ["dx", "reliability"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or let Claude update it for you via the `update_project_profile` tool.
|
|
79
|
+
|
|
80
|
+
### Global defaults
|
|
81
|
+
|
|
82
|
+
Set defaults for all projects in `~/.stacksherpa/defaults.json`. Local project profiles override globals.
|
|
83
|
+
|
|
84
|
+
## Advanced
|
|
85
|
+
|
|
86
|
+
### Self-hosting the catalog
|
|
87
|
+
|
|
88
|
+
Set environment variables to point at your own Turso database:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
TURSO_DATABASE_URL=libsql://your-db.turso.io
|
|
92
|
+
TURSO_AUTH_TOKEN=your-read-token
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Seeding the catalog
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
TURSO_WRITE_TOKEN=your-write-token npm run seed:turso
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Running scrapers
|
|
102
|
+
|
|
103
|
+
Scrapers update pricing, issues, and benchmarks. They require write access to Turso and API keys for data sources:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
FIRECRAWL_API_KEY=... TURSO_WRITE_TOKEN=... npm run cron:daily
|
|
107
|
+
```
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Client (Turso / libSQL)
|
|
3
|
+
*
|
|
4
|
+
* Reads provider catalog from a shared Turso database.
|
|
5
|
+
* All queries are async. Write methods are only available
|
|
6
|
+
* when TURSO_WRITE_TOKEN is set (for scrapers/admin).
|
|
7
|
+
*/
|
|
8
|
+
import { createClient } from '@libsql/client';
|
|
9
|
+
import { SCHEMA } from './schema.js';
|
|
10
|
+
// Turso connection — defaults to shared read-only catalog
|
|
11
|
+
const TURSO_URL = process.env.TURSO_DATABASE_URL ?? 'libsql://api-broker-astein91.aws-us-west-2.turso.io';
|
|
12
|
+
const TURSO_READ_TOKEN = process.env.TURSO_AUTH_TOKEN ?? 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicm8iLCJpYXQiOjE3NzA5MTg3OTUsImlkIjoiYjA4YmZkNTUtZjhhNi00ODU5LThjNzMtYjg4NzIzMmI4YmYyIiwicmlkIjoiMWJjN2NhMTgtMDU1OC00NjI2LWJjY2YtOGVmMWIwZjQ1ZDAxIn0.aZ4gR9f0I9Qq8T_tkQp-uXwL-4LzUPGaGCDATirOFBBu9kHmuFs5cfXXJ162RzfX2Nrsn-HEAx_H8-uSSfiKBA';
|
|
13
|
+
const TURSO_WRITE_TOKEN = process.env.TURSO_WRITE_TOKEN;
|
|
14
|
+
let client = null;
|
|
15
|
+
export function getClient() {
|
|
16
|
+
if (!client) {
|
|
17
|
+
client = createClient({
|
|
18
|
+
url: TURSO_URL,
|
|
19
|
+
authToken: TURSO_READ_TOKEN || undefined,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return client;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the database schema (for local/embedded use or first-time Turso setup).
|
|
26
|
+
* In production, schema is managed via Turso CLI.
|
|
27
|
+
*/
|
|
28
|
+
export async function ensureSchema() {
|
|
29
|
+
const db = getClient();
|
|
30
|
+
// Split SCHEMA into individual statements and execute each
|
|
31
|
+
const statements = SCHEMA
|
|
32
|
+
.split(';')
|
|
33
|
+
.map(s => s.trim())
|
|
34
|
+
.filter(s => s.length > 0);
|
|
35
|
+
for (const stmt of statements) {
|
|
36
|
+
await db.execute(stmt);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// ============================================
|
|
40
|
+
// Query Methods (for MCP server)
|
|
41
|
+
// ============================================
|
|
42
|
+
/**
|
|
43
|
+
* Get all providers in a category
|
|
44
|
+
*/
|
|
45
|
+
export async function getProvidersByCategory(category) {
|
|
46
|
+
const db = getClient();
|
|
47
|
+
const result = await db.execute({
|
|
48
|
+
sql: `SELECT * FROM providers WHERE category = ? AND status = 'active'`,
|
|
49
|
+
args: [category],
|
|
50
|
+
});
|
|
51
|
+
return result.rows.map(row => rowToProvider(row));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get a single provider by ID with all related data
|
|
55
|
+
*/
|
|
56
|
+
export async function getProviderById(id) {
|
|
57
|
+
const db = getClient();
|
|
58
|
+
const result = await db.execute({
|
|
59
|
+
sql: `SELECT * FROM providers WHERE id = ?`,
|
|
60
|
+
args: [id],
|
|
61
|
+
});
|
|
62
|
+
if (result.rows.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
const row = result.rows[0];
|
|
65
|
+
const provider = rowToProvider(row);
|
|
66
|
+
// Get latest pricing
|
|
67
|
+
const pricingResult = await db.execute({
|
|
68
|
+
sql: `SELECT * FROM latest_pricing WHERE provider_id = ?`,
|
|
69
|
+
args: [id],
|
|
70
|
+
});
|
|
71
|
+
if (pricingResult.rows.length > 0) {
|
|
72
|
+
provider.pricing = rowToPricing(pricingResult.rows[0]);
|
|
73
|
+
}
|
|
74
|
+
// Get known issues
|
|
75
|
+
const issuesResult = await db.execute({
|
|
76
|
+
sql: `SELECT * FROM active_issues WHERE provider_id = ?`,
|
|
77
|
+
args: [id],
|
|
78
|
+
});
|
|
79
|
+
if (issuesResult.rows.length > 0) {
|
|
80
|
+
provider.knownIssues = issuesResult.rows.map(r => rowToKnownIssue(r));
|
|
81
|
+
}
|
|
82
|
+
// Get AI benchmarks if applicable
|
|
83
|
+
if (row.category === 'ai') {
|
|
84
|
+
const benchResult = await db.execute({
|
|
85
|
+
sql: `SELECT * FROM latest_ai_benchmarks WHERE provider_id = ?`,
|
|
86
|
+
args: [id],
|
|
87
|
+
});
|
|
88
|
+
if (benchResult.rows.length > 0) {
|
|
89
|
+
provider.aiBenchmarks = rowToAiBenchmarks(benchResult.rows[0]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return provider;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Search providers with filters
|
|
96
|
+
*/
|
|
97
|
+
export async function searchProviders(filters) {
|
|
98
|
+
const db = getClient();
|
|
99
|
+
let sql = `
|
|
100
|
+
SELECT DISTINCT p.* FROM providers p
|
|
101
|
+
LEFT JOIN latest_pricing pr ON p.id = pr.provider_id
|
|
102
|
+
WHERE p.status = 'active'
|
|
103
|
+
`;
|
|
104
|
+
const args = [];
|
|
105
|
+
if (filters.category) {
|
|
106
|
+
sql += ` AND p.category = ?`;
|
|
107
|
+
args.push(filters.category);
|
|
108
|
+
}
|
|
109
|
+
if (filters.compliance && filters.compliance.length > 0) {
|
|
110
|
+
for (const c of filters.compliance) {
|
|
111
|
+
sql += ` AND EXISTS (SELECT 1 FROM json_each(p.compliance) WHERE json_each.value = ?)`;
|
|
112
|
+
args.push(c);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (filters.scale) {
|
|
116
|
+
sql += ` AND EXISTS (SELECT 1 FROM json_each(p.best_for) WHERE json_each.value = ?)`;
|
|
117
|
+
args.push(filters.scale);
|
|
118
|
+
}
|
|
119
|
+
if (filters.hasFreeTier) {
|
|
120
|
+
sql += ` AND pr.free_tier_included IS NOT NULL`;
|
|
121
|
+
}
|
|
122
|
+
if (filters.maxPricePerUnit !== undefined) {
|
|
123
|
+
sql += ` AND (pr.unit_price IS NULL OR pr.unit_price <= ?)`;
|
|
124
|
+
args.push(filters.maxPricePerUnit);
|
|
125
|
+
}
|
|
126
|
+
const result = await db.execute({ sql, args });
|
|
127
|
+
return result.rows.map(row => rowToProvider(row));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get active issues for a provider
|
|
131
|
+
*/
|
|
132
|
+
export async function getActiveIssues(providerId) {
|
|
133
|
+
const db = getClient();
|
|
134
|
+
const result = await db.execute({
|
|
135
|
+
sql: `SELECT * FROM active_issues WHERE provider_id = ?`,
|
|
136
|
+
args: [providerId],
|
|
137
|
+
});
|
|
138
|
+
return result.rows.map(r => rowToKnownIssue(r));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get all categories with provider counts
|
|
142
|
+
*/
|
|
143
|
+
export async function getCategories() {
|
|
144
|
+
const db = getClient();
|
|
145
|
+
const result = await db.execute(`
|
|
146
|
+
SELECT category, COUNT(*) as count
|
|
147
|
+
FROM providers
|
|
148
|
+
WHERE status = 'active'
|
|
149
|
+
GROUP BY category
|
|
150
|
+
ORDER BY count DESC
|
|
151
|
+
`);
|
|
152
|
+
return result.rows.map(r => ({
|
|
153
|
+
category: r.category,
|
|
154
|
+
count: Number(r.count),
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get a map of provider name (lowercase) -> ecosystem for all providers that have one.
|
|
159
|
+
* Used for ecosystem affinity scoring without loading all providers.
|
|
160
|
+
*/
|
|
161
|
+
export async function getProviderEcosystems() {
|
|
162
|
+
const db = getClient();
|
|
163
|
+
const result = await db.execute(`
|
|
164
|
+
SELECT LOWER(name) as name, ecosystem
|
|
165
|
+
FROM providers
|
|
166
|
+
WHERE ecosystem IS NOT NULL AND ecosystem != '' AND status = 'active'
|
|
167
|
+
`);
|
|
168
|
+
return new Map(result.rows.map(r => [r.name, r.ecosystem]));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get providers needing updates (stale data)
|
|
172
|
+
*/
|
|
173
|
+
export async function getStaleProviders(daysSinceUpdate = 30) {
|
|
174
|
+
const db = getClient();
|
|
175
|
+
const cutoff = new Date(Date.now() - daysSinceUpdate * 24 * 60 * 60 * 1000).toISOString();
|
|
176
|
+
const result = await db.execute({
|
|
177
|
+
sql: `
|
|
178
|
+
SELECT * FROM providers
|
|
179
|
+
WHERE last_verified < ? OR last_verified IS NULL
|
|
180
|
+
ORDER BY last_verified ASC NULLS FIRST
|
|
181
|
+
`,
|
|
182
|
+
args: [cutoff],
|
|
183
|
+
});
|
|
184
|
+
return result.rows;
|
|
185
|
+
}
|
|
186
|
+
// ============================================
|
|
187
|
+
// Write Methods (for scrapers/admin — requires TURSO_WRITE_TOKEN)
|
|
188
|
+
// ============================================
|
|
189
|
+
function getWriteClient() {
|
|
190
|
+
if (!TURSO_WRITE_TOKEN) {
|
|
191
|
+
throw new Error('TURSO_WRITE_TOKEN is required for write operations');
|
|
192
|
+
}
|
|
193
|
+
return createClient({
|
|
194
|
+
url: TURSO_URL,
|
|
195
|
+
authToken: TURSO_WRITE_TOKEN,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Upsert a provider (requires write token)
|
|
200
|
+
*/
|
|
201
|
+
export async function upsertProvider(provider) {
|
|
202
|
+
const db = getWriteClient();
|
|
203
|
+
await db.execute({
|
|
204
|
+
sql: `
|
|
205
|
+
INSERT INTO providers (
|
|
206
|
+
id, name, description, category, subcategories, status,
|
|
207
|
+
website, docs_url, package, package_alt_names,
|
|
208
|
+
compliance, data_residency, self_hostable, on_prem_option,
|
|
209
|
+
strengths, weaknesses, best_for,
|
|
210
|
+
avoid_if, requires, best_when, alternatives,
|
|
211
|
+
ecosystem, updated_at, last_verified
|
|
212
|
+
) VALUES (
|
|
213
|
+
?, ?, ?, ?, ?, ?,
|
|
214
|
+
?, ?, ?, ?,
|
|
215
|
+
?, ?, ?, ?,
|
|
216
|
+
?, ?, ?,
|
|
217
|
+
?, ?, ?, ?,
|
|
218
|
+
?, CURRENT_TIMESTAMP, ?
|
|
219
|
+
)
|
|
220
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
221
|
+
name = excluded.name,
|
|
222
|
+
description = excluded.description,
|
|
223
|
+
category = excluded.category,
|
|
224
|
+
subcategories = excluded.subcategories,
|
|
225
|
+
status = excluded.status,
|
|
226
|
+
website = excluded.website,
|
|
227
|
+
docs_url = excluded.docs_url,
|
|
228
|
+
package = excluded.package,
|
|
229
|
+
package_alt_names = excluded.package_alt_names,
|
|
230
|
+
compliance = excluded.compliance,
|
|
231
|
+
data_residency = excluded.data_residency,
|
|
232
|
+
self_hostable = excluded.self_hostable,
|
|
233
|
+
on_prem_option = excluded.on_prem_option,
|
|
234
|
+
strengths = excluded.strengths,
|
|
235
|
+
weaknesses = excluded.weaknesses,
|
|
236
|
+
best_for = excluded.best_for,
|
|
237
|
+
avoid_if = excluded.avoid_if,
|
|
238
|
+
requires = excluded.requires,
|
|
239
|
+
best_when = excluded.best_when,
|
|
240
|
+
alternatives = excluded.alternatives,
|
|
241
|
+
ecosystem = excluded.ecosystem,
|
|
242
|
+
updated_at = CURRENT_TIMESTAMP,
|
|
243
|
+
last_verified = excluded.last_verified
|
|
244
|
+
`,
|
|
245
|
+
args: [
|
|
246
|
+
provider.id ?? provider.name.toLowerCase().replace(/\s+/g, '-'),
|
|
247
|
+
provider.name,
|
|
248
|
+
provider.description ?? null,
|
|
249
|
+
provider.category ?? 'unknown',
|
|
250
|
+
JSON.stringify(provider.subcategories ?? []),
|
|
251
|
+
provider.status ?? 'active',
|
|
252
|
+
provider.website ?? null,
|
|
253
|
+
provider.docsUrl ?? null,
|
|
254
|
+
provider.package ?? null,
|
|
255
|
+
JSON.stringify(provider.packageAltNames ?? {}),
|
|
256
|
+
JSON.stringify(provider.compliance ?? []),
|
|
257
|
+
JSON.stringify(provider.dataResidency ?? []),
|
|
258
|
+
provider.selfHostable ? 1 : 0,
|
|
259
|
+
provider.onPremOption ? 1 : 0,
|
|
260
|
+
JSON.stringify(provider.strengths ?? []),
|
|
261
|
+
JSON.stringify(provider.weaknesses ?? []),
|
|
262
|
+
JSON.stringify(provider.bestFor ?? provider.scale ?? []),
|
|
263
|
+
JSON.stringify(provider.avoidIf ?? []),
|
|
264
|
+
JSON.stringify(provider.requires ?? []),
|
|
265
|
+
JSON.stringify(provider.bestWhen ?? []),
|
|
266
|
+
JSON.stringify(provider.alternatives ?? []),
|
|
267
|
+
provider.ecosystem ?? null,
|
|
268
|
+
provider.lastVerified ?? null,
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Insert pricing data (append-only for history, requires write token)
|
|
274
|
+
*/
|
|
275
|
+
export async function insertPricing(providerId, pricing) {
|
|
276
|
+
const db = getWriteClient();
|
|
277
|
+
await db.execute({
|
|
278
|
+
sql: `
|
|
279
|
+
INSERT INTO pricing (
|
|
280
|
+
provider_id, pricing_type, currency,
|
|
281
|
+
free_tier_included, free_tier_limitations,
|
|
282
|
+
unit, unit_price, volume_discounts,
|
|
283
|
+
plans, source_url, scraped_at, confidence
|
|
284
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
285
|
+
`,
|
|
286
|
+
args: [
|
|
287
|
+
providerId,
|
|
288
|
+
pricing.type,
|
|
289
|
+
pricing.currency,
|
|
290
|
+
pricing.freeTier?.included ?? null,
|
|
291
|
+
JSON.stringify(pricing.freeTier?.limitations ?? []),
|
|
292
|
+
pricing.unitPricing?.unit ?? null,
|
|
293
|
+
pricing.unitPricing?.price ?? null,
|
|
294
|
+
JSON.stringify(pricing.unitPricing?.volumeDiscounts ?? []),
|
|
295
|
+
JSON.stringify(pricing.plans ?? []),
|
|
296
|
+
pricing.source ?? null,
|
|
297
|
+
pricing.lastVerified,
|
|
298
|
+
'medium',
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Upsert a known issue (requires write token)
|
|
304
|
+
*/
|
|
305
|
+
export async function upsertKnownIssue(providerId, issue) {
|
|
306
|
+
const db = getWriteClient();
|
|
307
|
+
await db.execute({
|
|
308
|
+
sql: `
|
|
309
|
+
INSERT INTO known_issues (
|
|
310
|
+
id, provider_id, symptom, scope, workaround, severity,
|
|
311
|
+
affected_versions, github_issue_url,
|
|
312
|
+
reported_at, resolved_at, confidence, updated_at
|
|
313
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
314
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
315
|
+
symptom = excluded.symptom,
|
|
316
|
+
scope = excluded.scope,
|
|
317
|
+
workaround = excluded.workaround,
|
|
318
|
+
severity = excluded.severity,
|
|
319
|
+
affected_versions = excluded.affected_versions,
|
|
320
|
+
resolved_at = excluded.resolved_at,
|
|
321
|
+
confidence = excluded.confidence,
|
|
322
|
+
updated_at = CURRENT_TIMESTAMP
|
|
323
|
+
`,
|
|
324
|
+
args: [
|
|
325
|
+
issue.id,
|
|
326
|
+
providerId,
|
|
327
|
+
issue.symptom,
|
|
328
|
+
issue.scope ?? null,
|
|
329
|
+
issue.workaround ?? null,
|
|
330
|
+
issue.severity,
|
|
331
|
+
issue.affectedVersions ?? null,
|
|
332
|
+
issue.githubIssue ?? null,
|
|
333
|
+
issue.reportedAt ?? null,
|
|
334
|
+
issue.resolvedAt ?? null,
|
|
335
|
+
issue.confidence,
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// ============================================
|
|
340
|
+
// Row to Type Converters
|
|
341
|
+
// ============================================
|
|
342
|
+
function parseJson(json) {
|
|
343
|
+
if (!json)
|
|
344
|
+
return undefined;
|
|
345
|
+
try {
|
|
346
|
+
return JSON.parse(json);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function str(val) {
|
|
353
|
+
if (val === null || val === undefined)
|
|
354
|
+
return null;
|
|
355
|
+
return String(val);
|
|
356
|
+
}
|
|
357
|
+
function num(val) {
|
|
358
|
+
if (val === null || val === undefined)
|
|
359
|
+
return null;
|
|
360
|
+
return Number(val);
|
|
361
|
+
}
|
|
362
|
+
function rowToProvider(row) {
|
|
363
|
+
return {
|
|
364
|
+
id: row.id,
|
|
365
|
+
name: row.name,
|
|
366
|
+
description: row.description ?? undefined,
|
|
367
|
+
category: row.category,
|
|
368
|
+
subcategories: parseJson(row.subcategories),
|
|
369
|
+
status: row.status,
|
|
370
|
+
website: row.website ?? undefined,
|
|
371
|
+
docsUrl: row.docs_url ?? undefined,
|
|
372
|
+
package: row.package ?? undefined,
|
|
373
|
+
packageAltNames: parseJson(row.package_alt_names),
|
|
374
|
+
compliance: parseJson(row.compliance),
|
|
375
|
+
dataResidency: parseJson(row.data_residency),
|
|
376
|
+
selfHostable: row.self_hostable === 1,
|
|
377
|
+
onPremOption: row.on_prem_option === 1,
|
|
378
|
+
strengths: parseJson(row.strengths),
|
|
379
|
+
weaknesses: parseJson(row.weaknesses),
|
|
380
|
+
bestFor: parseJson(row.best_for),
|
|
381
|
+
avoidIf: parseJson(row.avoid_if),
|
|
382
|
+
requires: parseJson(row.requires),
|
|
383
|
+
bestWhen: parseJson(row.best_when),
|
|
384
|
+
alternatives: parseJson(row.alternatives),
|
|
385
|
+
ecosystem: row.ecosystem ?? undefined,
|
|
386
|
+
lastVerified: row.last_verified ?? undefined,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function rowToPricing(row) {
|
|
390
|
+
return {
|
|
391
|
+
type: row.pricing_type ?? 'usage',
|
|
392
|
+
currency: row.currency,
|
|
393
|
+
freeTier: row.free_tier_included ? {
|
|
394
|
+
included: row.free_tier_included,
|
|
395
|
+
limitations: parseJson(row.free_tier_limitations),
|
|
396
|
+
} : undefined,
|
|
397
|
+
unitPricing: row.unit ? {
|
|
398
|
+
unit: row.unit,
|
|
399
|
+
price: row.unit_price ?? 0,
|
|
400
|
+
volumeDiscounts: parseJson(row.volume_discounts),
|
|
401
|
+
} : undefined,
|
|
402
|
+
plans: parseJson(row.plans),
|
|
403
|
+
lastVerified: row.scraped_at,
|
|
404
|
+
source: row.source_url ?? undefined,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function rowToKnownIssue(row) {
|
|
408
|
+
return {
|
|
409
|
+
id: row.id,
|
|
410
|
+
symptom: row.symptom,
|
|
411
|
+
scope: row.scope ?? '',
|
|
412
|
+
workaround: row.workaround ?? undefined,
|
|
413
|
+
severity: row.severity,
|
|
414
|
+
affectedVersions: row.affected_versions ?? undefined,
|
|
415
|
+
githubIssue: row.github_issue_url ?? undefined,
|
|
416
|
+
reportedAt: row.reported_at ?? '',
|
|
417
|
+
resolvedAt: row.resolved_at ?? undefined,
|
|
418
|
+
confidence: row.confidence,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function rowToAiBenchmarks(row) {
|
|
422
|
+
return {
|
|
423
|
+
lmArena: row.lmarena_elo ? {
|
|
424
|
+
elo: Number(row.lmarena_elo),
|
|
425
|
+
rank: num(row.lmarena_rank) ?? undefined,
|
|
426
|
+
category: str(row.lmarena_category) ?? undefined,
|
|
427
|
+
measuredAt: str(row.measured_at) ?? '',
|
|
428
|
+
} : undefined,
|
|
429
|
+
artificialAnalysis: row.aa_quality_index ? {
|
|
430
|
+
qualityIndex: Number(row.aa_quality_index),
|
|
431
|
+
speedIndex: num(row.aa_speed_index) ?? undefined,
|
|
432
|
+
pricePerMToken: num(row.aa_price_per_m_token) ?? undefined,
|
|
433
|
+
tokensPerSecond: num(row.aa_tokens_per_second) ?? undefined,
|
|
434
|
+
ttft: num(row.aa_ttft_ms) ?? undefined,
|
|
435
|
+
measuredAt: str(row.measured_at) ?? '',
|
|
436
|
+
} : undefined,
|
|
437
|
+
contextWindow: row.context_max_tokens ? {
|
|
438
|
+
maxTokens: Number(row.context_max_tokens),
|
|
439
|
+
effectiveTokens: num(row.context_effective_tokens) ?? undefined,
|
|
440
|
+
} : undefined,
|
|
441
|
+
capabilities: parseJson(str(row.capabilities)),
|
|
442
|
+
benchmarks: parseJson(str(row.benchmarks)),
|
|
443
|
+
};
|
|
444
|
+
}
|