@kylewadegrove/cutline-mcp-cli 0.6.0 → 0.6.1

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.
@@ -0,0 +1,464 @@
1
+ import {
2
+ getStoredToken
3
+ } from "./chunk-NUBIEJTU.js";
4
+
5
+ // ../mcp/dist/mcp/src/data-client.js
6
+ function getBaseUrl(environment) {
7
+ return environment === "staging" ? "https://us-central1-cutline-staging.cloudfunctions.net" : "https://us-central1-cutline-prod.cloudfunctions.net";
8
+ }
9
+ var cachedBaseUrl;
10
+ var cachedIdToken;
11
+ var tokenExpiresAt = 0;
12
+ var FIREBASE_API_KEY_CACHE = {};
13
+ async function getFirebaseApiKey(environment) {
14
+ const envKey = process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
15
+ if (envKey)
16
+ return envKey;
17
+ if (FIREBASE_API_KEY_CACHE.key && FIREBASE_API_KEY_CACHE.env === environment && FIREBASE_API_KEY_CACHE.at && Date.now() - FIREBASE_API_KEY_CACHE.at < 36e5) {
18
+ return FIREBASE_API_KEY_CACHE.key;
19
+ }
20
+ const baseUrl = environment === "staging" ? "https://cutline-staging.web.app" : "https://thecutline.ai";
21
+ const res = await fetch(`${baseUrl}/api/firebase-config`, {
22
+ headers: { Accept: "application/json" },
23
+ signal: AbortSignal.timeout(5e3)
24
+ });
25
+ if (!res.ok)
26
+ throw new Error(`Failed to fetch firebase config: HTTP ${res.status}`);
27
+ const data = await res.json();
28
+ if (!data.apiKey)
29
+ throw new Error("No apiKey in firebase-config response");
30
+ FIREBASE_API_KEY_CACHE.key = data.apiKey;
31
+ FIREBASE_API_KEY_CACHE.env = environment;
32
+ FIREBASE_API_KEY_CACHE.at = Date.now();
33
+ return data.apiKey;
34
+ }
35
+ async function exchangeRefreshForId(refreshToken, environment) {
36
+ const apiKey = await getFirebaseApiKey(environment);
37
+ const res = await fetch(`https://securetoken.googleapis.com/v1/token?key=${apiKey}`, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify({ grant_type: "refresh_token", refresh_token: refreshToken }),
41
+ signal: AbortSignal.timeout(1e4)
42
+ });
43
+ if (!res.ok) {
44
+ const err = await res.json().catch(() => ({}));
45
+ throw new Error(`Token exchange failed: ${err?.error?.message || res.status}`);
46
+ }
47
+ const data = await res.json();
48
+ return data.id_token;
49
+ }
50
+ async function resolveAuth() {
51
+ if (cachedIdToken && Date.now() < tokenExpiresAt && cachedBaseUrl) {
52
+ return { baseUrl: cachedBaseUrl, idToken: cachedIdToken };
53
+ }
54
+ const stored = await getStoredToken();
55
+ if (!stored) {
56
+ throw new Error("Not authenticated. Run 'cutline-mcp login' first.");
57
+ }
58
+ const env = stored.environment || "production";
59
+ const baseUrl = getBaseUrl(env);
60
+ const idToken = await exchangeRefreshForId(stored.refreshToken, env);
61
+ cachedBaseUrl = baseUrl;
62
+ cachedIdToken = idToken;
63
+ tokenExpiresAt = Date.now() + 50 * 60 * 1e3;
64
+ return { baseUrl, idToken };
65
+ }
66
+ async function proxy(action, params = {}) {
67
+ const { baseUrl, idToken } = await resolveAuth();
68
+ const res = await fetch(`${baseUrl}/mcpDataProxy`, {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ Authorization: `Bearer ${idToken}`
73
+ },
74
+ body: JSON.stringify({ action, params }),
75
+ signal: AbortSignal.timeout(3e4)
76
+ });
77
+ if (!res.ok) {
78
+ if (res.status === 401) {
79
+ cachedIdToken = void 0;
80
+ tokenExpiresAt = 0;
81
+ throw new Error("Authentication expired \u2014 retrying on next call");
82
+ }
83
+ const body = await res.text().catch(() => "");
84
+ throw new Error(`mcpDataProxy ${action} failed (${res.status}): ${body.slice(0, 200)}`);
85
+ }
86
+ return res.json();
87
+ }
88
+ async function listPremortems(opts) {
89
+ const res = await proxy("premortem.list", opts || {});
90
+ return res.docs;
91
+ }
92
+ async function getPremortem(id) {
93
+ const res = await proxy("premortem.get", { id });
94
+ return res.doc;
95
+ }
96
+ async function createPremortem(data, id) {
97
+ return proxy("premortem.create", { data, id });
98
+ }
99
+ async function updatePremortem(id, data) {
100
+ return proxy("premortem.update", { id, data });
101
+ }
102
+ async function getIdeaReport(id) {
103
+ const res = await proxy("premortem.getIdea", { id });
104
+ return res.doc;
105
+ }
106
+ async function saveChat(id, data) {
107
+ return proxy("chat.save", { id, data });
108
+ }
109
+ async function getChat(id) {
110
+ const res = await proxy("chat.get", { id });
111
+ return res.doc;
112
+ }
113
+ async function updateChat(id, data) {
114
+ return proxy("chat.update", { id, data });
115
+ }
116
+ async function createExplorationSession(id, data, collection) {
117
+ return proxy("exploration.create", { id, data, collection });
118
+ }
119
+ async function getExplorationSession(id, collection) {
120
+ const res = await proxy("exploration.get", { id, collection });
121
+ return res.doc;
122
+ }
123
+ async function updateExplorationSession(id, data, collection) {
124
+ return proxy("exploration.update", { id, data, collection });
125
+ }
126
+ async function listExplorationSessions(collection, limit) {
127
+ const res = await proxy("exploration.list", { collection, limit });
128
+ return res.docs;
129
+ }
130
+ async function getAllEntities(productId) {
131
+ const res = await proxy("graph.getEntities", { productId });
132
+ return res.docs.map((d) => ({ ...d, ingested_at: new Date(d.ingested_at?._seconds ? d.ingested_at._seconds * 1e3 : d.ingested_at) }));
133
+ }
134
+ async function upsertEntities(productId, _sourceType, _sourceId, entities) {
135
+ await proxy("graph.upsertEntities", { productId, entities: entities.map((e) => ({ ...e, ingested_at: e.ingested_at?.toISOString?.() ?? e.ingested_at })) });
136
+ }
137
+ async function addEntity(productId, entity) {
138
+ await proxy("graph.upsertEntities", { productId, entities: [{ ...entity, ingested_at: entity.ingested_at?.toISOString?.() ?? entity.ingested_at }] });
139
+ }
140
+ async function getEntityById(productId, entityId) {
141
+ const all = await getAllEntities(productId);
142
+ return all.find((e) => e.id === entityId) ?? null;
143
+ }
144
+ async function getEntitiesWithEmbeddings(productId) {
145
+ const all = await getAllEntities(productId);
146
+ return all.filter((e) => e.embedding && e.embedding.length > 0);
147
+ }
148
+ async function updateEntityEmbedding(productId, entityId, embedding) {
149
+ await proxy("graph.updateEmbedding", { productId, collection: "entities", docId: entityId, embedding });
150
+ }
151
+ async function getAllEdges(productId) {
152
+ const res = await proxy("graph.getEdges", { productId });
153
+ return res.docs;
154
+ }
155
+ async function upsertEdges(productId, _sourceId, edges) {
156
+ await proxy("graph.upsertEdges", { productId, edges });
157
+ }
158
+ async function addEdges(productId, edges) {
159
+ await proxy("graph.upsertEdges", { productId, edges });
160
+ }
161
+ async function getAllBindings(productId) {
162
+ const res = await proxy("graph.getBindings", { productId });
163
+ return res.docs;
164
+ }
165
+ async function getBindingsForEntity(productId, entityId) {
166
+ const res = await proxy("graph.getBindings", { productId, entityId });
167
+ return res.docs;
168
+ }
169
+ async function upsertBindings(productId, bindings) {
170
+ await proxy("graph.upsertBindings", { productId, bindings });
171
+ }
172
+ async function deleteBinding(productId, bindingId) {
173
+ await proxy("graph.upsertBindings", { productId, deleteIds: [bindingId] });
174
+ }
175
+ var NODE_CACHE_TTL = 10 * 60 * 1e3;
176
+ var nodeCache = /* @__PURE__ */ new Map();
177
+ var lightNodeCache = /* @__PURE__ */ new Map();
178
+ function invalidateNodeCache(productId) {
179
+ nodeCache.delete(productId);
180
+ lightNodeCache.delete(productId);
181
+ }
182
+ async function getAllNodes(productId) {
183
+ const cached = nodeCache.get(productId);
184
+ if (cached && Date.now() - cached.fetchedAt < NODE_CACHE_TTL)
185
+ return cached.nodes;
186
+ const res = await proxy("graph.getNodes", { productId });
187
+ const nodes = res.docs.map((d) => ({
188
+ ...d,
189
+ ingested_at: new Date(d.ingested_at?._seconds ? d.ingested_at._seconds * 1e3 : d.ingested_at)
190
+ }));
191
+ nodeCache.set(productId, { nodes, fetchedAt: Date.now() });
192
+ return nodes;
193
+ }
194
+ async function getAllNodesLight(productId) {
195
+ const cached = lightNodeCache.get(productId);
196
+ if (cached && Date.now() - cached.fetchedAt < NODE_CACHE_TTL)
197
+ return cached.nodes;
198
+ const res = await proxy("graph.getNodes", { productId, light: true });
199
+ const nodes = res.docs.map((d) => ({
200
+ ...d,
201
+ ingested_at: new Date(d.ingested_at?._seconds ? d.ingested_at._seconds * 1e3 : d.ingested_at)
202
+ }));
203
+ lightNodeCache.set(productId, { nodes, fetchedAt: Date.now() });
204
+ return nodes;
205
+ }
206
+ async function getNodesByCategories(productId, categories) {
207
+ const res = await proxy("graph.getNodes", { productId, categories: categories.slice(0, 10) });
208
+ return res.docs.map((d) => ({
209
+ ...d,
210
+ ingested_at: new Date(d.ingested_at?._seconds ? d.ingested_at._seconds * 1e3 : d.ingested_at)
211
+ }));
212
+ }
213
+ async function getNodesBySource(productId, sourceType) {
214
+ const res = await proxy("graph.getNodes", { productId, sourceType });
215
+ return res.docs.map((d) => ({
216
+ ...d,
217
+ ingested_at: new Date(d.ingested_at?._seconds ? d.ingested_at._seconds * 1e3 : d.ingested_at)
218
+ }));
219
+ }
220
+ async function upsertNodes(productId, _sourceType, _sourceId, nodes) {
221
+ await proxy("graph.upsertNodes", { productId, nodes: nodes.map((n) => ({ ...n, ingested_at: n.ingested_at?.toISOString?.() ?? n.ingested_at })) });
222
+ invalidateNodeCache(productId);
223
+ }
224
+ async function addNodes(productId, nodes) {
225
+ await proxy("graph.upsertNodes", { productId, nodes: nodes.map((n) => ({ ...n, ingested_at: n.ingested_at?.toISOString?.() ?? n.ingested_at })) });
226
+ invalidateNodeCache(productId);
227
+ }
228
+ async function deleteAllNodes(productId) {
229
+ await proxy("graph.deleteAll", { productId });
230
+ invalidateNodeCache(productId);
231
+ }
232
+ async function hasConstraints(productId) {
233
+ const res = await proxy("graph.hasConstraints", { productId });
234
+ return res.hasConstraints;
235
+ }
236
+ async function searchNodesByKeywords(productId, keywords) {
237
+ const allNodes = await getAllNodes(productId);
238
+ const lowerKeywords = keywords.map((k) => k.toLowerCase());
239
+ return allNodes.filter((node) => {
240
+ const nodeKeywords = (node.keywords || []).map((k) => k.toLowerCase());
241
+ return lowerKeywords.some((kw) => nodeKeywords.some((nk) => nk.includes(kw) || kw.includes(nk)));
242
+ });
243
+ }
244
+ async function getNodesWithEmbeddings(productId) {
245
+ const all = await getAllNodes(productId);
246
+ return all.filter((n) => n.embedding && n.embedding.length > 0);
247
+ }
248
+ async function getNodesMissingEmbeddings(productId) {
249
+ const all = await getAllNodes(productId);
250
+ return all.filter((n) => !n.embedding || n.embedding.length === 0);
251
+ }
252
+ async function updateNodeEmbeddings(productId, updates) {
253
+ for (const { id, embedding } of updates) {
254
+ await proxy("graph.updateEmbedding", { productId, collection: "nodes", docId: id, embedding });
255
+ }
256
+ invalidateNodeCache(productId);
257
+ }
258
+ async function addTestCases(productId, cases) {
259
+ await proxy("graph.addTestCases", { productId, testCases: cases });
260
+ }
261
+ async function getTestCasesForEntity(productId, entityId) {
262
+ const res = await proxy("graph.getTestCases", { productId, entityId });
263
+ return res.docs;
264
+ }
265
+ async function getTestCasesForProduct(productId) {
266
+ const res = await proxy("graph.getTestCases", { productId });
267
+ return res.docs;
268
+ }
269
+ async function getGraphMetadata(productId) {
270
+ const res = await proxy("graph.getMeta", { productId });
271
+ if (!res.doc)
272
+ return null;
273
+ const data = res.doc;
274
+ return { ...data, last_build: new Date(data.last_build?._seconds ? data.last_build._seconds * 1e3 : data.last_build ?? Date.now()) };
275
+ }
276
+ async function updateGraphMetadata(productId, meta) {
277
+ const payload = { ...meta };
278
+ if (meta.last_build instanceof Date) {
279
+ payload.last_build = meta.last_build.toISOString();
280
+ }
281
+ await proxy("graph.updateMeta", { productId, data: payload });
282
+ }
283
+ async function recordScoreSnapshot(productId, metrics, event, _eventDetail) {
284
+ try {
285
+ const snapshot = {
286
+ engineering_readiness_pct: Math.round(metrics.engineering_readiness_pct ?? 0),
287
+ security_readiness_pct: Math.round(metrics.security_readiness_pct ?? 0),
288
+ reliability_readiness_pct: Math.round(metrics.reliability_readiness_pct ?? 0),
289
+ scalability_readiness_pct: Math.round(metrics.scalability_readiness_pct ?? 0),
290
+ nfr_coverage_pct: Math.round((metrics.nfr_coverage?.overall ?? 0) * 100),
291
+ speedup_factor: metrics.time_estimate?.speedup_factor ?? 1,
292
+ event,
293
+ ..._eventDetail ? { event_detail: _eventDetail } : {},
294
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
295
+ };
296
+ const snapshotId = `score_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
297
+ const prefix = "result.decision.productionalization_feasibility";
298
+ const jobUpdate = {
299
+ [`${prefix}.engineering_readiness_pct`]: snapshot.engineering_readiness_pct,
300
+ [`${prefix}.security_readiness_pct`]: snapshot.security_readiness_pct,
301
+ [`${prefix}.reliability_readiness_pct`]: snapshot.reliability_readiness_pct,
302
+ [`${prefix}.scalability_readiness_pct`]: snapshot.scalability_readiness_pct,
303
+ [`${prefix}.nfr_coverage_pct`]: snapshot.nfr_coverage_pct
304
+ };
305
+ await proxy("graph.recordScore", { productId, snapshotId, snapshot, jobUpdate });
306
+ } catch (err) {
307
+ console.error("[score-history] Failed to record snapshot:", err.message);
308
+ }
309
+ }
310
+ async function deleteGraphData(productId) {
311
+ await proxy("graph.deleteAll", { productId });
312
+ invalidateNodeCache(productId);
313
+ }
314
+ async function getReadinessReport(id) {
315
+ const res = await proxy("readiness.get", { id });
316
+ return res.doc;
317
+ }
318
+ async function saveReadinessReport(id, data) {
319
+ return proxy("readiness.save", { id, data });
320
+ }
321
+ async function generateReadinessReportViaProxy(input) {
322
+ const { productId, ownerUid, computeMetricsFromGraph: computeMetrics, computeGrade } = input;
323
+ const [entities, edges, constraintNodes, bindings] = await Promise.all([
324
+ getAllEntities(productId),
325
+ getAllEdges(productId),
326
+ getAllNodes(productId),
327
+ getAllBindings(productId)
328
+ ]);
329
+ if (entities.length === 0 && constraintNodes.length === 0) {
330
+ throw new Error(`No graph data for product "${productId}". Run graph_ingest_requirements or constraints_ingest first.`);
331
+ }
332
+ const metrics = computeMetrics(entities, edges, constraintNodes, bindings);
333
+ const readinessPct = metrics.engineering_readiness_pct ?? 0;
334
+ const jobData = await getPremortem(productId);
335
+ const result = jobData?.result;
336
+ const productName = result?.project?.name || jobData?.payload?.project?.name || "Untitled Product";
337
+ const verdict = result?.decision?.recommendation;
338
+ const techStack = entities.filter((e) => e.type === "component").map((e) => e.name).slice(0, 15);
339
+ const reportId = productId;
340
+ const now = /* @__PURE__ */ new Date();
341
+ const existing = await getReadinessReport(reportId);
342
+ const report = {
343
+ productId,
344
+ productName,
345
+ ownerUid,
346
+ grade: computeGrade(readinessPct),
347
+ readinessPct: Math.round(readinessPct),
348
+ securityReadinessPct: Math.round(metrics.security_readiness_pct ?? 0),
349
+ reliabilityReadinessPct: Math.round(metrics.reliability_readiness_pct ?? 0),
350
+ scalabilityReadinessPct: Math.round(metrics.scalability_readiness_pct ?? 0),
351
+ nfrCoverage: metrics.nfr_coverage,
352
+ timeEstimate: {
353
+ unassisted_hours: metrics.time_estimate.unassisted_hours,
354
+ assisted_hours: metrics.time_estimate.assisted_hours,
355
+ speedup_factor: metrics.time_estimate.speedup_factor
356
+ },
357
+ complexityFactors: {
358
+ entity_count: metrics.complexity_factors.entity_count,
359
+ nfr_count: metrics.complexity_factors.nfr_count,
360
+ critical_nfr_count: metrics.complexity_factors.critical_nfr_count,
361
+ pii_data_types: metrics.complexity_factors.pii_data_types,
362
+ conflict_count: metrics.complexity_factors.conflict_count
363
+ },
364
+ rgrCompletedPhases: metrics.rgr_completed_phases ?? [],
365
+ complianceFrameworks: metrics.compliance_frameworks ?? [],
366
+ verdict,
367
+ techStack,
368
+ public: true,
369
+ createdAt: existing?.createdAt ? new Date(existing.createdAt) : now,
370
+ updatedAt: now
371
+ };
372
+ await saveReadinessReport(reportId, report);
373
+ return { report, reportId };
374
+ }
375
+ async function listTemplates(opts) {
376
+ const res = await proxy("template.list", opts || {});
377
+ return res.docs;
378
+ }
379
+ async function getTemplate(id) {
380
+ const res = await proxy("template.get", { id });
381
+ return res.doc;
382
+ }
383
+ async function createTemplate(data, id) {
384
+ return proxy("template.create", { data, id });
385
+ }
386
+ async function updateTemplate(id, data) {
387
+ return proxy("template.update", { id, data });
388
+ }
389
+ async function saveScanReport(data, collection, id) {
390
+ return proxy("scan.save", { data, collection, id });
391
+ }
392
+ async function getScanRateLimit() {
393
+ return proxy("scan.rateCheck", {});
394
+ }
395
+ async function updateScanRateLimit(data) {
396
+ return proxy("scan.rateUpdate", { data });
397
+ }
398
+ async function listPersonas(productId) {
399
+ const res = await proxy("persona.list", { productId });
400
+ return res.docs;
401
+ }
402
+ async function getPersona(personaId) {
403
+ const res = await proxy("persona.get", { id: personaId });
404
+ return res.doc;
405
+ }
406
+
407
+ export {
408
+ listPremortems,
409
+ getPremortem,
410
+ createPremortem,
411
+ updatePremortem,
412
+ getIdeaReport,
413
+ saveChat,
414
+ getChat,
415
+ updateChat,
416
+ createExplorationSession,
417
+ getExplorationSession,
418
+ updateExplorationSession,
419
+ listExplorationSessions,
420
+ getAllEntities,
421
+ upsertEntities,
422
+ addEntity,
423
+ getEntityById,
424
+ getEntitiesWithEmbeddings,
425
+ updateEntityEmbedding,
426
+ getAllEdges,
427
+ upsertEdges,
428
+ addEdges,
429
+ getAllBindings,
430
+ getBindingsForEntity,
431
+ upsertBindings,
432
+ deleteBinding,
433
+ getAllNodes,
434
+ getAllNodesLight,
435
+ getNodesByCategories,
436
+ getNodesBySource,
437
+ upsertNodes,
438
+ addNodes,
439
+ deleteAllNodes,
440
+ hasConstraints,
441
+ searchNodesByKeywords,
442
+ getNodesWithEmbeddings,
443
+ getNodesMissingEmbeddings,
444
+ updateNodeEmbeddings,
445
+ addTestCases,
446
+ getTestCasesForEntity,
447
+ getTestCasesForProduct,
448
+ getGraphMetadata,
449
+ updateGraphMetadata,
450
+ recordScoreSnapshot,
451
+ deleteGraphData,
452
+ getReadinessReport,
453
+ saveReadinessReport,
454
+ generateReadinessReportViaProxy,
455
+ listTemplates,
456
+ getTemplate,
457
+ createTemplate,
458
+ updateTemplate,
459
+ saveScanReport,
460
+ getScanRateLimit,
461
+ updateScanRateLimit,
462
+ listPersonas,
463
+ getPersona
464
+ };