@clue-ai/cli 0.0.14 → 0.0.15
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 +11 -6
- package/bin/clue-cli.mjs +32 -11
- package/package.json +1 -1
- package/src/lifecycle-init.mjs +3 -0
- package/src/public-schema.cjs +15 -0
- package/src/semantic-ci.mjs +128 -17
- package/src/setup-check.mjs +83 -2
- package/src/setup-documents.mjs +99 -0
- package/src/setup-help.mjs +4 -2
- package/src/setup-prepare.mjs +23 -0
- package/src/setup-tool.mjs +65 -3
package/README.md
CHANGED
|
@@ -62,16 +62,21 @@ leaks, and SDK lifecycle presence when requested. With
|
|
|
62
62
|
installation, SDK imports in the target environments, app startup, and event
|
|
63
63
|
delivery remain required before setup can be called complete.
|
|
64
64
|
|
|
65
|
-
`npx -y @clue-ai/cli setup-watch` polls the Clue API setup-check endpoint
|
|
66
|
-
the
|
|
67
|
-
frontend URL. Use `--watch-targets` to list every
|
|
68
|
-
service key and the lifecycle checks expected for
|
|
65
|
+
`npx -y @clue-ai/cli setup-watch` polls the Clue API setup-check endpoint in
|
|
66
|
+
remote mode while you operate the service. `--clue-api-base-url` is the Clue API
|
|
67
|
+
URL, not the customer frontend URL. Use `--watch-targets` to list every
|
|
68
|
+
frontend/backend producer by service key and the lifecycle checks expected for
|
|
69
|
+
that service, for example
|
|
69
70
|
`frontend:web[init,identify,set-account,event-sent]=<frontend-url>,backend:api[init,identify,set-account,logout,event-sent]=<backend-url>`.
|
|
70
71
|
Do not assume localhost ports; use the actual URLs printed by the repository's
|
|
71
72
|
dev scripts or configured in its local env.
|
|
72
73
|
In `--local` mode, the watcher stays open until the expected lifecycle checks
|
|
73
|
-
pass or you stop it with Ctrl+C.
|
|
74
|
-
|
|
74
|
+
pass or you stop it with Ctrl+C. The frontend/backend endpoint URLs printed by
|
|
75
|
+
the command are local Clue receiver endpoints; configure the customer's Clue SDK
|
|
76
|
+
ingest endpoint to those receiver URLs while testing. Local mode does not ask
|
|
77
|
+
for or health-check customer service URLs. It prints a per-service Clue
|
|
78
|
+
lifecycle checklist and only prints a new snapshot when the observed state
|
|
79
|
+
changes.
|
|
75
80
|
|
|
76
81
|
`npx -y @clue-ai/cli setup` reads Clue values from the setup screen flags, detects local
|
|
77
82
|
services, writes `.clue/setup-manifest.json`, and prints service-specific env
|
package/bin/clue-cli.mjs
CHANGED
|
@@ -304,7 +304,18 @@ const maybeProtectEnvironmentGuide = async ({
|
|
|
304
304
|
};
|
|
305
305
|
};
|
|
306
306
|
|
|
307
|
-
const
|
|
307
|
+
const clearWatchTargetUrls = (watchTargets) =>
|
|
308
|
+
watchTargets.map((target) => ({
|
|
309
|
+
...target,
|
|
310
|
+
url: null,
|
|
311
|
+
urlEnvName: null,
|
|
312
|
+
localUrlCandidates: [],
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
const confirmTargetUrls = async ({ flags, localMode, watchTargets, env }) => {
|
|
316
|
+
if (localMode) {
|
|
317
|
+
return clearWatchTargetUrls(watchTargets);
|
|
318
|
+
}
|
|
308
319
|
const initialTargets = watchTargets.map((target) => ({
|
|
309
320
|
...target,
|
|
310
321
|
url: resolveTargetUrlFromEnv({ target, env }),
|
|
@@ -370,14 +381,20 @@ const checkTargetUrl = async (url) => {
|
|
|
370
381
|
}
|
|
371
382
|
};
|
|
372
383
|
|
|
373
|
-
const evaluateWatchTargets = async ({
|
|
384
|
+
const evaluateWatchTargets = async ({
|
|
385
|
+
latest,
|
|
386
|
+
requireTargetUrl = true,
|
|
387
|
+
watchTargets,
|
|
388
|
+
}) => {
|
|
374
389
|
const producers = Array.isArray(latest?.producers) ? latest.producers : [];
|
|
375
390
|
return Promise.all(
|
|
376
391
|
watchTargets.map(async (target) => {
|
|
377
392
|
const producer = producers.find(
|
|
378
393
|
(entry) => entry.id === target.producerId,
|
|
379
394
|
);
|
|
380
|
-
const urlHealth =
|
|
395
|
+
const urlHealth = requireTargetUrl
|
|
396
|
+
? await checkTargetUrl(target.url)
|
|
397
|
+
: { checked: false, reachable: true, status: null };
|
|
381
398
|
const lifecycleStatus = {
|
|
382
399
|
init: Boolean(producer?.clueInit ?? producer?.sdkInitialized),
|
|
383
400
|
identify: Boolean(producer?.clueIdentify),
|
|
@@ -411,7 +428,7 @@ const evaluateWatchTargets = async ({ latest, watchTargets }) => {
|
|
|
411
428
|
urlChecked: urlHealth.checked,
|
|
412
429
|
urlReachable: urlHealth.reachable,
|
|
413
430
|
urlStatus: urlHealth.status,
|
|
414
|
-
passed: producerPassed && urlHealth.reachable,
|
|
431
|
+
passed: producerPassed && (!requireTargetUrl || urlHealth.reachable),
|
|
415
432
|
};
|
|
416
433
|
}),
|
|
417
434
|
);
|
|
@@ -686,6 +703,7 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
686
703
|
const explicitWatchTargets = parseWatchTargets(flags.get("watch-targets"));
|
|
687
704
|
const watchTargets = await confirmTargetUrls({
|
|
688
705
|
flags,
|
|
706
|
+
localMode,
|
|
689
707
|
watchTargets:
|
|
690
708
|
explicitWatchTargets.length > 0
|
|
691
709
|
? explicitWatchTargets
|
|
@@ -739,6 +757,7 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
739
757
|
});
|
|
740
758
|
const targetChecks = await evaluateWatchTargets({
|
|
741
759
|
latest,
|
|
760
|
+
requireTargetUrl: false,
|
|
742
761
|
watchTargets,
|
|
743
762
|
});
|
|
744
763
|
const targetChecksPassed =
|
|
@@ -829,7 +848,7 @@ const usage = () =>
|
|
|
829
848
|
"",
|
|
830
849
|
"Usage:",
|
|
831
850
|
" /clue-init",
|
|
832
|
-
` ${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment dev")}`,
|
|
851
|
+
` ${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment dev --documents-url <url>")}`,
|
|
833
852
|
` ${clueCliCommand("setup-detect --repo .")}`,
|
|
834
853
|
` ${clueCliCommand("semantic-inventory --framework fastapi --backend-root-path backend --repo .")}`,
|
|
835
854
|
` ${clueCliCommand("semantic-agent-skills --output .clue/semantic-agent-skills.json")}`,
|
|
@@ -903,6 +922,7 @@ const main = async () => {
|
|
|
903
922
|
if (command === "setup") {
|
|
904
923
|
const report = await installSetupSkills({
|
|
905
924
|
repoRoot,
|
|
925
|
+
documentsUrl: flags.get("documents-url"),
|
|
906
926
|
target:
|
|
907
927
|
typeof flags.get("target") === "string"
|
|
908
928
|
? flags.get("target")
|
|
@@ -924,12 +944,13 @@ const main = async () => {
|
|
|
924
944
|
repoRoot,
|
|
925
945
|
target: report.target,
|
|
926
946
|
skillRoot: report.skill_root,
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
947
|
+
setupContext: {
|
|
948
|
+
clueApiKey: flags.get("clue-api-key"),
|
|
949
|
+
clueApiBaseUrl: flags.get("clue-api-base-url"),
|
|
950
|
+
documentsUrl: flags.get("documents-url"),
|
|
951
|
+
projectKey: flags.get("project-key"),
|
|
952
|
+
environment: flags.get("environment"),
|
|
953
|
+
},
|
|
933
954
|
});
|
|
934
955
|
const environmentFileProtection =
|
|
935
956
|
preEnvironmentFileProtection?.env_file_path ===
|
package/package.json
CHANGED
package/src/lifecycle-init.mjs
CHANGED
|
@@ -575,7 +575,10 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
575
575
|
"Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
576
576
|
"When browser SDK ingest is configured, implement a backend-owned browser token endpoint that reads server-side CLUE_API_KEY and requests POST /api/v1/ingest/browser-tokens from Clue.",
|
|
577
577
|
"Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
|
|
578
|
+
"The local backend token endpoint is part of the customer app, not the Clue API. It may use the customer's route convention, but it must call Clue server-side at /api/v1/ingest/browser-tokens.",
|
|
579
|
+
"The frontend browserTokenProvider must send the same service key used by ClueInit to the customer backend token endpoint. For Next.js this value comes from NEXT_PUBLIC_CLUE_SERVICE_KEY.",
|
|
578
580
|
"The browser token request must include project key, environment, service key, and the current browser origin; the backend must attach x-clue-api-key server-side when calling Clue.",
|
|
581
|
+
"For browser token proxy code, the service key sent to Clue must be the frontend ClueInit serviceKey from the browser request, not the backend service's CLUE_SERVICE_KEY.",
|
|
579
582
|
"If a backend-owned browser token endpoint is implemented, read CLUE_API_BASE_URL from the backend env block and normalize it so values with or without a trailing /api/v1 do not produce duplicate paths.",
|
|
580
583
|
"Do not add @clue-ai/browser-sdk or backend SDK dependencies with * or latest; use a concrete published version or package-manager-resolved semver range and update the repository lockfile when one exists.",
|
|
581
584
|
"Prefer minimal edits that engineers can review in one PR.",
|
package/src/public-schema.cjs
CHANGED
|
@@ -845,9 +845,23 @@ const semanticSnapshotRequestSchema = zod_1.z
|
|
|
845
845
|
], "semantic_meaning_candidates[].selection_ai_inference_evidence_ref");
|
|
846
846
|
});
|
|
847
847
|
});
|
|
848
|
+
const semanticSnapshotResponseSchema = zod_1.z.object({
|
|
849
|
+
accepted: zod_1.z.literal(true),
|
|
850
|
+
duplicate: zod_1.z.boolean(),
|
|
851
|
+
semantic_snapshot_id: nonEmptyStringSchema,
|
|
852
|
+
route_count: zod_1.z.number().int().nonnegative(),
|
|
853
|
+
diff_summary: zod_1.z.object({
|
|
854
|
+
added_routes: zod_1.z.number().int().nonnegative(),
|
|
855
|
+
removed_routes: zod_1.z.number().int().nonnegative(),
|
|
856
|
+
changed_routes: zod_1.z.number().int().nonnegative(),
|
|
857
|
+
unchanged_routes: zod_1.z.number().int().nonnegative(),
|
|
858
|
+
low_confidence_routes: zod_1.z.number().int().nonnegative(),
|
|
859
|
+
}),
|
|
860
|
+
});
|
|
848
861
|
|
|
849
862
|
return {
|
|
850
863
|
semanticSnapshotRequestSchema,
|
|
864
|
+
semanticSnapshotResponseSchema,
|
|
851
865
|
};
|
|
852
866
|
})();
|
|
853
867
|
|
|
@@ -855,6 +869,7 @@ const schemaPackage = {
|
|
|
855
869
|
clueInitToolRequestSchema: toolingSchemas.clueInitToolRequestSchema,
|
|
856
870
|
clueInitToolReportSchema: toolingSchemas.clueInitToolReportSchema,
|
|
857
871
|
semanticSnapshotRequestSchema: semanticSchemas.semanticSnapshotRequestSchema,
|
|
872
|
+
semanticSnapshotResponseSchema: semanticSchemas.semanticSnapshotResponseSchema,
|
|
858
873
|
};
|
|
859
874
|
|
|
860
875
|
module.exports = schemaPackage;
|
package/src/semantic-ci.mjs
CHANGED
|
@@ -706,7 +706,7 @@ const FORBIDDEN_TARGET_TEXT_PATTERNS = [
|
|
|
706
706
|
/\b[A-Za-z0-9_./-]+\.py\b/i,
|
|
707
707
|
/\b(from\s+[A-Za-z_][A-Za-z0-9_.]*\s+import|import\s+[A-Za-z_][A-Za-z0-9_.]*)\b/i,
|
|
708
708
|
/\b(class|def)\s+[A-Za-z_][A-Za-z0-9_]*/,
|
|
709
|
-
/\b(select|insert|update|delete
|
|
709
|
+
/\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b/i,
|
|
710
710
|
/\b(system|user|assistant)\s*:\s*.+\b(prompt|completion|transcript)\b/i,
|
|
711
711
|
/\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\b(?:\s*=\s*["']?[^"'\s,;}]+)?/,
|
|
712
712
|
/\b[A-Z][A-Z0-9_]{2,}\s*=\s*["']?[^"'\s,;}]+/,
|
|
@@ -1358,6 +1358,15 @@ const unresolvedEffect = ({
|
|
|
1358
1358
|
.map((entry) => sanitizeText(entry.trim(), route)),
|
|
1359
1359
|
});
|
|
1360
1360
|
|
|
1361
|
+
const findCatalogEntryByTargetObjectKey = (catalogByGroup, targetObjectKey) => {
|
|
1362
|
+
for (const [mapKey, entry] of catalogByGroup.entries()) {
|
|
1363
|
+
if (entry?.target_object_key === targetObjectKey) {
|
|
1364
|
+
return { mapKey, entry };
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return null;
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1361
1370
|
const buildOperationAssignmentCollections = ({
|
|
1362
1371
|
route,
|
|
1363
1372
|
aiRoute,
|
|
@@ -1535,7 +1544,15 @@ const buildOperationAssignmentCollections = ({
|
|
|
1535
1544
|
}
|
|
1536
1545
|
|
|
1537
1546
|
const groupKey = `${concept.toLowerCase()}::${businessRole.toLowerCase()}`;
|
|
1538
|
-
const
|
|
1547
|
+
const existingCatalogByTarget = findCatalogEntryByTargetObjectKey(
|
|
1548
|
+
catalogByGroup,
|
|
1549
|
+
targetKeyCandidate,
|
|
1550
|
+
);
|
|
1551
|
+
const existingCatalog =
|
|
1552
|
+
catalogByGroup.get(groupKey) ?? existingCatalogByTarget?.entry;
|
|
1553
|
+
const catalogMapKey = catalogByGroup.has(groupKey)
|
|
1554
|
+
? groupKey
|
|
1555
|
+
: (existingCatalogByTarget?.mapKey ?? groupKey);
|
|
1539
1556
|
const targetObjectKey =
|
|
1540
1557
|
existingCatalog?.target_object_key ?? targetKeyCandidate;
|
|
1541
1558
|
const operationEffectKey = `${targetObjectKey}.${effectAction}`;
|
|
@@ -1706,7 +1723,7 @@ const buildOperationAssignmentCollections = ({
|
|
|
1706
1723
|
catalogEntry.grouping_evidence_refs.push(ref);
|
|
1707
1724
|
}
|
|
1708
1725
|
}
|
|
1709
|
-
catalogByGroup.set(
|
|
1726
|
+
catalogByGroup.set(catalogMapKey, catalogEntry);
|
|
1710
1727
|
|
|
1711
1728
|
mappings.push({
|
|
1712
1729
|
id: mappingId,
|
|
@@ -1991,7 +2008,7 @@ const sanitizeText = (value, route) => {
|
|
|
1991
2008
|
}
|
|
1992
2009
|
result = result
|
|
1993
2010
|
.replace(
|
|
1994
|
-
/\b(select|insert|update|delete
|
|
2011
|
+
/\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b[^"',;}]+/gi,
|
|
1995
2012
|
"[sql]",
|
|
1996
2013
|
)
|
|
1997
2014
|
.replace(
|
|
@@ -2036,25 +2053,80 @@ const sanitizeSemantics = (value, route) => {
|
|
|
2036
2053
|
record && typeof record === "object" && !Array.isArray(record)
|
|
2037
2054
|
? record
|
|
2038
2055
|
: {};
|
|
2056
|
+
const routeConfidence = Number.isFinite(Number(safeRecord.route_confidence))
|
|
2057
|
+
? Math.max(0, Math.min(1, Number(safeRecord.route_confidence)))
|
|
2058
|
+
: 0;
|
|
2039
2059
|
return {
|
|
2040
2060
|
route_summary:
|
|
2041
2061
|
typeof safeRecord.route_summary === "string" &&
|
|
2042
2062
|
safeRecord.route_summary.trim()
|
|
2043
2063
|
? safeRecord.route_summary
|
|
2044
2064
|
: "unknown",
|
|
2045
|
-
action_candidates:
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2065
|
+
action_candidates: sanitizeSemanticCandidates(
|
|
2066
|
+
safeRecord.action_candidates,
|
|
2067
|
+
route,
|
|
2068
|
+
routeConfidence,
|
|
2069
|
+
),
|
|
2070
|
+
outcome_candidates: sanitizeSemanticCandidates(
|
|
2071
|
+
safeRecord.outcome_candidates,
|
|
2072
|
+
route,
|
|
2073
|
+
routeConfidence,
|
|
2074
|
+
),
|
|
2051
2075
|
value_event_candidates: [],
|
|
2052
|
-
route_confidence:
|
|
2053
|
-
? Math.max(0, Math.min(1, Number(safeRecord.route_confidence)))
|
|
2054
|
-
: 0,
|
|
2076
|
+
route_confidence: routeConfidence,
|
|
2055
2077
|
};
|
|
2056
2078
|
};
|
|
2057
2079
|
|
|
2080
|
+
const sanitizeSemanticCandidates = (value, route, routeConfidence) => {
|
|
2081
|
+
if (!Array.isArray(value)) {
|
|
2082
|
+
return [];
|
|
2083
|
+
}
|
|
2084
|
+
return value
|
|
2085
|
+
.map((entry) => {
|
|
2086
|
+
if (typeof entry === "string") {
|
|
2087
|
+
const label = sanitizeText(entry, route).trim();
|
|
2088
|
+
return label
|
|
2089
|
+
? {
|
|
2090
|
+
label,
|
|
2091
|
+
confidence: routeConfidence > 0 ? routeConfidence : 0.5,
|
|
2092
|
+
}
|
|
2093
|
+
: null;
|
|
2094
|
+
}
|
|
2095
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
const label =
|
|
2099
|
+
typeof entry.label === "string" && entry.label.trim()
|
|
2100
|
+
? sanitizeText(entry.label, route).trim()
|
|
2101
|
+
: "";
|
|
2102
|
+
if (!label) {
|
|
2103
|
+
return null;
|
|
2104
|
+
}
|
|
2105
|
+
const candidate = {
|
|
2106
|
+
label,
|
|
2107
|
+
confidence: Number.isFinite(Number(entry.confidence))
|
|
2108
|
+
? Math.max(0, Math.min(1, Number(entry.confidence)))
|
|
2109
|
+
: routeConfidence > 0
|
|
2110
|
+
? routeConfidence
|
|
2111
|
+
: 0.5,
|
|
2112
|
+
};
|
|
2113
|
+
for (const key of [
|
|
2114
|
+
"subject_type",
|
|
2115
|
+
"action_category",
|
|
2116
|
+
"outcome_kind",
|
|
2117
|
+
"value_kind",
|
|
2118
|
+
]) {
|
|
2119
|
+
if (typeof entry[key] === "string" && entry[key].trim()) {
|
|
2120
|
+
candidate[key] = sanitizeText(entry[key], route).trim();
|
|
2121
|
+
} else if (entry[key] === null) {
|
|
2122
|
+
candidate[key] = null;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return candidate;
|
|
2126
|
+
})
|
|
2127
|
+
.filter(Boolean);
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2058
2130
|
const sanitizeForClueStorage = (value, route) => {
|
|
2059
2131
|
if (typeof value === "string") {
|
|
2060
2132
|
return sanitizeText(value, route);
|
|
@@ -2152,6 +2224,10 @@ const previousRouteMap = (previousSnapshot) =>
|
|
|
2152
2224
|
]),
|
|
2153
2225
|
);
|
|
2154
2226
|
|
|
2227
|
+
const hasReusablePreviousRouteSemantics = (route) =>
|
|
2228
|
+
Number(route?.semantics?.route_confidence ?? 0) > 0 &&
|
|
2229
|
+
route?.semantics?.route_summary !== "unknown";
|
|
2230
|
+
|
|
2155
2231
|
const buildRouteEvidencePromptEntry = ({ route, hashScope }) => ({
|
|
2156
2232
|
operation_source_key: route.operation_source_key,
|
|
2157
2233
|
method: route.method,
|
|
@@ -2192,7 +2268,10 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2192
2268
|
continue;
|
|
2193
2269
|
}
|
|
2194
2270
|
|
|
2195
|
-
if (
|
|
2271
|
+
if (
|
|
2272
|
+
previousRoute.route_input_hash === currentHash &&
|
|
2273
|
+
hasReusablePreviousRouteSemantics(previousRoute)
|
|
2274
|
+
) {
|
|
2196
2275
|
plans.set(route.operation_source_key, {
|
|
2197
2276
|
origin: "unchanged_route_reused",
|
|
2198
2277
|
route_input_hash: currentHash,
|
|
@@ -2206,6 +2285,21 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2206
2285
|
continue;
|
|
2207
2286
|
}
|
|
2208
2287
|
|
|
2288
|
+
if (previousRoute.route_input_hash === currentHash) {
|
|
2289
|
+
plans.set(route.operation_source_key, {
|
|
2290
|
+
origin: "changed_route_semantic_regenerated",
|
|
2291
|
+
route_input_hash: currentHash,
|
|
2292
|
+
previous_route: previousRoute,
|
|
2293
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2294
|
+
previous_route_semantic_hash:
|
|
2295
|
+
previousRoute.route_semantic_hash ?? routeSemanticHash(previousRoute),
|
|
2296
|
+
semantic_change_reason:
|
|
2297
|
+
"Previous route semantics had no reusable confidence, so the route was regenerated.",
|
|
2298
|
+
});
|
|
2299
|
+
routesRequiringGeneration.push(route);
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2209
2303
|
const decision = await callAiReuseDecisionProvider({
|
|
2210
2304
|
request,
|
|
2211
2305
|
env,
|
|
@@ -2483,7 +2577,8 @@ const FORBIDDEN_CODE_STRUCTURE_PATTERNS = [
|
|
|
2483
2577
|
},
|
|
2484
2578
|
{
|
|
2485
2579
|
label: "raw sql",
|
|
2486
|
-
pattern:
|
|
2580
|
+
pattern:
|
|
2581
|
+
/\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b/i,
|
|
2487
2582
|
},
|
|
2488
2583
|
{
|
|
2489
2584
|
label: "raw prompt",
|
|
@@ -2627,6 +2722,9 @@ const fetchLatestSnapshot = async ({ request, env }) => {
|
|
|
2627
2722
|
);
|
|
2628
2723
|
}
|
|
2629
2724
|
const body = await response.json();
|
|
2725
|
+
if (body?.found === false && body?.snapshot === undefined) {
|
|
2726
|
+
return null;
|
|
2727
|
+
}
|
|
2630
2728
|
const candidate = body?.snapshot ?? body;
|
|
2631
2729
|
if (!candidate || !Array.isArray(candidate.routes)) {
|
|
2632
2730
|
if (allowFullRegeneration) {
|
|
@@ -2664,7 +2762,16 @@ const sendSnapshot = async ({ request, env, snapshot }) => {
|
|
|
2664
2762
|
body: JSON.stringify(validatedSnapshot),
|
|
2665
2763
|
});
|
|
2666
2764
|
if (!response.ok) {
|
|
2667
|
-
|
|
2765
|
+
const responseBody =
|
|
2766
|
+
typeof response.text === "function"
|
|
2767
|
+
? await response.text().catch(() => "")
|
|
2768
|
+
: "";
|
|
2769
|
+
const responseDetail = responseBody.trim()
|
|
2770
|
+
? `: ${responseBody.trim().slice(0, 1000)}`
|
|
2771
|
+
: "";
|
|
2772
|
+
throw new Error(
|
|
2773
|
+
`Clue semantic snapshot upload failed: ${response.status}${responseDetail}`,
|
|
2774
|
+
);
|
|
2668
2775
|
}
|
|
2669
2776
|
const body = await response.json();
|
|
2670
2777
|
try {
|
|
@@ -2782,7 +2889,11 @@ export const runSemanticCi = async ({
|
|
|
2782
2889
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
2783
2890
|
const routeNeedsAi = routes.some((route) => {
|
|
2784
2891
|
const previousRoute = previousRoutes.get(route.operation_source_key);
|
|
2785
|
-
return
|
|
2892
|
+
return (
|
|
2893
|
+
!previousRoute ||
|
|
2894
|
+
previousRoute.route_input_hash !== routeInputHash(route) ||
|
|
2895
|
+
!hasReusablePreviousRouteSemantics(previousRoute)
|
|
2896
|
+
);
|
|
2786
2897
|
});
|
|
2787
2898
|
if (routeNeedsAi && !env.CLUE_AI_PROVIDER_API_KEY) {
|
|
2788
2899
|
throw new Error("CLUE_AI_PROVIDER_API_KEY is required for semantic generation");
|
package/src/setup-check.mjs
CHANGED
|
@@ -27,7 +27,7 @@ const SETUP_SKILLS = [
|
|
|
27
27
|
"clue-local-verification",
|
|
28
28
|
"clue-setup-report",
|
|
29
29
|
];
|
|
30
|
-
const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.
|
|
30
|
+
const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v4";
|
|
31
31
|
const REQUIRED_SETUP_SKILL_PHRASES = {
|
|
32
32
|
"clue-sdk-instrumentation": [
|
|
33
33
|
"Do not create no-op wrappers",
|
|
@@ -40,6 +40,8 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
|
|
|
40
40
|
"CLUE_API_BASE_URL` is not part of backend SDK initialization",
|
|
41
41
|
"Do not add `@clue-ai/browser-sdk` or backend SDK dependencies with `*` or `latest`",
|
|
42
42
|
"Whitespace-only changes are allowed only on lines directly changed for Clue SDK wiring",
|
|
43
|
+
"The local backend token endpoint is part of the customer app, not the Clue API",
|
|
44
|
+
"The frontend `browserTokenProvider` must send the same service key used by `ClueInit`",
|
|
43
45
|
"For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
|
|
44
46
|
],
|
|
45
47
|
"clue-setup-audit": [
|
|
@@ -335,7 +337,9 @@ const readAllowedSourceText = async ({
|
|
|
335
337
|
|
|
336
338
|
const readDependencyText = async ({ repoRoot, roots }) => {
|
|
337
339
|
const expandedRoots = roots.flatMap((root) =>
|
|
338
|
-
root.endsWith("/src")
|
|
340
|
+
root.endsWith("/src") || root.endsWith("/app")
|
|
341
|
+
? [root, dirname(root)]
|
|
342
|
+
: [root],
|
|
339
343
|
);
|
|
340
344
|
const candidatePaths = [
|
|
341
345
|
...DEPENDENCY_FILE_CANDIDATES,
|
|
@@ -764,6 +768,65 @@ const findWildcardFrontendSdkDependencyFiles = (dependencySources) =>
|
|
|
764
768
|
})
|
|
765
769
|
.map((source) => source.file_path);
|
|
766
770
|
|
|
771
|
+
const sourceLooksLikeBrowserTokenProvider = (source) => {
|
|
772
|
+
const text = stripSourceNoise(source.text);
|
|
773
|
+
return (
|
|
774
|
+
sourceImportsFrontendSdk(source) &&
|
|
775
|
+
/browserTokenProvider|browser[-_]?tokens/i.test(text)
|
|
776
|
+
);
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const findBrowserTokenProviderMissingServiceKeyFiles = (frontendSources) =>
|
|
780
|
+
frontendSources
|
|
781
|
+
.filter(sourceLooksLikeBrowserTokenProvider)
|
|
782
|
+
.filter((source) => {
|
|
783
|
+
const text = stripSourceNoise(source.text);
|
|
784
|
+
if (/body\s*:\s*JSON\.stringify\s*\(\s*{\s*}\s*\)/.test(text)) {
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
return !/body\s*:\s*JSON\.stringify\s*\(\s*{[^}]*\b(?:service_key|serviceKey)\b/.test(
|
|
788
|
+
text,
|
|
789
|
+
);
|
|
790
|
+
})
|
|
791
|
+
.map((source) => source.file_path);
|
|
792
|
+
|
|
793
|
+
const findNextBrowserTokenProviderMissingPublicServiceKeyFiles = ({
|
|
794
|
+
dependencySources,
|
|
795
|
+
frontendSources,
|
|
796
|
+
}) => {
|
|
797
|
+
const roots = nextPackageRoots(dependencySources);
|
|
798
|
+
if (roots.length === 0) return [];
|
|
799
|
+
return frontendSources
|
|
800
|
+
.filter((source) => sourceIsUnderAnyRoot(source, roots))
|
|
801
|
+
.filter(sourceLooksLikeBrowserTokenProvider)
|
|
802
|
+
.filter(
|
|
803
|
+
(source) =>
|
|
804
|
+
!/\bprocess\.env\.NEXT_PUBLIC_CLUE_SERVICE_KEY\b/.test(source.text),
|
|
805
|
+
)
|
|
806
|
+
.map((source) => source.file_path);
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const sourceLooksLikeBrowserTokenProxy = (source) => {
|
|
810
|
+
const text = stripSourceNoise(source.text);
|
|
811
|
+
return (
|
|
812
|
+
/browser[-_]?tokens|ingest\/browser-tokens/i.test(text) &&
|
|
813
|
+
/CLUE_API_KEY|x-clue-api-key/i.test(text)
|
|
814
|
+
);
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const findBrowserTokenProxyUsingBackendServiceKeyFiles = (backendSources) =>
|
|
818
|
+
backendSources
|
|
819
|
+
.filter(sourceLooksLikeBrowserTokenProxy)
|
|
820
|
+
.filter((source) => {
|
|
821
|
+
const text = stripSourceNoise(source.text);
|
|
822
|
+
return [
|
|
823
|
+
/["']service_key["']\s*:\s*[^,\n}]*CLUE_SERVICE_KEY\b/i,
|
|
824
|
+
/\bserviceKey\s*:\s*[^,\n}]*CLUE_SERVICE_KEY\b/i,
|
|
825
|
+
/\b(?:browser_)?service_key\s*=\s*[^,\n]*CLUE_SERVICE_KEY\b/i,
|
|
826
|
+
].some((pattern) => pattern.test(text));
|
|
827
|
+
})
|
|
828
|
+
.map((source) => source.file_path);
|
|
829
|
+
|
|
767
830
|
const backendSdkSpec = (framework) =>
|
|
768
831
|
BACKEND_SDK_BY_FRAMEWORK[String(framework ?? "").toLowerCase()] ?? {
|
|
769
832
|
packages: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
|
|
@@ -882,6 +945,15 @@ const checkSdkLifecycle = ({
|
|
|
882
945
|
});
|
|
883
946
|
const wildcardFrontendSdkDependencyFiles =
|
|
884
947
|
findWildcardFrontendSdkDependencyFiles(dependencySources);
|
|
948
|
+
const browserTokenProviderMissingServiceKeyFiles =
|
|
949
|
+
findBrowserTokenProviderMissingServiceKeyFiles(frontendSources);
|
|
950
|
+
const nextBrowserTokenProviderMissingPublicServiceKeyFiles =
|
|
951
|
+
findNextBrowserTokenProviderMissingPublicServiceKeyFiles({
|
|
952
|
+
dependencySources,
|
|
953
|
+
frontendSources,
|
|
954
|
+
});
|
|
955
|
+
const browserTokenProxyUsingBackendServiceKeyFiles =
|
|
956
|
+
findBrowserTokenProxyUsingBackendServiceKeyFiles(backendSources);
|
|
885
957
|
const backendIdentityRequired =
|
|
886
958
|
backendPresent &&
|
|
887
959
|
/\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
|
|
@@ -940,6 +1012,12 @@ const checkSdkLifecycle = ({
|
|
|
940
1012
|
wrong_sdk_packages: wrongFrontendSdkPackages,
|
|
941
1013
|
next_lifecycle_non_public_env_files: nextLifecycleNonPublicEnvFiles,
|
|
942
1014
|
wildcard_sdk_dependency_files: wildcardFrontendSdkDependencyFiles,
|
|
1015
|
+
browser_token_provider_missing_service_key_files:
|
|
1016
|
+
browserTokenProviderMissingServiceKeyFiles,
|
|
1017
|
+
next_browser_token_provider_missing_public_service_key_files:
|
|
1018
|
+
nextBrowserTokenProviderMissingPublicServiceKeyFiles,
|
|
1019
|
+
browser_token_proxy_uses_backend_service_key_files:
|
|
1020
|
+
browserTokenProxyUsingBackendServiceKeyFiles,
|
|
943
1021
|
},
|
|
944
1022
|
has_noop_wrapper: noOpPattern.test(combined),
|
|
945
1023
|
component_lifecycle_init_files: componentLifecycleInitFiles,
|
|
@@ -958,6 +1036,9 @@ const checkSdkLifecycle = ({
|
|
|
958
1036
|
wrongFrontendSdkPackages.length === 0 &&
|
|
959
1037
|
nextLifecycleNonPublicEnvFiles.length === 0 &&
|
|
960
1038
|
wildcardFrontendSdkDependencyFiles.length === 0 &&
|
|
1039
|
+
browserTokenProviderMissingServiceKeyFiles.length === 0 &&
|
|
1040
|
+
nextBrowserTokenProviderMissingPublicServiceKeyFiles.length === 0 &&
|
|
1041
|
+
browserTokenProxyUsingBackendServiceKeyFiles.length === 0 &&
|
|
961
1042
|
!noOpPattern.test(combined) &&
|
|
962
1043
|
componentLifecycleInitFiles.length === 0 &&
|
|
963
1044
|
blockingLifecycleFiles.length === 0,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const SETUP_DOCUMENTATION_CONTRACT_VERSION =
|
|
2
|
+
"2026-05-10.setup-documents.v1";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_SETUP_DOCUMENTS_URL = "/documents";
|
|
5
|
+
|
|
6
|
+
export const CORE_SETUP_DOCUMENT_IDS = [
|
|
7
|
+
"ai-setup-order",
|
|
8
|
+
"clue-boundary",
|
|
9
|
+
"environment-and-secrets",
|
|
10
|
+
"find-integration-points",
|
|
11
|
+
"browser-token-endpoint",
|
|
12
|
+
"clue-init",
|
|
13
|
+
"clue-identify",
|
|
14
|
+
"clue-set-account",
|
|
15
|
+
"clue-logout",
|
|
16
|
+
"setup-verification",
|
|
17
|
+
"forbidden-patterns",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const FRAMEWORK_SETUP_DOCUMENT_IDS = {
|
|
21
|
+
angular: "framework-react-spa",
|
|
22
|
+
django: "framework-django",
|
|
23
|
+
fastapi: "framework-fastapi",
|
|
24
|
+
nextjs: "framework-nextjs",
|
|
25
|
+
react: "framework-react-spa",
|
|
26
|
+
vite: "framework-react-spa",
|
|
27
|
+
vue: "framework-react-spa",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const optionalString = (value) =>
|
|
31
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
32
|
+
|
|
33
|
+
const normalizeDocumentsUrl = (documentsUrl) =>
|
|
34
|
+
optionalString(documentsUrl)?.replace(/\/+$/, "") ??
|
|
35
|
+
DEFAULT_SETUP_DOCUMENTS_URL;
|
|
36
|
+
|
|
37
|
+
const normalizeFrameworks = (frameworks = []) => [
|
|
38
|
+
...new Set(
|
|
39
|
+
frameworks
|
|
40
|
+
.map((framework) =>
|
|
41
|
+
typeof framework === "string" && framework.trim()
|
|
42
|
+
? framework.trim().toLowerCase()
|
|
43
|
+
: null,
|
|
44
|
+
)
|
|
45
|
+
.filter(Boolean),
|
|
46
|
+
),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const docUrlFor = ({ documentsUrl, docId }) => `${documentsUrl}#${docId}`;
|
|
50
|
+
|
|
51
|
+
export const frameworkDocIdsFor = (frameworks = []) =>
|
|
52
|
+
[
|
|
53
|
+
...new Set(
|
|
54
|
+
normalizeFrameworks(frameworks)
|
|
55
|
+
.map((framework) => FRAMEWORK_SETUP_DOCUMENT_IDS[framework])
|
|
56
|
+
.filter(Boolean),
|
|
57
|
+
),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export const buildSetupDocumentationContract = ({
|
|
61
|
+
documentsUrl,
|
|
62
|
+
framework,
|
|
63
|
+
frameworks = [],
|
|
64
|
+
} = {}) => {
|
|
65
|
+
const normalizedDocumentsUrl = normalizeDocumentsUrl(documentsUrl);
|
|
66
|
+
const selectedFrameworks = normalizeFrameworks([
|
|
67
|
+
...(framework ? [framework] : []),
|
|
68
|
+
...frameworks,
|
|
69
|
+
]);
|
|
70
|
+
const selectedFrameworkDocIds = frameworkDocIdsFor(selectedFrameworks);
|
|
71
|
+
const requiredDocIds = [
|
|
72
|
+
...new Set([...CORE_SETUP_DOCUMENT_IDS, ...selectedFrameworkDocIds]),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
version: SETUP_DOCUMENTATION_CONTRACT_VERSION,
|
|
77
|
+
documents_url: normalizedDocumentsUrl,
|
|
78
|
+
required_doc_ids: requiredDocIds,
|
|
79
|
+
framework_doc_ids_by_framework: FRAMEWORK_SETUP_DOCUMENT_IDS,
|
|
80
|
+
selected_frameworks: selectedFrameworks,
|
|
81
|
+
selected_framework_doc_ids: selectedFrameworkDocIds,
|
|
82
|
+
doc_urls: Object.fromEntries(
|
|
83
|
+
requiredDocIds.map((docId) => [
|
|
84
|
+
docId,
|
|
85
|
+
docUrlFor({ documentsUrl: normalizedDocumentsUrl, docId }),
|
|
86
|
+
]),
|
|
87
|
+
),
|
|
88
|
+
pre_editing_gate: [
|
|
89
|
+
"Open documents_url before editing when tool access allows it.",
|
|
90
|
+
"Read every required_doc_ids entry that applies to the detected framework.",
|
|
91
|
+
"If documents_url cannot be opened, continue only from the generated skills and manifest doc ids, and report documentation_access_blocked.",
|
|
92
|
+
"Do not implement by prompt memory alone when the manifest contains a documentation contract.",
|
|
93
|
+
],
|
|
94
|
+
report_required_fields: ["consulted_document_ids"],
|
|
95
|
+
agent_rule:
|
|
96
|
+
"Read the relevant Clue setup documents before editing and list consulted_document_ids in the final report.",
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
package/src/setup-help.mjs
CHANGED
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
CLUE_CLI_INVOCATION_CONTRACT,
|
|
3
3
|
clueCliCommand,
|
|
4
4
|
} from "./cli-invocation.mjs";
|
|
5
|
+
import { buildSetupDocumentationContract } from "./setup-documents.mjs";
|
|
5
6
|
|
|
6
|
-
export const AI_SETUP_HELP_VERSION = "2026-05-10.lifecycle-placement-only.
|
|
7
|
+
export const AI_SETUP_HELP_VERSION = "2026-05-10.lifecycle-placement-only.v4";
|
|
7
8
|
|
|
8
9
|
export const buildAiSetupHelp = () => ({
|
|
9
10
|
name: "@clue-ai/cli AI setup help",
|
|
@@ -55,9 +56,10 @@ export const buildAiSetupHelp = () => ({
|
|
|
55
56
|
},
|
|
56
57
|
backend_browser_token_proxy_env: {
|
|
57
58
|
variables: ["CLUE_API_KEY", "CLUE_API_BASE_URL"],
|
|
58
|
-
rule: "CLUE_API_KEY stays server-side. CLUE_API_BASE_URL is used only by backend-owned browser token proxy code and is not part of backend SDK initialization.",
|
|
59
|
+
rule: "CLUE_API_KEY stays server-side. CLUE_API_BASE_URL is used only by backend-owned browser token proxy code and is not part of backend SDK initialization. The proxy endpoint belongs to the customer backend, but it calls the Clue API server-side at /api/v1/ingest/browser-tokens. The frontend browserTokenProvider must send the frontend ClueInit serviceKey to that customer backend proxy, and the proxy must issue browser tokens for that frontend serviceKey, not the backend service key.",
|
|
59
60
|
},
|
|
60
61
|
},
|
|
62
|
+
documentation_contract: buildSetupDocumentationContract(),
|
|
61
63
|
setup_watch: {
|
|
62
64
|
owner: "user",
|
|
63
65
|
ai_agent_must_run: false,
|
package/src/setup-prepare.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
CLUE_CLI_INVOCATION_CONTRACT,
|
|
9
9
|
clueCliCommand,
|
|
10
10
|
} from "./cli-invocation.mjs";
|
|
11
|
+
import { buildSetupDocumentationContract } from "./setup-documents.mjs";
|
|
11
12
|
import { runSetupDetect } from "./setup-detect.mjs";
|
|
12
13
|
|
|
13
14
|
const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
@@ -167,6 +168,7 @@ const aiProviderGuideForTarget = (target) =>
|
|
|
167
168
|
const setupContextFromInput = (input = {}) => ({
|
|
168
169
|
clue_api_key: optionalString(input.clueApiKey),
|
|
169
170
|
clue_api_base_url: optionalString(input.clueApiBaseUrl),
|
|
171
|
+
documents_url: optionalString(input.documentsUrl),
|
|
170
172
|
project_key: optionalString(input.projectKey),
|
|
171
173
|
environment: optionalString(input.environment),
|
|
172
174
|
});
|
|
@@ -386,10 +388,22 @@ export const runSetupPrepare = async ({
|
|
|
386
388
|
const detection = await runSetupDetect({ repoRoot: resolvedRepoRoot });
|
|
387
389
|
const { candidate, blockers } = firstCandidateOrBlocker(detection);
|
|
388
390
|
if (!candidate) {
|
|
391
|
+
const detectedFrameworks = [
|
|
392
|
+
...(Array.isArray(detection.services?.frontend)
|
|
393
|
+
? detection.services.frontend.map((service) => service.framework)
|
|
394
|
+
: []),
|
|
395
|
+
...(Array.isArray(detection.services?.backend)
|
|
396
|
+
? detection.services.backend.map((service) => service.framework)
|
|
397
|
+
: []),
|
|
398
|
+
];
|
|
389
399
|
const manifest = {
|
|
390
400
|
status: "blocked",
|
|
391
401
|
target,
|
|
392
402
|
skill_root: skillRoot,
|
|
403
|
+
documentation: buildSetupDocumentationContract({
|
|
404
|
+
documentsUrl: setupContext.documents_url,
|
|
405
|
+
frameworks: detectedFrameworks,
|
|
406
|
+
}),
|
|
393
407
|
blockers,
|
|
394
408
|
detection,
|
|
395
409
|
ai_next_scope: "blocked_until_backend_routes_are_detected",
|
|
@@ -435,6 +449,10 @@ export const runSetupPrepare = async ({
|
|
|
435
449
|
request,
|
|
436
450
|
});
|
|
437
451
|
const watchTargets = buildWatchTargets(detection, candidate);
|
|
452
|
+
const detectedFrameworks = [
|
|
453
|
+
candidate.framework,
|
|
454
|
+
...watchTargets.map((target) => target.framework),
|
|
455
|
+
];
|
|
438
456
|
const frontendRuntimeEnvNames = requiredFrontendEnvNames(watchTargets);
|
|
439
457
|
const backendRuntimeEnvNames = requiredBackendEnvNames(watchTargets);
|
|
440
458
|
const serviceRuntimeEnvNames = [
|
|
@@ -450,6 +468,11 @@ export const runSetupPrepare = async ({
|
|
|
450
468
|
backend_root_path: candidate.backend_root_path,
|
|
451
469
|
service_key: candidate.service_key,
|
|
452
470
|
},
|
|
471
|
+
documentation: buildSetupDocumentationContract({
|
|
472
|
+
documentsUrl: setupContext.documents_url,
|
|
473
|
+
framework: candidate.framework,
|
|
474
|
+
frameworks: detectedFrameworks,
|
|
475
|
+
}),
|
|
453
476
|
service_identity: {
|
|
454
477
|
canonical_field: "service_key",
|
|
455
478
|
backend_env_name: "CLUE_SERVICE_KEY",
|
package/src/setup-tool.mjs
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
CLUE_CLI_RECOMMENDED_PREFIX,
|
|
8
8
|
clueCliCommand,
|
|
9
9
|
} from "./cli-invocation.mjs";
|
|
10
|
+
import { buildSetupDocumentationContract } from "./setup-documents.mjs";
|
|
10
11
|
|
|
11
12
|
const SKILL_NAMES = [
|
|
12
13
|
"clue-setup-orchestrator",
|
|
14
|
+
"clue-setup-reference",
|
|
13
15
|
"clue-route-semantic-snapshot",
|
|
14
16
|
"clue-semantic-gen",
|
|
15
17
|
"clue-sdk-instrumentation",
|
|
@@ -17,7 +19,7 @@ const SKILL_NAMES = [
|
|
|
17
19
|
"clue-local-verification",
|
|
18
20
|
"clue-setup-report",
|
|
19
21
|
];
|
|
20
|
-
const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.
|
|
22
|
+
const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v4";
|
|
21
23
|
|
|
22
24
|
const TARGETS = new Set(["codex", "claude_code"]);
|
|
23
25
|
|
|
@@ -37,10 +39,21 @@ const normalizeTarget = (target) => {
|
|
|
37
39
|
return normalized;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
|
-
const skillBody = (name) => {
|
|
42
|
+
const skillBody = (name, { documentsUrl } = {}) => {
|
|
43
|
+
const documentationContract = buildSetupDocumentationContract({
|
|
44
|
+
documentsUrl,
|
|
45
|
+
});
|
|
46
|
+
const requiredDocIds = documentationContract.required_doc_ids.join(", ");
|
|
47
|
+
const frameworkDocIds = Object.entries(
|
|
48
|
+
documentationContract.framework_doc_ids_by_framework,
|
|
49
|
+
)
|
|
50
|
+
.map(([framework, docId]) => `${framework} -> ${docId}`)
|
|
51
|
+
.join(", ");
|
|
41
52
|
const descriptions = {
|
|
42
53
|
"clue-setup-orchestrator":
|
|
43
54
|
"Use first when running Clue setup so lifecycle placement remains the only implementation workstream and read-only checks stay separate.",
|
|
55
|
+
"clue-setup-reference":
|
|
56
|
+
"Use before Clue setup implementation to read the public setup documents contract, select framework-specific docs, and record consulted document ids.",
|
|
44
57
|
"clue-route-semantic-snapshot":
|
|
45
58
|
"Use when checking backend route coverage and semantic snapshot readiness without hand-authoring generated snapshot files.",
|
|
46
59
|
"clue-semantic-gen":
|
|
@@ -58,6 +71,8 @@ const skillBody = (name) => {
|
|
|
58
71
|
const agentRoles = {
|
|
59
72
|
"clue-setup-orchestrator":
|
|
60
73
|
"Coordinator agent. Owns sequencing, agent assignment, gates, and blocker handling. It must not edit product code directly.",
|
|
74
|
+
"clue-setup-reference":
|
|
75
|
+
"Documentation reference agent. Owns reading the Clue setup docs contract, selecting relevant document ids, and reporting documentation blockers. It must not edit product code.",
|
|
61
76
|
"clue-route-semantic-snapshot":
|
|
62
77
|
"Semantic route readiness agent. Owns backend route inventory/readiness validation only. It must not author generated snapshot content or SDK code.",
|
|
63
78
|
"clue-semantic-gen":
|
|
@@ -75,10 +90,18 @@ const skillBody = (name) => {
|
|
|
75
90
|
const owns = {
|
|
76
91
|
"clue-setup-orchestrator": [
|
|
77
92
|
"read `.clue/setup-manifest.json` first",
|
|
93
|
+
"require `clue-setup-reference` before any SDK lifecycle placement",
|
|
78
94
|
"assign exactly one implementation agent for SDK lifecycle placement",
|
|
79
95
|
"assign multiple read-only monitoring agents",
|
|
80
96
|
"stop on manifest blockers or P0/P1 findings",
|
|
81
97
|
],
|
|
98
|
+
"clue-setup-reference": [
|
|
99
|
+
"read `.clue/setup-manifest.json` documentation contract",
|
|
100
|
+
"open the public setup documents URL when tool access allows it",
|
|
101
|
+
"select required lifecycle docs and the detected framework doc",
|
|
102
|
+
"report documentation_access_blocked when docs cannot be opened",
|
|
103
|
+
"produce consulted_document_ids for downstream implementation and final report",
|
|
104
|
+
],
|
|
82
105
|
"clue-route-semantic-snapshot": [
|
|
83
106
|
"backend route discovery readiness",
|
|
84
107
|
"route coverage gaps and unsupported-framework blockers",
|
|
@@ -122,6 +145,12 @@ const skillBody = (name) => {
|
|
|
122
145
|
"merge workstreams into one broad task",
|
|
123
146
|
"continue after a blocker without reporting it",
|
|
124
147
|
],
|
|
148
|
+
"clue-setup-reference": [
|
|
149
|
+
"edit product code",
|
|
150
|
+
"infer setup behavior from memory when the docs contract is available",
|
|
151
|
+
"skip framework-specific docs when a framework doc id is available",
|
|
152
|
+
"claim docs were read without listing consulted_document_ids",
|
|
153
|
+
],
|
|
125
154
|
"clue-route-semantic-snapshot": [
|
|
126
155
|
"create SDK lifecycle calls",
|
|
127
156
|
"create or edit the CI workflow",
|
|
@@ -161,6 +190,12 @@ const skillBody = (name) => {
|
|
|
161
190
|
"agent plan with execution agents, monitoring agents, file ownership, and gate order",
|
|
162
191
|
"blocker list if setup cannot proceed",
|
|
163
192
|
],
|
|
193
|
+
"clue-setup-reference": [
|
|
194
|
+
"documentation readiness result: ready or blocked",
|
|
195
|
+
"documents_url used",
|
|
196
|
+
"consulted_document_ids or documentation_access_blocked reason",
|
|
197
|
+
"framework document id selected from the manifest",
|
|
198
|
+
],
|
|
164
199
|
"clue-route-semantic-snapshot": [
|
|
165
200
|
"route readiness result: ready or blocked",
|
|
166
201
|
"route coverage evidence and blocker details",
|
|
@@ -198,18 +233,32 @@ const skillBody = (name) => {
|
|
|
198
233
|
"Use multiple monitoring agents for read-only checks, or named review passes if subagents are unavailable.",
|
|
199
234
|
`The initial \`${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment <environment>")}\` command already performs repository discovery, semantic CI workflow generation, setup manifest generation, and writes service-specific environment guidance to \`.env.clue\` when backend routes can be detected.`,
|
|
200
235
|
"Before implementation, read `.clue/setup-manifest.json` and treat it as the mechanical setup source of truth.",
|
|
236
|
+
"Read `.clue/setup-manifest.json` `documentation` and run `clue-setup-reference` before assigning SDK lifecycle placement.",
|
|
201
237
|
"Read `.clue/setup-manifest.json` `cli_invocation` before running any Clue CLI subcommand.",
|
|
202
238
|
`Before editing a customer repository, run \`${clueCliCommand("help --json")}\` and follow its setup_execution_contract.`,
|
|
203
239
|
"Use the service keys and watch targets from `.clue/setup-manifest.json`; do not invent service keys.",
|
|
204
240
|
"If `.clue/setup-manifest.json` has status `blocked`, stop and report its blockers instead of guessing.",
|
|
205
241
|
"Treat semantic snapshot readiness and semantic CI as generated/static verification surfaces, not AI implementation workstreams.",
|
|
206
242
|
"Do not implement or refresh semantic snapshot CI during lifecycle placement; report a blocker if generated semantic artifacts are missing or stale.",
|
|
243
|
+
"Before lifecycle placement, require `clue-setup-reference` to produce consulted_document_ids.",
|
|
207
244
|
"Before lifecycle placement, read and apply `clue-sdk-instrumentation`.",
|
|
208
245
|
"After lifecycle placement, run a monitoring check with `clue-setup-audit` before continuing.",
|
|
209
246
|
"For final local verification, read and apply `clue-local-verification`.",
|
|
210
247
|
"For the final report, read and apply `clue-setup-report`.",
|
|
211
248
|
"Do not continue past P0/P1 monitoring findings until fixed or reported as blocked.",
|
|
212
249
|
],
|
|
250
|
+
"clue-setup-reference": [
|
|
251
|
+
"Read `.clue/setup-manifest.json` first.",
|
|
252
|
+
"Find the `documentation` object. If it is missing, run `npx -y @clue-ai/cli help --json` and use `setup_execution_contract.documentation_contract` as the fallback contract.",
|
|
253
|
+
"Use `documentation.documents_url` as the public docs page.",
|
|
254
|
+
"Open the docs page when the AI tool has browser or HTTP access. If it cannot be opened, continue only from the generated skill text and manifest doc ids, then report `documentation_access_blocked`.",
|
|
255
|
+
"Read all ids in `documentation.required_doc_ids` that apply to the detected repository.",
|
|
256
|
+
"For framework-specific implementation, select ids from `documentation.selected_framework_doc_ids` first. If empty, use `documentation.framework_doc_ids_by_framework` for the detected framework.",
|
|
257
|
+
"Minimum lifecycle docs before implementation: ai-setup-order, environment-and-secrets, browser-token-endpoint, clue-init, clue-identify, clue-set-account, clue-logout, setup-verification, forbidden-patterns.",
|
|
258
|
+
"For Next.js, read framework-nextjs. For React, Vite, Vue, or Angular browser apps, read framework-react-spa. For FastAPI, read framework-fastapi. For Django, read framework-django.",
|
|
259
|
+
"Return `consulted_document_ids` to the orchestrator and final report.",
|
|
260
|
+
"If a document contradicts `.clue/setup-manifest.json` or generated skills, stop and report a blocker instead of choosing behavior silently.",
|
|
261
|
+
],
|
|
213
262
|
"clue-route-semantic-snapshot": [
|
|
214
263
|
"Use this skill as the source of truth for semantic snapshot readiness and route coverage verification.",
|
|
215
264
|
"Do not hand-author semantic snapshot content files.",
|
|
@@ -251,6 +300,7 @@ const skillBody = (name) => {
|
|
|
251
300
|
],
|
|
252
301
|
"clue-sdk-instrumentation": [
|
|
253
302
|
"Use this skill as the source of truth for Clue SDK lifecycle implementation.",
|
|
303
|
+
"Before editing, confirm `clue-setup-reference` has produced consulted_document_ids for the relevant framework and lifecycle docs.",
|
|
254
304
|
"The implementation scope is only ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout placement in existing lifecycle boundaries.",
|
|
255
305
|
"Keep SDK lifecycle implementation separate from semantic snapshot and CI workflow work.",
|
|
256
306
|
"Do not create semantic snapshot content or semantic snapshot CI workflow files from this skill.",
|
|
@@ -272,7 +322,10 @@ const skillBody = (name) => {
|
|
|
272
322
|
"Never put `CLUE_API_KEY` in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
273
323
|
"When browser SDK ingest is configured, implement a backend-owned browser token endpoint that reads server-side `CLUE_API_KEY` and requests `POST /api/v1/ingest/browser-tokens` from Clue.",
|
|
274
324
|
"Configure frontend `ClueInit` with `browserTokenProvider` that calls the local backend token endpoint and returns the token string.",
|
|
325
|
+
"The local backend token endpoint is part of the customer app, not the Clue API. It may be named with the customer app's route convention, but it must call Clue server-side at `/api/v1/ingest/browser-tokens`.",
|
|
326
|
+
"The frontend `browserTokenProvider` must send the same service key used by `ClueInit` to the customer backend token endpoint. For Next.js this value comes from `NEXT_PUBLIC_CLUE_SERVICE_KEY`.",
|
|
275
327
|
"The browser token request must include project key, environment, service key, and the current browser origin; the backend must attach `x-clue-api-key` server-side when calling Clue.",
|
|
328
|
+
"For browser token proxy code, the service key sent to Clue must be the frontend `ClueInit` serviceKey from the browser request, not the backend service's `CLUE_SERVICE_KEY`.",
|
|
276
329
|
"If a backend-owned browser token endpoint is implemented, read `CLUE_API_BASE_URL` from the backend env block and normalize it so values with or without a trailing `/api/v1` do not produce duplicate paths.",
|
|
277
330
|
"For FastAPI code, add `clue-fastapi-sdk` to the backend dependency file when missing, import `clue_init_fastapi` plus `ClueIdentify`, `ClueSetAccount`, and `ClueLogout` where needed, and use `CLUE_PROJECT_KEY`, `CLUE_ENVIRONMENT`, `CLUE_API_KEY`, and `CLUE_INGEST_ENDPOINT` from the backend env block.",
|
|
278
331
|
"`CLUE_API_BASE_URL` is not part of backend SDK initialization. Use it only when the application backend owns a browser-token proxy for a frontend service.",
|
|
@@ -294,6 +347,7 @@ const skillBody = (name) => {
|
|
|
294
347
|
],
|
|
295
348
|
"clue-setup-audit": [
|
|
296
349
|
"Act as a read-only monitoring agent, not the execution agent.",
|
|
350
|
+
"Confirm the final report draft includes consulted_document_ids and that lifecycle edits do not contradict the referenced docs.",
|
|
297
351
|
"Check one completed workstream at a time and report P0/P1 issues before more implementation continues.",
|
|
298
352
|
"Review changed files line by line.",
|
|
299
353
|
"Verify semantic snapshot, semantic CI, and SDK lifecycle responsibilities did not bleed into each other.",
|
|
@@ -318,6 +372,7 @@ const skillBody = (name) => {
|
|
|
318
372
|
],
|
|
319
373
|
"clue-local-verification": [
|
|
320
374
|
"Act as a read-only monitoring agent for local verification evidence.",
|
|
375
|
+
"Confirm documentation_access_blocked is reported if docs could not be opened, and do not treat that as event delivery verification.",
|
|
321
376
|
"Verify each workstream independently before the final setup report.",
|
|
322
377
|
"Confirm generated skill files exist.",
|
|
323
378
|
"Confirm workflow files and SDK lifecycle imports/calls exist when those phases have run.",
|
|
@@ -341,6 +396,7 @@ const skillBody = (name) => {
|
|
|
341
396
|
"clue-setup-report": [
|
|
342
397
|
"Use this skill only after execution and monitoring passes are finished.",
|
|
343
398
|
"Never claim `setup completed` from `setup-check --require-sdk-lifecycle` alone. That check is static and does not verify dependency installation, imports, app startup, or event delivery.",
|
|
399
|
+
"Include `consulted_document_ids` from `clue-setup-reference`; if docs could not be opened, include `documentation_access_blocked` with the reason.",
|
|
344
400
|
"Completion requires all applicable evidence: SDK dependencies install successfully, SDK imports work in the target frontend/backend environments, the app starts, `setup-check --require-sdk-lifecycle` passes, and user-provided `setup-watch --local` or Clue setup screen evidence confirms expected event delivery.",
|
|
345
401
|
"If any SDK package is unpublished, install/import checks were not run, app startup was not verified, or user-operated setup-watch/setup-screen evidence was not provided, the final status must be `blocked`, `partially complete`, or `user_verification_pending`; it must not be `complete`.",
|
|
346
402
|
"For every completion claim, include the evidence source: command, exit status, output summary, file path, runtime URL, user-provided setup-watch result, or user-provided setup-screen result.",
|
|
@@ -386,11 +442,16 @@ const skillBody = (name) => {
|
|
|
386
442
|
`- Use \`${CLUE_CLI_RECOMMENDED_PREFIX} <command>\` for Clue CLI commands unless \`.clue/setup-manifest.json\` explicitly provides a different invocation.`,
|
|
387
443
|
`- If checking Clue CLI availability, run \`${CLUE_CLI_RECOMMENDED_PREFIX} --version\` or \`${CLUE_CLI_RECOMMENDED_PREFIX} --help\` and report the exact fetch/runtime error if it fails.`,
|
|
388
444
|
`- Before editing a customer repository, run \`${clueCliCommand("help --json")}\` and follow its setup_execution_contract.`,
|
|
445
|
+
`- Public Clue setup docs: \`${documentationContract.documents_url}\`.`,
|
|
446
|
+
`- Required setup doc ids: ${requiredDocIds}.`,
|
|
447
|
+
`- Framework setup doc mapping: ${frameworkDocIds}.`,
|
|
448
|
+
"- Before editing, read `.clue/setup-manifest.json` `documentation`, run `clue-setup-reference`, and carry `consulted_document_ids` into the final report.",
|
|
389
449
|
`- Do not search for a global \`${CLUE_CLI_BINARY_NAME}\` binary or block on \`which ${CLUE_CLI_BINARY_NAME}\`; missing global binary is normal.`,
|
|
390
450
|
"- The AI implementation task is only to decide where ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout belong in existing code and apply the minimal SDK wiring for those calls.",
|
|
391
451
|
"- Only exact changes required to place ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout are allowed. Do not perform ClueTrack instrumentation unless the user explicitly requested product event tracking.",
|
|
392
452
|
"- Do not perform unrelated refactors, renames, file moves, formatting churn, broad cleanup, business logic rewrites, auth/session rewrites beyond minimal Clue hook insertion, UI changes unrelated to Clue setup, or unrelated dependency upgrades.",
|
|
393
453
|
"- Do not run broad formatters or make whitespace-only cleanup. Keep formatting changes limited to lines directly touched for Clue SDK wiring.",
|
|
454
|
+
"- A customer backend browser-token endpoint is only a proxy for browser SDK token issuance. It is not the Clue API itself; the backend must call Clue server-side at `/api/v1/ingest/browser-tokens` with the frontend `ClueInit` service key supplied by the browser token provider.",
|
|
394
455
|
"- Do not implement or refresh semantic snapshot CI during lifecycle placement; report a blocker if generated semantic artifacts are missing or stale.",
|
|
395
456
|
`- Do not run \`${clueCliCommand("setup-watch --local")}\` automatically. setup-watch and the Clue setup screen are user-operated verification steps, not implementation-agent responsibility.`,
|
|
396
457
|
"- The full setup must start with `clue-setup-orchestrator`.",
|
|
@@ -432,6 +493,7 @@ const askTarget = async ({
|
|
|
432
493
|
export const installSetupSkills = async ({
|
|
433
494
|
repoRoot,
|
|
434
495
|
target,
|
|
496
|
+
documentsUrl,
|
|
435
497
|
input,
|
|
436
498
|
output,
|
|
437
499
|
} = {}) => {
|
|
@@ -449,7 +511,7 @@ export const installSetupSkills = async ({
|
|
|
449
511
|
const skillDir = join(skillRoot, skillName);
|
|
450
512
|
const skillPath = join(skillDir, "SKILL.md");
|
|
451
513
|
await mkdir(skillDir, { recursive: true });
|
|
452
|
-
await writeFile(skillPath, skillBody(skillName), "utf8");
|
|
514
|
+
await writeFile(skillPath, skillBody(skillName, { documentsUrl }), "utf8");
|
|
453
515
|
installed.push(skillPath);
|
|
454
516
|
}
|
|
455
517
|
|