@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.
- package/README.md +17 -3
- package/bin/clue-cli.mjs +805 -762
- package/commands/claude-code/clue-init.md +7 -1
- package/commands/codex/clue-init.md +7 -1
- package/package.json +1 -1
- package/src/ai-provider.mjs +146 -0
- package/src/command-spec.mjs +7 -7
- package/src/contracts.mjs +49 -15
- package/src/init-tool.mjs +158 -124
- package/src/lifecycle-init.mjs +180 -205
- package/src/public-schema.cjs +1 -1
- package/src/semantic-ci.mjs +122 -163
- package/src/setup-check.mjs +373 -372
- package/src/setup-prepare.mjs +266 -147
- package/src/setup-tool.mjs +231 -229
package/src/semantic-ci.mjs
CHANGED
|
@@ -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 = [
|
|
248
|
-
|
|
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
|
|
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:
|
|
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.
|
|
311
|
+
const apiKey = env.CLUE_AI_PROVIDER_API_KEY;
|
|
332
312
|
if (!apiKey) {
|
|
333
|
-
throw new Error(
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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 =
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1821
|
-
|
|
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 = ({
|
|
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(
|
|
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(
|
|
2278
|
-
|
|
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(
|
|
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
|
|
2455
|
+
return (
|
|
2456
|
+
!previousRoute || previousRoute.route_input_hash !== routeInputHash(route)
|
|
2457
|
+
);
|
|
2505
2458
|
});
|
|
2506
2459
|
const aiProviderApiKey = routeNeedsAi ? requireAiProviderApiKey(env) : null;
|
|
2507
|
-
const {
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
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,
|