@clue-ai/cli 0.0.17 → 0.0.18
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/package.json +1 -1
- package/src/lifecycle-init.mjs +9 -2
- package/src/semantic-ci.mjs +153 -18
- package/src/setup-ai-contract.mjs +24 -1
- package/src/setup-check.mjs +108 -3
- package/src/setup-doctor.mjs +11 -4
- package/src/setup-help.mjs +4 -1
- package/src/setup-prepare.mjs +19 -0
- package/src/setup-tool.mjs +12 -2
package/package.json
CHANGED
package/src/lifecycle-init.mjs
CHANGED
|
@@ -580,15 +580,20 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
580
580
|
"Use environment variable names for Clue configuration values.",
|
|
581
581
|
"For Python/FastAPI code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_API_KEY, and CLUE_INGEST_ENDPOINT from the backend env block.",
|
|
582
582
|
"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.",
|
|
583
|
-
"For Next.js browser/client code, read NEXT_PUBLIC_CLUE_PROJECT_KEY, NEXT_PUBLIC_CLUE_ENVIRONMENT, NEXT_PUBLIC_CLUE_SERVICE_KEY, and
|
|
583
|
+
"For Next.js browser/client code, read NEXT_PUBLIC_CLUE_PROJECT_KEY, NEXT_PUBLIC_CLUE_ENVIRONMENT, NEXT_PUBLIC_CLUE_SERVICE_KEY, NEXT_PUBLIC_CLUE_INGEST_ENDPOINT, and NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT from the frontend .env.local block.",
|
|
584
584
|
"Do not read process.env.CLUE_PROJECT_KEY, process.env.CLUE_ENVIRONMENT, process.env.CLUE_SERVICE_KEY, or process.env.CLUE_INGEST_ENDPOINT in Next.js browser/client code, and do not add non-public CLUE_* fallbacks there.",
|
|
585
|
+
"Frontend SDK adapter code is contract-owned Clue setup wiring. The AI may choose the existing import/mount point, but must not invent token URL, env, or initialization semantics.",
|
|
586
|
+
"For Next.js frontend adapters, read the full customer-backend browser-token proxy URL from NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT. Do not derive it from NEXT_PUBLIC_API_URL, generic app API env names, detected backend ports, or relative frontend-origin paths.",
|
|
587
|
+
"Do not mix stale browser-token paths such as /api/clue/browser-token, /clue/browser-tokens, or /browser-tokens with the canonical /api/v1/clue/browser-tokens path.",
|
|
588
|
+
"Do not call ClueInit with empty-string fallbacks for required NEXT_PUBLIC_CLUE_* values. If required Clue env is absent, skip initialization and report the missing env names.",
|
|
589
|
+
"If a singleton guard is used, do not set initialized = true before ClueInit has actually been called with required config present.",
|
|
585
590
|
"For non-Next.js browser code, use the exact frontend env names written in .env.clue for that service instead of inventing a framework-specific prefix.",
|
|
586
591
|
"Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
587
592
|
"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.",
|
|
588
593
|
"Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
|
|
589
594
|
"Keep the four setup API hops distinct: customer frontend -> customer backend /api/v1/clue/browser-tokens, customer backend -> Clue /api/v1/ingest/browser-tokens, customer frontend -> Clue /api/v1/ingest/browser, and customer backend -> Clue /api/v1/ingest/backend.",
|
|
590
595
|
"The local backend token endpoint is part of the customer app, not the Clue API. Place it under a Clue-reserved local route such as /api/v1/clue/browser-tokens; do not use a generic path such as /browser-tokens that could be confused with product behavior. It must call Clue server-side at /api/v1/ingest/browser-tokens.",
|
|
591
|
-
"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.",
|
|
596
|
+
"The frontend browserTokenProvider must call NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT and 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.",
|
|
592
597
|
"The browser token request must include the frontend service key used by ClueInit. Project key and environment may be included only as public consistency hints; the backend must use server configuration or validate them against server configuration before calling Clue.",
|
|
593
598
|
"The backend browser token proxy must derive request origin from trusted request headers or server request metadata. Do not trust origin, projectKey, or environment from JSON/body payload fields when calling Clue with server CLUE_API_KEY.",
|
|
594
599
|
"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.",
|
|
@@ -614,6 +619,8 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
614
619
|
browser_ingest_endpoint_env: "framework_specific",
|
|
615
620
|
nextjs_browser_ingest_endpoint_env: "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
616
621
|
client_backend_browser_token_proxy_path: "/api/v1/clue/browser-tokens",
|
|
622
|
+
nextjs_browser_token_endpoint_env:
|
|
623
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
617
624
|
clue_backend_browser_token_issue_path: "/api/v1/ingest/browser-tokens",
|
|
618
625
|
nextjs_browser_service_key_env: "NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
619
626
|
service_key: request.service_key,
|
package/src/semantic-ci.mjs
CHANGED
|
@@ -97,6 +97,9 @@ const PURPOSE_CHANGE_STATES = new Set([
|
|
|
97
97
|
"new_route",
|
|
98
98
|
]);
|
|
99
99
|
|
|
100
|
+
const shouldPurposeRecheckAllRoutes = (env) =>
|
|
101
|
+
env.CLUE_SEMANTIC_PURPOSE_RECHECK_ALL === "1";
|
|
102
|
+
|
|
100
103
|
const semanticSnapshotHashScope = (request) =>
|
|
101
104
|
[
|
|
102
105
|
"clue_tools_semantic_snapshot",
|
|
@@ -2067,27 +2070,27 @@ const buildSnapshot = ({
|
|
|
2067
2070
|
currentRoute: route,
|
|
2068
2071
|
plan,
|
|
2069
2072
|
route: {
|
|
2070
|
-
...baseRoute,
|
|
2071
|
-
operation_effects: assignmentCollections.operationEffects,
|
|
2072
|
-
unresolved_operation_effects:
|
|
2073
|
-
assignmentCollections.unresolvedOperationEffects,
|
|
2074
|
-
source_evidence_refs: routeEvidenceRefs.map((entry) => entry.id),
|
|
2075
|
-
route_input_hash: plan.route_input_hash,
|
|
2076
|
-
previous_route_input_hash: plan.previous_route_input_hash,
|
|
2077
|
-
previous_route_semantic_hash: plan.previous_route_semantic_hash,
|
|
2078
|
-
semantic_origin:
|
|
2079
|
-
plan.origin === "changed_route_needs_review" && aiRoute?.semantics
|
|
2080
|
-
? "changed_route_needs_review"
|
|
2081
|
-
: plan.origin,
|
|
2082
|
-
semantic_change_reason: plan.semantic_change_reason,
|
|
2083
|
-
previous_semantic_snapshot_version:
|
|
2084
|
-
plan.previous_route?.semantic_snapshot_version,
|
|
2085
|
-
route_semantic_hash: routeSemanticHash({
|
|
2086
2073
|
...baseRoute,
|
|
2087
2074
|
operation_effects: assignmentCollections.operationEffects,
|
|
2088
2075
|
unresolved_operation_effects:
|
|
2089
2076
|
assignmentCollections.unresolvedOperationEffects,
|
|
2090
|
-
|
|
2077
|
+
source_evidence_refs: routeEvidenceRefs.map((entry) => entry.id),
|
|
2078
|
+
route_input_hash: plan.route_input_hash,
|
|
2079
|
+
previous_route_input_hash: plan.previous_route_input_hash,
|
|
2080
|
+
previous_route_semantic_hash: plan.previous_route_semantic_hash,
|
|
2081
|
+
semantic_origin:
|
|
2082
|
+
plan.origin === "changed_route_needs_review" && aiRoute?.semantics
|
|
2083
|
+
? "changed_route_needs_review"
|
|
2084
|
+
: plan.origin,
|
|
2085
|
+
semantic_change_reason: plan.semantic_change_reason,
|
|
2086
|
+
previous_semantic_snapshot_version:
|
|
2087
|
+
plan.previous_route?.semantic_snapshot_version,
|
|
2088
|
+
route_semantic_hash: routeSemanticHash({
|
|
2089
|
+
...baseRoute,
|
|
2090
|
+
operation_effects: assignmentCollections.operationEffects,
|
|
2091
|
+
unresolved_operation_effects:
|
|
2092
|
+
assignmentCollections.unresolvedOperationEffects,
|
|
2093
|
+
}),
|
|
2091
2094
|
},
|
|
2092
2095
|
});
|
|
2093
2096
|
});
|
|
@@ -2537,6 +2540,7 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2537
2540
|
hashScope,
|
|
2538
2541
|
generationContract,
|
|
2539
2542
|
agentSkills,
|
|
2543
|
+
purposeRecheckAllRoutes = false,
|
|
2540
2544
|
}) => {
|
|
2541
2545
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
2542
2546
|
const plans = new Map();
|
|
@@ -2562,6 +2566,80 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2562
2566
|
previousRoute.route_input_hash === currentHash &&
|
|
2563
2567
|
hasReusablePreviousRouteSemantics(previousRoute)
|
|
2564
2568
|
) {
|
|
2569
|
+
if (purposeRecheckAllRoutes) {
|
|
2570
|
+
const decision = await callAiReuseDecisionProvider({
|
|
2571
|
+
request,
|
|
2572
|
+
env,
|
|
2573
|
+
apiKey: aiProviderApiKey,
|
|
2574
|
+
route: buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
2575
|
+
previousRoute,
|
|
2576
|
+
currentHash,
|
|
2577
|
+
generationContract,
|
|
2578
|
+
agentSkills,
|
|
2579
|
+
});
|
|
2580
|
+
if (
|
|
2581
|
+
decision.purpose_change_state === "same_purpose" &&
|
|
2582
|
+
decision.confidence >= PURPOSE_STABILITY_CONFIDENCE_THRESHOLD
|
|
2583
|
+
) {
|
|
2584
|
+
plans.set(route.operation_source_key, {
|
|
2585
|
+
origin: "unchanged_route_reused",
|
|
2586
|
+
purpose_change_state: "same_purpose",
|
|
2587
|
+
active_semantic_source: "previous_reused",
|
|
2588
|
+
stability_confidence: decision.confidence,
|
|
2589
|
+
stability_missing_context: decision.missing_context,
|
|
2590
|
+
route_input_hash: currentHash,
|
|
2591
|
+
previous_route: previousRoute,
|
|
2592
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2593
|
+
previous_route_semantic_hash:
|
|
2594
|
+
previousRoute.route_semantic_hash ??
|
|
2595
|
+
routeSemanticHash(previousRoute),
|
|
2596
|
+
semantic_change_reason: sanitizeText(
|
|
2597
|
+
`Periodic full purpose check confirmed previous semantics still apply: ${decision.reason}`,
|
|
2598
|
+
route,
|
|
2599
|
+
),
|
|
2600
|
+
});
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (
|
|
2604
|
+
decision.decision === "regenerate" &&
|
|
2605
|
+
decision.confidence >= PURPOSE_STABILITY_CONFIDENCE_THRESHOLD
|
|
2606
|
+
) {
|
|
2607
|
+
plans.set(route.operation_source_key, {
|
|
2608
|
+
origin: "changed_route_semantic_regenerated",
|
|
2609
|
+
purpose_change_state: decision.purpose_change_state,
|
|
2610
|
+
active_semantic_source: "new_confirmed",
|
|
2611
|
+
stability_confidence: decision.confidence,
|
|
2612
|
+
stability_missing_context: decision.missing_context,
|
|
2613
|
+
route_input_hash: currentHash,
|
|
2614
|
+
previous_route: previousRoute,
|
|
2615
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2616
|
+
previous_route_semantic_hash:
|
|
2617
|
+
previousRoute.route_semantic_hash ??
|
|
2618
|
+
routeSemanticHash(previousRoute),
|
|
2619
|
+
semantic_change_reason: sanitizeText(decision.reason, route),
|
|
2620
|
+
});
|
|
2621
|
+
routesRequiringGeneration.push(route);
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
plans.set(route.operation_source_key, {
|
|
2625
|
+
origin: "changed_route_needs_review",
|
|
2626
|
+
purpose_change_state: "insufficient_evidence",
|
|
2627
|
+
active_semantic_source: "previous_kept_pending_review",
|
|
2628
|
+
stability_confidence: decision.confidence,
|
|
2629
|
+
stability_missing_context: decision.missing_context,
|
|
2630
|
+
route_input_hash: currentHash,
|
|
2631
|
+
previous_route: previousRoute,
|
|
2632
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2633
|
+
previous_route_semantic_hash:
|
|
2634
|
+
previousRoute.route_semantic_hash ??
|
|
2635
|
+
routeSemanticHash(previousRoute),
|
|
2636
|
+
semantic_change_reason: sanitizeText(
|
|
2637
|
+
`Periodic full purpose check could not prove a purpose-level change: ${decision.reason}`,
|
|
2638
|
+
route,
|
|
2639
|
+
),
|
|
2640
|
+
});
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2565
2643
|
plans.set(route.operation_source_key, {
|
|
2566
2644
|
origin: "unchanged_route_reused",
|
|
2567
2645
|
purpose_change_state: "same_purpose",
|
|
@@ -3186,6 +3264,10 @@ const assertSemanticSnapshotAudit = ({
|
|
|
3186
3264
|
const routeByKey = new Map(
|
|
3187
3265
|
routes.map((route) => [route.operation_source_key, route]),
|
|
3188
3266
|
);
|
|
3267
|
+
const previousActiveSources = new Set([
|
|
3268
|
+
"previous_reused",
|
|
3269
|
+
"previous_kept_pending_review",
|
|
3270
|
+
]);
|
|
3189
3271
|
const allowedOrigins = new Set([
|
|
3190
3272
|
"new_route_ai_generated",
|
|
3191
3273
|
"unchanged_route_reused",
|
|
@@ -3216,6 +3298,56 @@ const assertSemanticSnapshotAudit = ({
|
|
|
3216
3298
|
`semantic snapshot audit found stale route_semantic_hash: ${route.operation_source_key}`,
|
|
3217
3299
|
);
|
|
3218
3300
|
}
|
|
3301
|
+
if (auditedSnapshot.schema_version >= 3) {
|
|
3302
|
+
if (route.semantic_stability.purpose_change_state !== route.purpose_change_state) {
|
|
3303
|
+
throw new Error(
|
|
3304
|
+
`semantic snapshot audit found mismatched purpose stability metadata: ${route.operation_source_key}`,
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
if (
|
|
3308
|
+
previousActiveSources.has(route.active_semantic_source) &&
|
|
3309
|
+
(!route.previous_route_input_hash ||
|
|
3310
|
+
!route.previous_route_semantic_hash ||
|
|
3311
|
+
!route.previous_semantic_snapshot_version)
|
|
3312
|
+
) {
|
|
3313
|
+
throw new Error(
|
|
3314
|
+
`semantic snapshot audit found incomplete previous active semantic metadata: ${route.operation_source_key}`,
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
if (
|
|
3318
|
+
previousActiveSources.has(route.active_semantic_source) &&
|
|
3319
|
+
route.route_semantic_hash !== route.previous_route_semantic_hash
|
|
3320
|
+
) {
|
|
3321
|
+
throw new Error(
|
|
3322
|
+
`semantic snapshot audit found changed active semantics for previous-backed route: ${route.operation_source_key}`,
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
if (
|
|
3326
|
+
route.active_semantic_source === "previous_kept_pending_review" &&
|
|
3327
|
+
(route.purpose_change_state !== "insufficient_evidence" ||
|
|
3328
|
+
route.semantic_origin !== "changed_route_needs_review")
|
|
3329
|
+
) {
|
|
3330
|
+
throw new Error(
|
|
3331
|
+
`semantic snapshot audit found inconsistent pending-review semantics: ${route.operation_source_key}`,
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
if (
|
|
3335
|
+
route.active_semantic_source === "previous_reused" &&
|
|
3336
|
+
route.purpose_change_state !== "same_purpose"
|
|
3337
|
+
) {
|
|
3338
|
+
throw new Error(
|
|
3339
|
+
`semantic snapshot audit found inconsistent reused semantics: ${route.operation_source_key}`,
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
if (
|
|
3343
|
+
route.purpose_change_state === "purpose_added" &&
|
|
3344
|
+
(!route.previous_route_input_hash || !route.previous_route_semantic_hash)
|
|
3345
|
+
) {
|
|
3346
|
+
throw new Error(
|
|
3347
|
+
`semantic snapshot audit found missing previous metadata for added purpose: ${route.operation_source_key}`,
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3219
3351
|
if (
|
|
3220
3352
|
(route.semantic_origin === "unchanged_route_reused" ||
|
|
3221
3353
|
route.semantic_origin === "changed_route_semantic_reused") &&
|
|
@@ -3272,12 +3404,14 @@ export const runSemanticCi = async ({
|
|
|
3272
3404
|
? await fetchLatestSnapshot({ request, env })
|
|
3273
3405
|
: validatePreviousSnapshotForReuse(providedPreviousSnapshot);
|
|
3274
3406
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
3407
|
+
const purposeRecheckAllRoutes = shouldPurposeRecheckAllRoutes(env);
|
|
3275
3408
|
const routeNeedsAi = routes.some((route) => {
|
|
3276
3409
|
const previousRoute = previousRoutes.get(route.operation_source_key);
|
|
3277
3410
|
return (
|
|
3278
3411
|
!previousRoute ||
|
|
3279
3412
|
previousRoute.route_input_hash !== routeInputHash(route) ||
|
|
3280
|
-
!hasReusablePreviousRouteSemantics(previousRoute)
|
|
3413
|
+
!hasReusablePreviousRouteSemantics(previousRoute) ||
|
|
3414
|
+
purposeRecheckAllRoutes
|
|
3281
3415
|
);
|
|
3282
3416
|
});
|
|
3283
3417
|
if (routeNeedsAi && !env.CLUE_AI_PROVIDER_API_KEY) {
|
|
@@ -3310,6 +3444,7 @@ export const runSemanticCi = async ({
|
|
|
3310
3444
|
hashScope,
|
|
3311
3445
|
generationContract,
|
|
3312
3446
|
agentSkills,
|
|
3447
|
+
purposeRecheckAllRoutes,
|
|
3313
3448
|
});
|
|
3314
3449
|
const promptRoutes = routesRequiringGeneration.map((route) =>
|
|
3315
3450
|
buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const AI_SETUP_CONTRACT_VERSION =
|
|
2
|
-
"2026-05-10.
|
|
2
|
+
"2026-05-10.frontend-adapter-contract.v8";
|
|
3
3
|
|
|
4
4
|
export const SETUP_DOCTRINE = {
|
|
5
5
|
purpose:
|
|
@@ -32,6 +32,7 @@ export const DETERMINISTIC_CONTROL_MODEL = {
|
|
|
32
32
|
"setup-watch ownership as user-operated verification",
|
|
33
33
|
"static rejection of known unsafe wiring such as leaked secrets, wrong SDKs, blocking lifecycle calls, broad ClueTrack setup, and unsafe browser token proxy patterns",
|
|
34
34
|
"local API connectivity preflight for the four required setup hops before user-operated setup-watch",
|
|
35
|
+
"canonical frontend SDK adapter env names, token proxy path, and initialization safety checks",
|
|
35
36
|
],
|
|
36
37
|
};
|
|
37
38
|
|
|
@@ -79,6 +80,27 @@ export const API_CONNECTIVITY_CONTRACT = {
|
|
|
79
80
|
"setup-doctor checks local API connectivity before user flows. setup-watch remains user-operated lifecycle and event-delivery verification.",
|
|
80
81
|
};
|
|
81
82
|
|
|
83
|
+
export const FRONTEND_ADAPTER_CONTRACT = {
|
|
84
|
+
purpose:
|
|
85
|
+
"Frontend SDK adapter code is part of the Clue setup contract. The AI may choose where the adapter is imported, but must not invent new token URL, env, or initialization semantics.",
|
|
86
|
+
nextjs_public_env: [
|
|
87
|
+
"NEXT_PUBLIC_CLUE_PROJECT_KEY",
|
|
88
|
+
"NEXT_PUBLIC_CLUE_ENVIRONMENT",
|
|
89
|
+
"NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
90
|
+
"NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
91
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
92
|
+
],
|
|
93
|
+
browser_token_proxy_path: "/api/v1/clue/browser-tokens",
|
|
94
|
+
rules: [
|
|
95
|
+
"Do not derive the Clue browser-token proxy from generic app API env names such as NEXT_PUBLIC_API_URL.",
|
|
96
|
+
"Do not mix stale browser token paths with the canonical /api/v1/clue/browser-tokens path in the same adapter.",
|
|
97
|
+
"Next.js browserTokenProvider must call NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT, whose value is the customer backend browser-token proxy URL.",
|
|
98
|
+
"Do not call ClueInit with empty-string fallbacks for required NEXT_PUBLIC_CLUE_* values.",
|
|
99
|
+
"If a singleton guard is used, do not mark initialized=true before ClueInit has been called with required config present.",
|
|
100
|
+
"The browser token provider must send the same frontend serviceKey used by ClueInit.",
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
82
104
|
export const setupDoctrineSkillLines = () => [
|
|
83
105
|
`- Purpose: ${SETUP_DOCTRINE.purpose}`,
|
|
84
106
|
`- Minimal diff reason: ${SETUP_DOCTRINE.minimal_diff_reason}`,
|
|
@@ -87,5 +109,6 @@ export const setupDoctrineSkillLines = () => [
|
|
|
87
109
|
`- Documentation reason: ${SETUP_DOCTRINE.documentation_reason}`,
|
|
88
110
|
`- Failure posture: ${SETUP_DOCTRINE.failure_posture}`,
|
|
89
111
|
`- API connectivity: ${API_CONNECTIVITY_CONTRACT.purpose}`,
|
|
112
|
+
`- Frontend adapter: ${FRONTEND_ADAPTER_CONTRACT.purpose}`,
|
|
90
113
|
`- API preflight: run ${API_CONNECTIVITY_CONTRACT.preflight_command} when local services and required env are available; do not substitute it for user-operated setup-watch.`,
|
|
91
114
|
];
|
package/src/setup-check.mjs
CHANGED
|
@@ -42,8 +42,13 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
|
|
|
42
42
|
"Do not add `@clue-ai/browser-sdk` or backend SDK dependencies with `*` or `latest`",
|
|
43
43
|
"Whitespace-only changes are allowed only on lines directly changed for Clue SDK wiring",
|
|
44
44
|
"The local backend token endpoint is part of the customer app, not the Clue API",
|
|
45
|
-
"
|
|
45
|
+
"send the same service key used by `ClueInit`",
|
|
46
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
46
47
|
"Do not forward `origin`, `projectKey`, or `environment` from JSON/body payload fields under server `CLUE_API_KEY`",
|
|
48
|
+
"Frontend SDK adapter code is contract-owned Clue setup wiring",
|
|
49
|
+
"Do not derive it from `NEXT_PUBLIC_API_URL`",
|
|
50
|
+
"Do not call `ClueInit` with empty-string fallbacks",
|
|
51
|
+
"do not set `initialized = true` before `ClueInit`",
|
|
47
52
|
"For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
|
|
48
53
|
],
|
|
49
54
|
"clue-setup-audit": [
|
|
@@ -52,6 +57,10 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
|
|
|
52
57
|
"Reject ClueTrack instrumentation unless the user explicitly requested product event tracking",
|
|
53
58
|
"Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables",
|
|
54
59
|
"Reject browser token proxy code that forwards origin, projectKey, or environment from request JSON/body under server `CLUE_API_KEY`",
|
|
60
|
+
"Reject frontend browser token providers that derive the Clue proxy URL from `NEXT_PUBLIC_API_URL`",
|
|
61
|
+
"Reject frontend adapters that mix stale browser-token paths",
|
|
62
|
+
"Reject frontend adapters that set `initialized = true` before calling `ClueInit`",
|
|
63
|
+
"Audit the setup diff against the Clue setup contract even when the code was written by another agent",
|
|
55
64
|
"Reject whitespace-only edits, import sorting, formatter churn",
|
|
56
65
|
"Reject unrelated refactors, renames, file moves",
|
|
57
66
|
"Execution agents must not approve, certify, or mark their own work complete",
|
|
@@ -736,7 +745,13 @@ const NEXT_PUBLIC_CLUE_NAMES = [
|
|
|
736
745
|
"CLUE_ENVIRONMENT",
|
|
737
746
|
"CLUE_SERVICE_KEY",
|
|
738
747
|
"CLUE_INGEST_ENDPOINT",
|
|
748
|
+
"CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
739
749
|
];
|
|
750
|
+
const CANONICAL_BROWSER_TOKEN_PROXY_PATH = "/api/v1/clue/browser-tokens";
|
|
751
|
+
const BROWSER_TOKEN_PATH_PATTERN =
|
|
752
|
+
/\/[^"'`\s]*(?:browser[-_]?tokens?|browser[-_]?token)\b/i;
|
|
753
|
+
const GENERIC_PUBLIC_API_ENV_PATTERN =
|
|
754
|
+
/\bprocess\.env\.(?:(?:NEXT_PUBLIC|VITE|REACT_APP)_(?!CLUE_)[A-Z0-9_]*(?:API|BACKEND|BASE|URL)[A-Z0-9_]*)\b/;
|
|
740
755
|
|
|
741
756
|
const findNextLifecycleNonPublicEnvFiles = ({
|
|
742
757
|
dependencySources,
|
|
@@ -810,14 +825,83 @@ const findNextBrowserTokenProviderMissingPublicServiceKeyFiles = ({
|
|
|
810
825
|
.map((source) => source.file_path);
|
|
811
826
|
};
|
|
812
827
|
|
|
828
|
+
const findNextBrowserTokenProviderMissingPublicEndpointFiles = ({
|
|
829
|
+
dependencySources,
|
|
830
|
+
frontendSources,
|
|
831
|
+
}) => {
|
|
832
|
+
const roots = nextPackageRoots(dependencySources);
|
|
833
|
+
if (roots.length === 0) return [];
|
|
834
|
+
return frontendSources
|
|
835
|
+
.filter((source) => sourceIsUnderAnyRoot(source, roots))
|
|
836
|
+
.filter(sourceLooksLikeBrowserTokenProvider)
|
|
837
|
+
.filter(
|
|
838
|
+
(source) =>
|
|
839
|
+
!/\bprocess\.env\.NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT\b/.test(
|
|
840
|
+
source.text,
|
|
841
|
+
),
|
|
842
|
+
)
|
|
843
|
+
.map((source) => source.file_path);
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const browserTokenPathLiterals = (text) =>
|
|
847
|
+
[
|
|
848
|
+
...stripSourceNoise(text).matchAll(
|
|
849
|
+
/(["'`])([^"'`]*(?:browser[-_]?tokens?|browser[-_]?token)[^"'`]*)\1/gi,
|
|
850
|
+
),
|
|
851
|
+
].map((match) => match[2]);
|
|
852
|
+
|
|
813
853
|
const findBrowserTokenProviderWrongProxyPathFiles = (frontendSources) =>
|
|
814
854
|
frontendSources
|
|
815
855
|
.filter(sourceLooksLikeBrowserTokenProvider)
|
|
816
856
|
.filter((source) => {
|
|
817
857
|
const text = stripSourceNoise(source.text);
|
|
818
858
|
if (!/\bfetch\s*\(/.test(text)) return false;
|
|
819
|
-
|
|
820
|
-
|
|
859
|
+
const tokenPathLiterals = browserTokenPathLiterals(text).filter(
|
|
860
|
+
(literal) => BROWSER_TOKEN_PATH_PATTERN.test(literal),
|
|
861
|
+
);
|
|
862
|
+
if (tokenPathLiterals.length === 0) {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
return tokenPathLiterals.some(
|
|
866
|
+
(literal) => !literal.includes(CANONICAL_BROWSER_TOKEN_PROXY_PATH),
|
|
867
|
+
);
|
|
868
|
+
})
|
|
869
|
+
.map((source) => source.file_path);
|
|
870
|
+
|
|
871
|
+
const findBrowserTokenProviderNonClueEndpointEnvFiles = (frontendSources) =>
|
|
872
|
+
frontendSources
|
|
873
|
+
.filter(sourceLooksLikeBrowserTokenProvider)
|
|
874
|
+
.filter((source) =>
|
|
875
|
+
GENERIC_PUBLIC_API_ENV_PATTERN.test(stripSourceNoise(source.text)),
|
|
876
|
+
)
|
|
877
|
+
.map((source) => source.file_path);
|
|
878
|
+
|
|
879
|
+
const findClueInitEmptyEnvFallbackFiles = (frontendSources) =>
|
|
880
|
+
frontendSources
|
|
881
|
+
.filter(
|
|
882
|
+
(source) =>
|
|
883
|
+
sourceImportsFrontendSdk(source) ||
|
|
884
|
+
sourceLooksLikeBrowserTokenProvider(source),
|
|
885
|
+
)
|
|
886
|
+
.filter((source) =>
|
|
887
|
+
/process\.env\.NEXT_PUBLIC_CLUE_[A-Z0-9_]+\s*(?:\?\?|\|\|)\s*(["'])\1/.test(
|
|
888
|
+
stripSourceNoise(source.text),
|
|
889
|
+
),
|
|
890
|
+
)
|
|
891
|
+
.map((source) => source.file_path);
|
|
892
|
+
|
|
893
|
+
const findFrontendInitBeforeClueInitFiles = (frontendSources) =>
|
|
894
|
+
frontendSources
|
|
895
|
+
.filter((source) => sourceImportsFrontendSdk(source))
|
|
896
|
+
.filter((source) => {
|
|
897
|
+
const text = stripSourceNoise(source.text, { stripStrings: true });
|
|
898
|
+
const initializedIndex = text.search(/\binitialized\s*=\s*true\b/);
|
|
899
|
+
const clueInitIndex = text.search(/\bClueInit\s*\(/);
|
|
900
|
+
return (
|
|
901
|
+
initializedIndex >= 0 &&
|
|
902
|
+
clueInitIndex >= 0 &&
|
|
903
|
+
initializedIndex < clueInitIndex
|
|
904
|
+
);
|
|
821
905
|
})
|
|
822
906
|
.map((source) => source.file_path);
|
|
823
907
|
|
|
@@ -1001,11 +1085,22 @@ const checkSdkLifecycle = ({
|
|
|
1001
1085
|
findBrowserTokenProviderMissingServiceKeyFiles(frontendSources);
|
|
1002
1086
|
const browserTokenProviderWrongProxyPathFiles =
|
|
1003
1087
|
findBrowserTokenProviderWrongProxyPathFiles(frontendSources);
|
|
1088
|
+
const browserTokenProviderNonClueEndpointEnvFiles =
|
|
1089
|
+
findBrowserTokenProviderNonClueEndpointEnvFiles(frontendSources);
|
|
1090
|
+
const clueInitEmptyEnvFallbackFiles =
|
|
1091
|
+
findClueInitEmptyEnvFallbackFiles(frontendSources);
|
|
1092
|
+
const frontendInitBeforeClueInitFiles =
|
|
1093
|
+
findFrontendInitBeforeClueInitFiles(frontendSources);
|
|
1004
1094
|
const nextBrowserTokenProviderMissingPublicServiceKeyFiles =
|
|
1005
1095
|
findNextBrowserTokenProviderMissingPublicServiceKeyFiles({
|
|
1006
1096
|
dependencySources,
|
|
1007
1097
|
frontendSources,
|
|
1008
1098
|
});
|
|
1099
|
+
const nextBrowserTokenProviderMissingPublicEndpointFiles =
|
|
1100
|
+
findNextBrowserTokenProviderMissingPublicEndpointFiles({
|
|
1101
|
+
dependencySources,
|
|
1102
|
+
frontendSources,
|
|
1103
|
+
});
|
|
1009
1104
|
const browserTokenProxyUsingBackendServiceKeyFiles =
|
|
1010
1105
|
findBrowserTokenProxyUsingBackendServiceKeyFiles(backendSources);
|
|
1011
1106
|
const browserTokenProxyTrustingBodyOriginFiles =
|
|
@@ -1074,8 +1169,14 @@ const checkSdkLifecycle = ({
|
|
|
1074
1169
|
browserTokenProviderMissingServiceKeyFiles,
|
|
1075
1170
|
browser_token_provider_wrong_proxy_path_files:
|
|
1076
1171
|
browserTokenProviderWrongProxyPathFiles,
|
|
1172
|
+
browser_token_provider_non_clue_endpoint_env_files:
|
|
1173
|
+
browserTokenProviderNonClueEndpointEnvFiles,
|
|
1174
|
+
clue_init_empty_env_fallback_files: clueInitEmptyEnvFallbackFiles,
|
|
1175
|
+
frontend_init_before_clue_init_files: frontendInitBeforeClueInitFiles,
|
|
1077
1176
|
next_browser_token_provider_missing_public_service_key_files:
|
|
1078
1177
|
nextBrowserTokenProviderMissingPublicServiceKeyFiles,
|
|
1178
|
+
next_browser_token_provider_missing_public_endpoint_files:
|
|
1179
|
+
nextBrowserTokenProviderMissingPublicEndpointFiles,
|
|
1079
1180
|
browser_token_proxy_uses_backend_service_key_files:
|
|
1080
1181
|
browserTokenProxyUsingBackendServiceKeyFiles,
|
|
1081
1182
|
browser_token_proxy_trusts_body_origin_files:
|
|
@@ -1102,7 +1203,11 @@ const checkSdkLifecycle = ({
|
|
|
1102
1203
|
wildcardFrontendSdkDependencyFiles.length === 0 &&
|
|
1103
1204
|
browserTokenProviderMissingServiceKeyFiles.length === 0 &&
|
|
1104
1205
|
browserTokenProviderWrongProxyPathFiles.length === 0 &&
|
|
1206
|
+
browserTokenProviderNonClueEndpointEnvFiles.length === 0 &&
|
|
1207
|
+
clueInitEmptyEnvFallbackFiles.length === 0 &&
|
|
1208
|
+
frontendInitBeforeClueInitFiles.length === 0 &&
|
|
1105
1209
|
nextBrowserTokenProviderMissingPublicServiceKeyFiles.length === 0 &&
|
|
1210
|
+
nextBrowserTokenProviderMissingPublicEndpointFiles.length === 0 &&
|
|
1106
1211
|
browserTokenProxyUsingBackendServiceKeyFiles.length === 0 &&
|
|
1107
1212
|
browserTokenProxyTrustingBodyOriginFiles.length === 0 &&
|
|
1108
1213
|
browserTokenProxyTrustingBodyProjectEnvironmentFiles.length === 0 &&
|
package/src/setup-doctor.mjs
CHANGED
|
@@ -247,9 +247,11 @@ export const runSetupDoctor = async ({
|
|
|
247
247
|
optionalString(flags.get("origin")) ??
|
|
248
248
|
clientFrontendUrl ??
|
|
249
249
|
"http://localhost";
|
|
250
|
-
const browserTokenProxyUrl =
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
const browserTokenProxyUrl =
|
|
251
|
+
optionalString(flags.get("browser-token-proxy-url")) ??
|
|
252
|
+
optionalString(env.NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT) ??
|
|
253
|
+
optionalString(env.CLUE_BROWSER_TOKEN_ENDPOINT) ??
|
|
254
|
+
(clientBackendUrl ? joinUrl(clientBackendUrl, BROWSER_TOKEN_PROXY_PATH) : null);
|
|
253
255
|
const clueBrowserTokenUrl = clueApiBaseUrl
|
|
254
256
|
? joinUrl(clueApiBaseUrl, CLUE_BROWSER_TOKEN_PATH)
|
|
255
257
|
: null;
|
|
@@ -272,7 +274,11 @@ export const runSetupDoctor = async ({
|
|
|
272
274
|
requiredInputCheck({
|
|
273
275
|
id: "client_backend_browser_token_proxy",
|
|
274
276
|
missing: [
|
|
275
|
-
...(!browserTokenProxyUrl
|
|
277
|
+
...(!browserTokenProxyUrl
|
|
278
|
+
? [
|
|
279
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT or CLUE_BROWSER_TOKEN_ENDPOINT or client-backend-url",
|
|
280
|
+
]
|
|
281
|
+
: []),
|
|
276
282
|
...(!serviceKey ? ["service-key"] : []),
|
|
277
283
|
],
|
|
278
284
|
url: browserTokenProxyUrl,
|
|
@@ -424,6 +430,7 @@ export const runSetupDoctor = async ({
|
|
|
424
430
|
manifest_loaded: Boolean(manifest),
|
|
425
431
|
client_backend_url_configured: Boolean(clientBackendUrl),
|
|
426
432
|
client_frontend_url_configured: Boolean(clientFrontendUrl),
|
|
433
|
+
browser_token_proxy_url_configured: Boolean(browserTokenProxyUrl),
|
|
427
434
|
clue_api_base_url_configured: Boolean(clueApiBaseUrl),
|
|
428
435
|
project_key_configured: Boolean(projectKey),
|
|
429
436
|
environment_configured: Boolean(environment),
|
package/src/setup-help.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
AI_SETUP_CONTRACT_VERSION,
|
|
7
7
|
API_CONNECTIVITY_CONTRACT,
|
|
8
8
|
DETERMINISTIC_CONTROL_MODEL,
|
|
9
|
+
FRONTEND_ADAPTER_CONTRACT,
|
|
9
10
|
SETUP_DOCTRINE,
|
|
10
11
|
} from "./setup-ai-contract.mjs";
|
|
11
12
|
import { buildSetupDocumentationContract } from "./setup-documents.mjs";
|
|
@@ -22,6 +23,7 @@ export const buildAiSetupHelp = () => ({
|
|
|
22
23
|
setup_doctrine: SETUP_DOCTRINE,
|
|
23
24
|
deterministic_control_model: DETERMINISTIC_CONTROL_MODEL,
|
|
24
25
|
api_connectivity_contract: API_CONNECTIVITY_CONTRACT,
|
|
26
|
+
frontend_adapter_contract: FRONTEND_ADAPTER_CONTRACT,
|
|
25
27
|
agent_primary_task:
|
|
26
28
|
"Decide where to place ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout in existing repository lifecycle boundaries, then apply only those minimal Clue SDK wiring changes.",
|
|
27
29
|
implementation_workstreams: ["sdk_lifecycle_placement"],
|
|
@@ -60,8 +62,9 @@ export const buildAiSetupHelp = () => ({
|
|
|
60
62
|
"NEXT_PUBLIC_CLUE_ENVIRONMENT",
|
|
61
63
|
"NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
62
64
|
"NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
65
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
63
66
|
],
|
|
64
|
-
rule: "When the frontend framework is Next.js, browser/client code must read only these NEXT_PUBLIC_* Clue variables. Do not add process.env.CLUE_* fallbacks in client-bundled code.",
|
|
67
|
+
rule: "When the frontend framework is Next.js, browser/client code must read only these NEXT_PUBLIC_* Clue variables. Do not add process.env.CLUE_* fallbacks in client-bundled code. browserTokenProvider must call NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT, not a relative frontend-origin path or generic NEXT_PUBLIC_API_URL.",
|
|
65
68
|
},
|
|
66
69
|
backend_browser_token_proxy_env: {
|
|
67
70
|
variables: ["CLUE_API_KEY", "CLUE_API_BASE_URL"],
|
package/src/setup-prepare.mjs
CHANGED
|
@@ -16,11 +16,14 @@ const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
|
16
16
|
const DEFAULT_ENV_GUIDE_PATH = ".env.clue";
|
|
17
17
|
const BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
|
|
18
18
|
const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
|
|
19
|
+
const BROWSER_TOKEN_PROXY_PATH =
|
|
20
|
+
API_CONNECTIVITY_CONTRACT.hops.client_backend_browser_token_proxy.path;
|
|
19
21
|
const FRONTEND_PUBLIC_ENV_NAMES = [
|
|
20
22
|
"CLUE_INGEST_ENDPOINT",
|
|
21
23
|
"CLUE_PROJECT_KEY",
|
|
22
24
|
"CLUE_ENVIRONMENT",
|
|
23
25
|
"CLUE_SERVICE_KEY",
|
|
26
|
+
"CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
24
27
|
];
|
|
25
28
|
const BACKEND_RUNTIME_ENV_NAMES = [
|
|
26
29
|
"CLUE_SERVICE_KEY",
|
|
@@ -187,6 +190,7 @@ const frontendEnvName = ({ target, name }) =>
|
|
|
187
190
|
: name;
|
|
188
191
|
|
|
189
192
|
const buildServiceEnvBlock = ({
|
|
193
|
+
browserTokenEndpoint,
|
|
190
194
|
target,
|
|
191
195
|
setupContext,
|
|
192
196
|
includeBrowserTokenProxyConfig = false,
|
|
@@ -211,6 +215,12 @@ const buildServiceEnvBlock = ({
|
|
|
211
215
|
value: target.service_key,
|
|
212
216
|
},
|
|
213
217
|
];
|
|
218
|
+
if (target.kind === "frontend") {
|
|
219
|
+
variables.push({
|
|
220
|
+
name: frontendEnvName({ target, name: "CLUE_BROWSER_TOKEN_ENDPOINT" }),
|
|
221
|
+
value: browserTokenEndpoint,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
214
224
|
if (target.kind === "backend") {
|
|
215
225
|
variables.push({ name: "CLUE_API_KEY", value: setupContext.clue_api_key });
|
|
216
226
|
if (includeBrowserTokenProxyConfig) {
|
|
@@ -266,6 +276,14 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
|
|
|
266
276
|
const includeBrowserTokenProxyConfig = watchTargets.some(
|
|
267
277
|
(target) => target.kind === "frontend",
|
|
268
278
|
);
|
|
279
|
+
const backendTarget = watchTargets.find((target) => target.kind === "backend");
|
|
280
|
+
const backendUrlCandidate =
|
|
281
|
+
optionalString(backendTarget?.local_url_candidates?.[0]) ??
|
|
282
|
+
"http://<client-backend-url>";
|
|
283
|
+
const browserTokenEndpoint = buildEndpoint(
|
|
284
|
+
backendUrlCandidate,
|
|
285
|
+
BROWSER_TOKEN_PROXY_PATH,
|
|
286
|
+
);
|
|
269
287
|
return {
|
|
270
288
|
status: "ready",
|
|
271
289
|
env_file_path: DEFAULT_ENV_GUIDE_PATH,
|
|
@@ -277,6 +295,7 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
|
|
|
277
295
|
service_key: target.service_key,
|
|
278
296
|
env_file_candidates: envFileCandidates(target),
|
|
279
297
|
env_block: buildServiceEnvBlock({
|
|
298
|
+
browserTokenEndpoint,
|
|
280
299
|
target,
|
|
281
300
|
setupContext,
|
|
282
301
|
includeBrowserTokenProxyConfig,
|
package/src/setup-tool.mjs
CHANGED
|
@@ -322,14 +322,19 @@ const skillBody = (name, { documentsUrl } = {}) => {
|
|
|
322
322
|
"Delete the temporary lifecycle plan file after applying it unless the user explicitly asks to keep it for review.",
|
|
323
323
|
"Use environment variable names for Clue configuration values; do not paste project keys or API keys into code.",
|
|
324
324
|
`For local env files, use the service-specific env blocks written to \`.env.clue\` by \`${clueCliCommand("setup")}\`; do not ask the user to guess \`CLUE_SERVICE_KEY\`.`,
|
|
325
|
-
"For Next.js browser/client code, use only `NEXT_PUBLIC_CLUE_PROJECT_KEY`, `NEXT_PUBLIC_CLUE_ENVIRONMENT`, `NEXT_PUBLIC_CLUE_SERVICE_KEY`, and `
|
|
325
|
+
"For Next.js browser/client code, use only `NEXT_PUBLIC_CLUE_PROJECT_KEY`, `NEXT_PUBLIC_CLUE_ENVIRONMENT`, `NEXT_PUBLIC_CLUE_SERVICE_KEY`, `NEXT_PUBLIC_CLUE_INGEST_ENDPOINT`, and `NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT` from the frontend `.env.local` block.",
|
|
326
326
|
"Do not read `process.env.CLUE_PROJECT_KEY`, `process.env.CLUE_ENVIRONMENT`, `process.env.CLUE_SERVICE_KEY`, or `process.env.CLUE_INGEST_ENDPOINT` in Next.js browser/client code, and do not add non-public `CLUE_*` fallbacks there.",
|
|
327
|
+
"Frontend SDK adapter code is contract-owned Clue setup wiring. The AI may choose the existing import/mount point, but must not invent token URL, env, or initialization semantics.",
|
|
328
|
+
"For Next.js frontend adapters, read the full customer-backend browser-token proxy URL from `NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT`. Do not derive it from `NEXT_PUBLIC_API_URL`, generic app API env names, detected backend ports, or relative frontend-origin paths.",
|
|
329
|
+
"Do not mix stale browser-token paths such as `/api/clue/browser-token`, `/clue/browser-tokens`, or `/browser-tokens` with the canonical `/api/v1/clue/browser-tokens` path.",
|
|
330
|
+
"Do not call `ClueInit` with empty-string fallbacks for required `NEXT_PUBLIC_CLUE_*` values. If required Clue env is absent, skip initialization and report the missing env names.",
|
|
331
|
+
"If a singleton guard is used, do not set `initialized = true` before `ClueInit` has actually been called with required config present.",
|
|
327
332
|
"For non-Next.js browser code, use the exact frontend env names written in `.env.clue` for that service instead of inventing a framework-specific prefix.",
|
|
328
333
|
"Never put `CLUE_API_KEY` in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
329
334
|
"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.",
|
|
330
335
|
"Configure frontend `ClueInit` with `browserTokenProvider` that calls the local backend token endpoint and returns the token string.",
|
|
331
336
|
"The local backend token endpoint is part of the customer app, not the Clue API. Place it under a Clue-reserved local route such as `/api/v1/clue/browser-tokens`; do not use a generic path such as `/browser-tokens` that could be confused with product behavior. It must call Clue server-side at `/api/v1/ingest/browser-tokens`.",
|
|
332
|
-
"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`.",
|
|
337
|
+
"The frontend `browserTokenProvider` must call `NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT` and 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`.",
|
|
333
338
|
"The browser token request must include the frontend service key used by `ClueInit`. Project key and environment may be included only as public consistency hints; the backend must use server configuration or validate them against server configuration before calling Clue.",
|
|
334
339
|
"The backend browser token proxy must derive origin from trusted request headers or server request metadata. Do not forward `origin`, `projectKey`, or `environment` from JSON/body payload fields under server `CLUE_API_KEY`.",
|
|
335
340
|
"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`.",
|
|
@@ -367,12 +372,17 @@ const skillBody = (name, { documentsUrl } = {}) => {
|
|
|
367
372
|
"Reject backend setup when backend routes exist but no backend Clue SDK dependency/import/init was added.",
|
|
368
373
|
"Reject awaited lifecycle calls that can block host service behavior.",
|
|
369
374
|
"Reject browser token proxy code that forwards origin, projectKey, or environment from request JSON/body under server `CLUE_API_KEY`.",
|
|
375
|
+
"Reject frontend browser token providers that derive the Clue proxy URL from `NEXT_PUBLIC_API_URL`, generic app API env names, detected backend ports, or non-Clue routing assumptions.",
|
|
376
|
+
"Reject frontend adapters that mix stale browser-token paths such as `/api/clue/browser-token`, `/clue/browser-tokens`, or `/browser-tokens` with the canonical `/api/v1/clue/browser-tokens` path.",
|
|
377
|
+
"Reject frontend adapters that set `initialized = true` before calling `ClueInit`, or pass empty-string fallbacks for required `NEXT_PUBLIC_CLUE_*` values into `ClueInit`.",
|
|
378
|
+
"Audit the setup diff against the Clue setup contract even when the code was written by another agent or an earlier pass. Ownership of authorship is irrelevant to approval.",
|
|
370
379
|
"Reject setup that covers only one login path when multiple login success paths are clearly present.",
|
|
371
380
|
"Reject ClueInit inside React component lifecycle hooks, page components, sidebars, login/register success callbacks, or any repeated user interaction path.",
|
|
372
381
|
"Reject broad ClueTrack instrumentation and DOM clue tags.",
|
|
373
382
|
"Reject ClueTrack instrumentation unless the user explicitly requested product event tracking.",
|
|
374
383
|
"Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables.",
|
|
375
384
|
"Reject Clue SDK dependency entries that use `*` or `latest` instead of a concrete published version or package-manager-resolved semver range.",
|
|
385
|
+
"Reject Next.js browser token providers that do not read `NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT`.",
|
|
376
386
|
"Confirm no project key, API key, secret, or env value appears in diff or report.",
|
|
377
387
|
"Confirm lifecycle insertions are minimal and reviewable.",
|
|
378
388
|
"Reject whitespace-only edits, import sorting, formatter churn, or comment/style cleanup outside the exact Clue SDK wiring lines.",
|