@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clue-ai/cli",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clue-ai": "bin/clue-cli.mjs"
@@ -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 NEXT_PUBLIC_CLUE_INGEST_ENDPOINT from the frontend .env.local block.",
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,
@@ -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.api-connectivity-contract.v6";
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
  ];
@@ -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
- "The frontend `browserTokenProvider` must send the same service key used by `ClueInit`",
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
- if (/\/api\/v1\/clue\/browser-tokens\b/.test(text)) return false;
820
- return /browser[-_]?tokens?|browser[-_]?token/i.test(text);
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 &&
@@ -247,9 +247,11 @@ export const runSetupDoctor = async ({
247
247
  optionalString(flags.get("origin")) ??
248
248
  clientFrontendUrl ??
249
249
  "http://localhost";
250
- const browserTokenProxyUrl = clientBackendUrl
251
- ? joinUrl(clientBackendUrl, BROWSER_TOKEN_PROXY_PATH)
252
- : null;
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 ? ["client-backend-url"] : []),
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),
@@ -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"],
@@ -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,
@@ -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 `NEXT_PUBLIC_CLUE_INGEST_ENDPOINT` from the frontend `.env.local` block.",
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.",