@clue-ai/cli 0.0.5 → 0.0.6

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.
@@ -1,4 +1,5 @@
1
1
  import { createHash, createHmac } from "node:crypto";
2
+ import { callJsonAiProvider, resolveAiProviderConfig } from "./ai-provider.mjs";
2
3
  import {
3
4
  validateSemanticCiRequest,
4
5
  validateSemanticSnapshotRequest,
@@ -244,9 +245,11 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
244
245
  reason: "AI reuse decision omitted this changed route.",
245
246
  };
246
247
  }
247
- const normalizedDecision = ["reuse_previous", "regenerate", "needs_review"].includes(
248
- decision.decision,
249
- )
248
+ const normalizedDecision = [
249
+ "reuse_previous",
250
+ "regenerate",
251
+ "needs_review",
252
+ ].includes(decision.decision)
250
253
  ? decision.decision
251
254
  : "needs_review";
252
255
  return {
@@ -263,109 +266,67 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
263
266
 
264
267
  const callAiReuseDecisionProvider = async ({
265
268
  request,
269
+ env,
266
270
  apiKey,
267
271
  route,
268
272
  previousRoute,
269
273
  currentHash,
270
274
  }) => {
271
- const response = await fetch(
272
- `${request.ai_provider_base_url.replace(/\/+$/, "")}/chat/completions`,
273
- {
274
- method: "POST",
275
- headers: {
276
- "content-type": "application/json",
277
- authorization: `Bearer ${apiKey}`,
278
- },
279
- body: JSON.stringify({
280
- model: request.ai_model,
281
- messages: [
282
- {
283
- role: "system",
284
- content:
285
- "You judge whether previous route semantics can be reused from privacy-safe evidence.",
286
- },
287
- {
288
- role: "user",
289
- content: buildAiReuseDecisionPrompt({
290
- route,
291
- previousRoute,
292
- currentHash,
293
- }),
294
- },
295
- ],
296
- response_format: { type: "json_object" },
297
- }),
298
- },
299
- );
300
- if (!response.ok) {
301
- return {
302
- decision: "needs_review",
303
- confidence: 0,
304
- reason: `AI reuse decision failed: provider_${response.status}.`,
305
- };
306
- }
307
- const body = await response.json();
308
- const content = body?.choices?.[0]?.message?.content;
309
- if (typeof content !== "string" || content.trim() === "") {
310
- return {
311
- decision: "needs_review",
312
- confidence: 0,
313
- reason: "AI reuse decision returned empty content.",
314
- };
315
- }
316
275
  try {
276
+ const parsed = await callJsonAiProvider({
277
+ config: resolveAiProviderConfig({ env, request, apiKey }),
278
+ system:
279
+ "You judge whether previous route semantics can be reused from privacy-safe evidence.",
280
+ user: buildAiReuseDecisionPrompt({
281
+ route,
282
+ previousRoute,
283
+ currentHash,
284
+ }),
285
+ toolName: "return_reuse_decision",
286
+ toolDescription:
287
+ "Return whether the previous route semantics can be reused.",
288
+ failureMessage: "AI reuse decision failed",
289
+ emptyMessage: "AI reuse decision returned empty content.",
290
+ });
317
291
  return normalizeReuseDecision({
318
- parsed: JSON.parse(content),
292
+ parsed,
319
293
  operationSourceKey: route.operation_source_key,
320
294
  });
321
- } catch {
295
+ } catch (error) {
296
+ const providerStatusMatch =
297
+ error instanceof Error
298
+ ? /AI reuse decision failed: (?<status>\d+)/.exec(error.message)
299
+ : null;
322
300
  return {
323
301
  decision: "needs_review",
324
302
  confidence: 0,
325
- reason: "AI reuse decision returned malformed JSON.",
303
+ reason: providerStatusMatch?.groups?.status
304
+ ? `AI reuse decision failed: provider_${providerStatusMatch.groups.status}.`
305
+ : "AI reuse decision returned malformed JSON.",
326
306
  };
327
307
  }
328
308
  };
329
309
 
330
310
  const requireAiProviderApiKey = (env) => {
331
- const apiKey = env.AI_PROVIDER_API_KEY;
311
+ const apiKey = env.CLUE_AI_PROVIDER_API_KEY;
332
312
  if (!apiKey) {
333
- throw new Error("AI_PROVIDER_API_KEY is required for semantic generation");
313
+ throw new Error(
314
+ "CLUE_AI_PROVIDER_API_KEY is required for semantic generation",
315
+ );
334
316
  }
335
317
  return apiKey;
336
318
  };
337
319
 
338
- const callAiProvider = async ({ request, apiKey, routes }) => {
339
- const response = await fetch(
340
- `${request.ai_provider_base_url.replace(/\/+$/, "")}/chat/completions`,
341
- {
342
- method: "POST",
343
- headers: {
344
- "content-type": "application/json",
345
- authorization: `Bearer ${apiKey}`,
346
- },
347
- body: JSON.stringify({
348
- model: request.ai_model,
349
- messages: [
350
- {
351
- role: "system",
352
- content: "You generate privacy-safe route semantics JSON.",
353
- },
354
- { role: "user", content: buildAiPrompt(routes) },
355
- ],
356
- response_format: { type: "json_object" },
357
- }),
358
- },
359
- );
360
- if (!response.ok) {
361
- throw new Error(`AI provider failed: ${response.status}`);
362
- }
363
- const body = await response.json();
364
- const content = body?.choices?.[0]?.message?.content;
365
- if (typeof content !== "string" || content.trim() === "") {
366
- throw new Error("AI provider returned empty semantic generation content");
367
- }
368
- const parsed = JSON.parse(content);
320
+ const callAiProvider = async ({ request, env, apiKey, routes }) => {
321
+ const parsed = await callJsonAiProvider({
322
+ config: resolveAiProviderConfig({ env, request, apiKey }),
323
+ system: "You generate privacy-safe route semantics JSON.",
324
+ user: buildAiPrompt(routes),
325
+ toolName: "return_route_semantics",
326
+ toolDescription: "Return privacy-safe route semantics JSON.",
327
+ failureMessage: "AI provider failed",
328
+ emptyMessage: "AI provider returned empty semantic generation content",
329
+ });
369
330
  return new Map(
370
331
  (parsed.routes ?? []).map((route) => [route.operation_source_key, route]),
371
332
  );
@@ -413,12 +374,13 @@ const assertSnapshotRouteCoverage = ({ routes, snapshotRoutes }) => {
413
374
  }
414
375
  };
415
376
 
416
- const generateAiRoutesPerRoute = async ({ request, apiKey, routes }) => {
377
+ const generateAiRoutesPerRoute = async ({ request, env, apiKey, routes }) => {
417
378
  const aiRoutes = new Map();
418
379
  for (const route of routes) {
419
380
  try {
420
381
  const routeAiRoutes = await callAiProvider({
421
382
  request,
383
+ env,
422
384
  apiKey,
423
385
  routes: [route],
424
386
  });
@@ -476,6 +438,7 @@ const buildAiSelectionPrompt = (selectionCandidates) =>
476
438
 
477
439
  const callAiSelectionProvider = async ({
478
440
  request,
441
+ env,
479
442
  apiKey,
480
443
  selectionCandidates,
481
444
  routeBySelectionKey,
@@ -484,40 +447,16 @@ const callAiSelectionProvider = async ({
484
447
  return new Map();
485
448
  }
486
449
  assertNoRawCodeStructure(selectionCandidates, "semantic_selection_prompt");
487
- const response = await fetch(
488
- `${request.ai_provider_base_url.replace(/\/+$/, "")}/chat/completions`,
489
- {
490
- method: "POST",
491
- headers: {
492
- "content-type": "application/json",
493
- authorization: `Bearer ${apiKey}`,
494
- },
495
- body: JSON.stringify({
496
- model: request.ai_model,
497
- messages: [
498
- {
499
- role: "system",
500
- content:
501
- "You select adopted semantic values from privacy-safe candidates.",
502
- },
503
- {
504
- role: "user",
505
- content: buildAiSelectionPrompt(selectionCandidates),
506
- },
507
- ],
508
- response_format: { type: "json_object" },
509
- }),
510
- },
511
- );
512
- if (!response.ok) {
513
- throw new Error(`AI selector failed: ${response.status}`);
514
- }
515
- const body = await response.json();
516
- const content = body?.choices?.[0]?.message?.content;
517
- if (typeof content !== "string" || content.trim() === "") {
518
- throw new Error("AI selector returned empty semantic selection content");
519
- }
520
- const parsed = JSON.parse(content);
450
+ const parsed = await callJsonAiProvider({
451
+ config: resolveAiProviderConfig({ env, request, apiKey }),
452
+ system: "You select adopted semantic values from privacy-safe candidates.",
453
+ user: buildAiSelectionPrompt(selectionCandidates),
454
+ toolName: "return_semantic_selection",
455
+ toolDescription:
456
+ "Return selected semantic values from privacy-safe candidates.",
457
+ failureMessage: "AI selector failed",
458
+ emptyMessage: "AI selector returned empty semantic selection content",
459
+ });
521
460
  return normalizeAiSelectionResponse({
522
461
  parsed,
523
462
  routeBySelectionKey,
@@ -1710,31 +1649,28 @@ const buildSnapshot = ({
1710
1649
  ? `changed_route_needs_review: ${plan.semantic_change_reason}`
1711
1650
  : (aiRoute?.confidence_reason ??
1712
1651
  "AI semantics were unavailable for this route.");
1713
- const baseRoute = !aiRoute?.semantics || plan.origin === "changed_route_needs_review"
1714
- ? fallbackSemantics(
1715
- routeWithVersion,
1716
- unavailableReason,
1717
- hashScope,
1718
- )
1719
- : {
1720
- operation_source_key: route.operation_source_key,
1721
- semantic_snapshot_version: semanticSnapshotVersion,
1722
- method: route.method,
1723
- path_template: route.path_template,
1724
- semantics: sanitizeSemantics(aiRoute.semantics, route),
1725
- layer_evidence: sanitizeLayerEvidence(
1726
- aiRoute.layer_evidence,
1727
- route,
1728
- hashScope,
1729
- ),
1730
- confidence_reason: sanitizeText(
1731
- String(
1732
- aiRoute.confidence_reason ||
1733
- "Generated by AI from privacy-safe route evidence.",
1652
+ const baseRoute =
1653
+ !aiRoute?.semantics || plan.origin === "changed_route_needs_review"
1654
+ ? fallbackSemantics(routeWithVersion, unavailableReason, hashScope)
1655
+ : {
1656
+ operation_source_key: route.operation_source_key,
1657
+ semantic_snapshot_version: semanticSnapshotVersion,
1658
+ method: route.method,
1659
+ path_template: route.path_template,
1660
+ semantics: sanitizeSemantics(aiRoute.semantics, route),
1661
+ layer_evidence: sanitizeLayerEvidence(
1662
+ aiRoute.layer_evidence,
1663
+ route,
1664
+ hashScope,
1734
1665
  ),
1735
- route,
1736
- ),
1737
- };
1666
+ confidence_reason: sanitizeText(
1667
+ String(
1668
+ aiRoute.confidence_reason ||
1669
+ "Generated by AI from privacy-safe route evidence.",
1670
+ ),
1671
+ route,
1672
+ ),
1673
+ };
1738
1674
  const assignmentCollections = buildOperationAssignmentCollections({
1739
1675
  route,
1740
1676
  aiRoute,
@@ -1794,7 +1730,11 @@ const buildSnapshot = ({
1794
1730
  );
1795
1731
 
1796
1732
  const uniqueSourceEvidenceRefs = [];
1797
- addUniqueBy(uniqueSourceEvidenceRefs, sourceEvidenceRefs, (entry) => entry.id);
1733
+ addUniqueBy(
1734
+ uniqueSourceEvidenceRefs,
1735
+ sourceEvidenceRefs,
1736
+ (entry) => entry.id,
1737
+ );
1798
1738
 
1799
1739
  const snapshot = validateSemanticSnapshotRequest({
1800
1740
  project_key: request.project_key,
@@ -1816,10 +1756,9 @@ const buildSnapshot = ({
1816
1756
  ).length,
1817
1757
  failed_routes: failedRouteCount,
1818
1758
  routes_reused: snapshotRoutes.filter((route) =>
1819
- [
1820
- "unchanged_route_reused",
1821
- "changed_route_semantic_reused",
1822
- ].includes(route.semantic_origin),
1759
+ ["unchanged_route_reused", "changed_route_semantic_reused"].includes(
1760
+ route.semantic_origin,
1761
+ ),
1823
1762
  ).length,
1824
1763
  routes_ai_generated: snapshotRoutes.filter((route) =>
1825
1764
  [
@@ -2039,6 +1978,7 @@ const buildRouteEvidencePromptEntry = ({ route, hashScope }) => ({
2039
1978
 
2040
1979
  const classifyRoutesForSnapshot = async ({
2041
1980
  request,
1981
+ env,
2042
1982
  routes,
2043
1983
  previousSnapshot,
2044
1984
  aiProviderApiKey,
@@ -2076,6 +2016,7 @@ const classifyRoutesForSnapshot = async ({
2076
2016
 
2077
2017
  const decision = await callAiReuseDecisionProvider({
2078
2018
  request,
2019
+ env,
2079
2020
  apiKey: aiProviderApiKey,
2080
2021
  route: buildRouteEvidencePromptEntry({ route, hashScope }),
2081
2022
  previousRoute,
@@ -2158,7 +2099,13 @@ const rebasePreviousRoute = ({
2158
2099
  };
2159
2100
  };
2160
2101
 
2161
- const fieldPathMatchesKeys = ({ fieldPath, routeKeys, profileIds, mappingIds, catalogKeys }) => {
2102
+ const fieldPathMatchesKeys = ({
2103
+ fieldPath,
2104
+ routeKeys,
2105
+ profileIds,
2106
+ mappingIds,
2107
+ catalogKeys,
2108
+ }) => {
2162
2109
  for (const routeKey of routeKeys) {
2163
2110
  if (fieldPath.startsWith(`routes[${routeKey}]`)) {
2164
2111
  return true;
@@ -2197,7 +2144,9 @@ const reusablePreviousContext = ({ previousSnapshot, reusedRouteKeys }) => {
2197
2144
  ...safeArray(previousSnapshot?.routes)
2198
2145
  .filter((route) => routeKeys.has(route.operation_source_key))
2199
2146
  .flatMap((route) =>
2200
- safeArray(route.operation_effects).map((effect) => effect.target_object_key),
2147
+ safeArray(route.operation_effects).map(
2148
+ (effect) => effect.target_object_key,
2149
+ ),
2201
2150
  ),
2202
2151
  ]);
2203
2152
  const catalogs = safeArray(previousSnapshot?.target_object_catalog).filter(
@@ -2274,9 +2223,9 @@ const reusablePreviousContext = ({ previousSnapshot, reusedRouteKeys }) => {
2274
2223
  sourceEvidenceIds.add(ref);
2275
2224
  }
2276
2225
  }
2277
- const sourceEvidenceRefs = safeArray(previousSnapshot?.source_evidence_refs).filter(
2278
- (entry) => sourceEvidenceIds.has(entry.id),
2279
- );
2226
+ const sourceEvidenceRefs = safeArray(
2227
+ previousSnapshot?.source_evidence_refs,
2228
+ ).filter((entry) => sourceEvidenceIds.has(entry.id));
2280
2229
  return {
2281
2230
  profiles,
2282
2231
  mappings,
@@ -2396,7 +2345,9 @@ const semanticSnapshotUrl = (baseUrl) => {
2396
2345
  };
2397
2346
 
2398
2347
  const semanticSnapshotLatestUrl = (request) => {
2399
- const url = new URL(`${semanticSnapshotUrl(request.clue_api_base_url)}/latest`);
2348
+ const url = new URL(
2349
+ `${semanticSnapshotUrl(request.clue_api_base_url)}/latest`,
2350
+ );
2400
2351
  url.searchParams.set("project_key", request.project_key);
2401
2352
  url.searchParams.set("environment", request.environment);
2402
2353
  url.searchParams.set("repository_id", request.repository.repository_id);
@@ -2501,22 +2452,29 @@ export const runSemanticCi = async ({
2501
2452
  const previousRoutes = previousRouteMap(previousSnapshot);
2502
2453
  const routeNeedsAi = routes.some((route) => {
2503
2454
  const previousRoute = previousRoutes.get(route.operation_source_key);
2504
- return !previousRoute || previousRoute.route_input_hash !== routeInputHash(route);
2455
+ return (
2456
+ !previousRoute || previousRoute.route_input_hash !== routeInputHash(route)
2457
+ );
2505
2458
  });
2506
2459
  const aiProviderApiKey = routeNeedsAi ? requireAiProviderApiKey(env) : null;
2507
- const { plans: routePlans, routesRequiringGeneration, deletedRouteCount } =
2508
- await classifyRoutesForSnapshot({
2509
- request,
2510
- routes,
2511
- previousSnapshot,
2512
- aiProviderApiKey,
2513
- hashScope,
2514
- });
2460
+ const {
2461
+ plans: routePlans,
2462
+ routesRequiringGeneration,
2463
+ deletedRouteCount,
2464
+ } = await classifyRoutesForSnapshot({
2465
+ request,
2466
+ env,
2467
+ routes,
2468
+ previousSnapshot,
2469
+ aiProviderApiKey,
2470
+ hashScope,
2471
+ });
2515
2472
  const promptRoutes = routesRequiringGeneration.map((route) =>
2516
2473
  buildRouteEvidencePromptEntry({ route, hashScope }),
2517
2474
  );
2518
2475
  const aiRoutes = await generateAiRoutesPerRoute({
2519
2476
  request,
2477
+ env,
2520
2478
  apiKey: aiProviderApiKey,
2521
2479
  routes: promptRoutes,
2522
2480
  });
@@ -2530,6 +2488,7 @@ export const runSemanticCi = async ({
2530
2488
  ? new Map()
2531
2489
  : await callAiSelectionProvider({
2532
2490
  request,
2491
+ env,
2533
2492
  apiKey: aiProviderApiKey,
2534
2493
  selectionCandidates,
2535
2494
  routeBySelectionKey,