@clue-ai/cli 0.0.13 → 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.
@@ -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.v2";
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",
@@ -35,14 +35,20 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
35
35
  "add the real `@clue-ai/browser-sdk` dependency",
36
36
  "Do not invent `clue-js-sdk`",
37
37
  "The implementation scope is only ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout placement",
38
- "CLUE_API_BASE_URL` is for Clue CLI and semantic snapshot CI configuration",
38
+ "For Next.js browser/client code, use only `NEXT_PUBLIC_CLUE_PROJECT_KEY`",
39
+ "Do not read `process.env.CLUE_PROJECT_KEY`",
40
+ "CLUE_API_BASE_URL` is not part of backend SDK initialization",
41
+ "Do not add `@clue-ai/browser-sdk` or backend SDK dependencies with `*` or `latest`",
39
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`",
40
45
  "For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
41
46
  ],
42
47
  "clue-setup-audit": [
43
48
  "Reject wrong SDK package names",
44
49
  "Reject Django SDK setup when `clue-django-sdk` installability has not been verified",
45
50
  "Reject ClueTrack instrumentation unless the user explicitly requested product event tracking",
51
+ "Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables",
46
52
  "Reject whitespace-only edits, import sorting, formatter churn",
47
53
  "Reject unrelated refactors, renames, file moves",
48
54
  "Execution agents must not approve, certify, or mark their own work complete",
@@ -225,6 +231,43 @@ const validateSetupManifestContract = (manifest) => {
225
231
  "required_final_verification.local_event_delivery must report user_verification_pending without user evidence",
226
232
  );
227
233
  }
234
+ const watchTargets = Array.isArray(
235
+ manifest.lifecycle_verification?.watch_targets,
236
+ )
237
+ ? manifest.lifecycle_verification.watch_targets
238
+ : [];
239
+ const hasNextFrontend = watchTargets.some(
240
+ (target) => target?.kind === "frontend" && target?.framework === "nextjs",
241
+ );
242
+ const hasFrontend = watchTargets.some((target) => target?.kind === "frontend");
243
+ const frontendRuntime = Array.isArray(
244
+ manifest.required_env_scopes?.frontend_runtime,
245
+ )
246
+ ? manifest.required_env_scopes.frontend_runtime
247
+ : [];
248
+ const backendRuntime = Array.isArray(
249
+ manifest.required_env_scopes?.backend_runtime,
250
+ )
251
+ ? manifest.required_env_scopes.backend_runtime
252
+ : [];
253
+ if (
254
+ hasNextFrontend &&
255
+ ![
256
+ "NEXT_PUBLIC_CLUE_PROJECT_KEY",
257
+ "NEXT_PUBLIC_CLUE_ENVIRONMENT",
258
+ "NEXT_PUBLIC_CLUE_SERVICE_KEY",
259
+ "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
260
+ ].every((name) => frontendRuntime.includes(name))
261
+ ) {
262
+ findings.push(
263
+ "Next.js frontend runtime env must use NEXT_PUBLIC_CLUE_* names",
264
+ );
265
+ }
266
+ if (hasFrontend && !backendRuntime.includes("CLUE_API_BASE_URL")) {
267
+ findings.push(
268
+ "backend_runtime must include CLUE_API_BASE_URL when a frontend browser token proxy can be required",
269
+ );
270
+ }
228
271
  return {
229
272
  checked: true,
230
273
  findings,
@@ -294,7 +337,9 @@ const readAllowedSourceText = async ({
294
337
 
295
338
  const readDependencyText = async ({ repoRoot, roots }) => {
296
339
  const expandedRoots = roots.flatMap((root) =>
297
- root.endsWith("/src") ? [root, dirname(root)] : [root],
340
+ root.endsWith("/src") || root.endsWith("/app")
341
+ ? [root, dirname(root)]
342
+ : [root],
298
343
  );
299
344
  const candidatePaths = [
300
345
  ...DEPENDENCY_FILE_CANDIDATES,
@@ -372,6 +417,57 @@ const packageJsonDependencyNames = (text) => {
372
417
  }
373
418
  };
374
419
 
420
+ const packageJsonDependencies = (text) => {
421
+ try {
422
+ const parsed = JSON.parse(text);
423
+ return [
424
+ "dependencies",
425
+ "devDependencies",
426
+ "optionalDependencies",
427
+ "peerDependencies",
428
+ ].reduce((result, field) => {
429
+ if (parsed && typeof parsed[field] === "object" && parsed[field] !== null) {
430
+ for (const [name, version] of Object.entries(parsed[field])) {
431
+ if (typeof version === "string") {
432
+ result.set(name, version);
433
+ }
434
+ }
435
+ }
436
+ return result;
437
+ }, new Map());
438
+ } catch {
439
+ return new Map();
440
+ }
441
+ };
442
+
443
+ const packageJsonHasDependency = (text, packageName) =>
444
+ packageJsonDependencies(text).has(packageName);
445
+
446
+ const packageJsonDependencyVersion = (text, packageName) =>
447
+ packageJsonDependencies(text).get(packageName) ?? null;
448
+
449
+ const packageJsonSourcesWithDependency = (dependencySources, packageName) =>
450
+ dependencySources.filter(
451
+ (source) =>
452
+ source.file_path.endsWith("package.json") &&
453
+ packageJsonHasDependency(source.text, packageName),
454
+ );
455
+
456
+ const nextPackageRoots = (dependencySources) =>
457
+ packageJsonSourcesWithDependency(dependencySources, "next").map((source) =>
458
+ dirname(source.file_path),
459
+ );
460
+
461
+ const sourceIsUnderAnyRoot = (source, roots) =>
462
+ roots.some((root) => {
463
+ const normalizedRoot = root.replace(/\/+$/, "");
464
+ return (
465
+ normalizedRoot === "." ||
466
+ normalizedRoot === "" ||
467
+ startsWithRoot(source.file_path, normalizedRoot)
468
+ );
469
+ });
470
+
375
471
  const dependencySourceHasPackage = (source, packageName) => {
376
472
  if (source.file_path.endsWith("package.json")) {
377
473
  return packageJsonDependencyNames(source.text).includes(packageName);
@@ -631,6 +727,106 @@ const findWrongFrontendSdkPackages = ({ sources, dependencySources }) => {
631
727
  );
632
728
  };
633
729
 
730
+ const NEXT_PUBLIC_CLUE_NAMES = [
731
+ "CLUE_PROJECT_KEY",
732
+ "CLUE_ENVIRONMENT",
733
+ "CLUE_SERVICE_KEY",
734
+ "CLUE_INGEST_ENDPOINT",
735
+ ];
736
+
737
+ const findNextLifecycleNonPublicEnvFiles = ({
738
+ dependencySources,
739
+ frontendSources,
740
+ }) => {
741
+ const roots = nextPackageRoots(dependencySources);
742
+ if (roots.length === 0) return [];
743
+ return frontendSources
744
+ .filter((source) => sourceIsUnderAnyRoot(source, roots))
745
+ .filter(
746
+ (source) =>
747
+ sourceImportsFrontendSdk(source) ||
748
+ findLifecycleCallApiNames(
749
+ stripSourceNoise(source.text, { stripStrings: true }),
750
+ ).length > 0,
751
+ )
752
+ .filter((source) =>
753
+ NEXT_PUBLIC_CLUE_NAMES.some((name) =>
754
+ new RegExp(`process\\.env\\.${name}\\b`).test(source.text),
755
+ ),
756
+ )
757
+ .map((source) => source.file_path);
758
+ };
759
+
760
+ const findWildcardFrontendSdkDependencyFiles = (dependencySources) =>
761
+ packageJsonSourcesWithDependency(dependencySources, FRONTEND_SDK_PACKAGE)
762
+ .filter((source) => {
763
+ const version = packageJsonDependencyVersion(
764
+ source.text,
765
+ FRONTEND_SDK_PACKAGE,
766
+ );
767
+ return version === "*" || version === "latest";
768
+ })
769
+ .map((source) => source.file_path);
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
+
634
830
  const backendSdkSpec = (framework) =>
635
831
  BACKEND_SDK_BY_FRAMEWORK[String(framework ?? "").toLowerCase()] ?? {
636
832
  packages: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
@@ -743,6 +939,21 @@ const checkSdkLifecycle = ({
743
939
  sources: frontendSources,
744
940
  dependencySources,
745
941
  });
942
+ const nextLifecycleNonPublicEnvFiles = findNextLifecycleNonPublicEnvFiles({
943
+ dependencySources,
944
+ frontendSources,
945
+ });
946
+ const wildcardFrontendSdkDependencyFiles =
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);
746
957
  const backendIdentityRequired =
747
958
  backendPresent &&
748
959
  /\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
@@ -799,6 +1010,14 @@ const checkSdkLifecycle = ({
799
1010
  lifecycle_files_without_verified_sdk:
800
1011
  frontendLifecycleFilesWithoutVerifiedSdk,
801
1012
  wrong_sdk_packages: wrongFrontendSdkPackages,
1013
+ next_lifecycle_non_public_env_files: nextLifecycleNonPublicEnvFiles,
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,
802
1021
  },
803
1022
  has_noop_wrapper: noOpPattern.test(combined),
804
1023
  component_lifecycle_init_files: componentLifecycleInitFiles,
@@ -815,6 +1034,11 @@ const checkSdkLifecycle = ({
815
1034
  frontendSdkPresent &&
816
1035
  frontendLifecycleFilesWithoutVerifiedSdk.length === 0 &&
817
1036
  wrongFrontendSdkPackages.length === 0 &&
1037
+ nextLifecycleNonPublicEnvFiles.length === 0 &&
1038
+ wildcardFrontendSdkDependencyFiles.length === 0 &&
1039
+ browserTokenProviderMissingServiceKeyFiles.length === 0 &&
1040
+ nextBrowserTokenProviderMissingPublicServiceKeyFiles.length === 0 &&
1041
+ browserTokenProxyUsingBackendServiceKeyFiles.length === 0 &&
818
1042
  !noOpPattern.test(combined) &&
819
1043
  componentLifecycleInitFiles.length === 0 &&
820
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
+
@@ -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.v2";
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",
@@ -43,6 +44,22 @@ export const buildAiSetupHelp = () => ({
43
44
  "whitespace-only edits, import sorting, formatter churn, or comment/style cleanup outside the exact Clue SDK wiring lines",
44
45
  ],
45
46
  },
47
+ environment_contract: {
48
+ nextjs_frontend_client_env: {
49
+ variables: [
50
+ "NEXT_PUBLIC_CLUE_PROJECT_KEY",
51
+ "NEXT_PUBLIC_CLUE_ENVIRONMENT",
52
+ "NEXT_PUBLIC_CLUE_SERVICE_KEY",
53
+ "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
54
+ ],
55
+ 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.",
56
+ },
57
+ backend_browser_token_proxy_env: {
58
+ variables: ["CLUE_API_KEY", "CLUE_API_BASE_URL"],
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.",
60
+ },
61
+ },
62
+ documentation_contract: buildSetupDocumentationContract(),
46
63
  setup_watch: {
47
64
  owner: "user",
48
65
  ai_agent_must_run: false,
@@ -8,12 +8,26 @@ 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";
14
15
  const DEFAULT_ENV_GUIDE_PATH = ".env.clue";
15
16
  const BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
16
17
  const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
18
+ const FRONTEND_PUBLIC_ENV_NAMES = [
19
+ "CLUE_INGEST_ENDPOINT",
20
+ "CLUE_PROJECT_KEY",
21
+ "CLUE_ENVIRONMENT",
22
+ "CLUE_SERVICE_KEY",
23
+ ];
24
+ const BACKEND_RUNTIME_ENV_NAMES = [
25
+ "CLUE_SERVICE_KEY",
26
+ "CLUE_PROJECT_KEY",
27
+ "CLUE_ENVIRONMENT",
28
+ "CLUE_INGEST_ENDPOINT",
29
+ "CLUE_API_KEY",
30
+ ];
17
31
  const AI_PROVIDER_GUIDES = {
18
32
  codex: {
19
33
  provider: "openai",
@@ -154,6 +168,7 @@ const aiProviderGuideForTarget = (target) =>
154
168
  const setupContextFromInput = (input = {}) => ({
155
169
  clue_api_key: optionalString(input.clueApiKey),
156
170
  clue_api_base_url: optionalString(input.clueApiBaseUrl),
171
+ documents_url: optionalString(input.documentsUrl),
157
172
  project_key: optionalString(input.projectKey),
158
173
  environment: optionalString(input.environment),
159
174
  });
@@ -165,24 +180,67 @@ const envFileCandidates = (target) => {
165
180
  return [".env"];
166
181
  };
167
182
 
168
- const buildServiceEnvBlock = ({ target, setupContext }) => {
183
+ const frontendEnvName = ({ target, name }) =>
184
+ target.kind === "frontend" && target.framework === "nextjs"
185
+ ? `NEXT_PUBLIC_${name}`
186
+ : name;
187
+
188
+ const buildServiceEnvBlock = ({
189
+ target,
190
+ setupContext,
191
+ includeBrowserTokenProxyConfig = false,
192
+ }) => {
169
193
  const ingestPath =
170
194
  target.kind === "frontend" ? BROWSER_INGEST_PATH : BACKEND_INGEST_PATH;
171
195
  const variables = [
172
196
  {
173
- name: "CLUE_INGEST_ENDPOINT",
197
+ name: frontendEnvName({ target, name: "CLUE_INGEST_ENDPOINT" }),
174
198
  value: buildEndpoint(setupContext.clue_api_base_url, ingestPath),
175
199
  },
176
- { name: "CLUE_PROJECT_KEY", value: setupContext.project_key },
177
- { name: "CLUE_ENVIRONMENT", value: setupContext.environment },
178
- { name: "CLUE_SERVICE_KEY", value: target.service_key },
200
+ {
201
+ name: frontendEnvName({ target, name: "CLUE_PROJECT_KEY" }),
202
+ value: setupContext.project_key,
203
+ },
204
+ {
205
+ name: frontendEnvName({ target, name: "CLUE_ENVIRONMENT" }),
206
+ value: setupContext.environment,
207
+ },
208
+ {
209
+ name: frontendEnvName({ target, name: "CLUE_SERVICE_KEY" }),
210
+ value: target.service_key,
211
+ },
179
212
  ];
180
213
  if (target.kind === "backend") {
181
214
  variables.push({ name: "CLUE_API_KEY", value: setupContext.clue_api_key });
215
+ if (includeBrowserTokenProxyConfig) {
216
+ variables.push({
217
+ name: "CLUE_API_BASE_URL",
218
+ value: setupContext.clue_api_base_url,
219
+ });
220
+ }
182
221
  }
183
222
  return variables.map(({ name, value }) => `${name}=${value}`).join("\n");
184
223
  };
185
224
 
225
+ const requiredFrontendEnvNames = (watchTargets) => [
226
+ ...new Set(
227
+ watchTargets
228
+ .filter((target) => target.kind === "frontend")
229
+ .flatMap((target) =>
230
+ FRONTEND_PUBLIC_ENV_NAMES.map((name) =>
231
+ frontendEnvName({ target, name }),
232
+ ),
233
+ ),
234
+ ),
235
+ ];
236
+
237
+ const requiredBackendEnvNames = (watchTargets) => [
238
+ ...BACKEND_RUNTIME_ENV_NAMES,
239
+ ...(watchTargets.some((target) => target.kind === "frontend")
240
+ ? ["CLUE_API_BASE_URL"]
241
+ : []),
242
+ ];
243
+
186
244
  const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
187
245
  const missingFlags = [
188
246
  ["clue_api_key", "--clue-api-key"],
@@ -204,6 +262,9 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
204
262
 
205
263
  const watchTargets = manifest.lifecycle_verification.watch_targets;
206
264
  const aiProviderGuide = aiProviderGuideForTarget(manifest.target);
265
+ const includeBrowserTokenProxyConfig = watchTargets.some(
266
+ (target) => target.kind === "frontend",
267
+ );
207
268
  return {
208
269
  status: "ready",
209
270
  env_file_path: DEFAULT_ENV_GUIDE_PATH,
@@ -214,7 +275,11 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
214
275
  root_path: target.root_path,
215
276
  service_key: target.service_key,
216
277
  env_file_candidates: envFileCandidates(target),
217
- env_block: buildServiceEnvBlock({ target, setupContext }),
278
+ env_block: buildServiceEnvBlock({
279
+ target,
280
+ setupContext,
281
+ includeBrowserTokenProxyConfig,
282
+ }),
218
283
  })),
219
284
  ci_github: {
220
285
  secrets: [
@@ -323,10 +388,22 @@ export const runSetupPrepare = async ({
323
388
  const detection = await runSetupDetect({ repoRoot: resolvedRepoRoot });
324
389
  const { candidate, blockers } = firstCandidateOrBlocker(detection);
325
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
+ ];
326
399
  const manifest = {
327
400
  status: "blocked",
328
401
  target,
329
402
  skill_root: skillRoot,
403
+ documentation: buildSetupDocumentationContract({
404
+ documentsUrl: setupContext.documents_url,
405
+ frameworks: detectedFrameworks,
406
+ }),
330
407
  blockers,
331
408
  detection,
332
409
  ai_next_scope: "blocked_until_backend_routes_are_detected",
@@ -371,6 +448,16 @@ export const runSetupPrepare = async ({
371
448
  repoRoot: resolvedRepoRoot,
372
449
  request,
373
450
  });
451
+ const watchTargets = buildWatchTargets(detection, candidate);
452
+ const detectedFrameworks = [
453
+ candidate.framework,
454
+ ...watchTargets.map((target) => target.framework),
455
+ ];
456
+ const frontendRuntimeEnvNames = requiredFrontendEnvNames(watchTargets);
457
+ const backendRuntimeEnvNames = requiredBackendEnvNames(watchTargets);
458
+ const serviceRuntimeEnvNames = [
459
+ ...new Set([...frontendRuntimeEnvNames, ...backendRuntimeEnvNames]),
460
+ ];
374
461
 
375
462
  const manifest = {
376
463
  status: "ready_for_ai",
@@ -381,10 +468,19 @@ export const runSetupPrepare = async ({
381
468
  backend_root_path: candidate.backend_root_path,
382
469
  service_key: candidate.service_key,
383
470
  },
471
+ documentation: buildSetupDocumentationContract({
472
+ documentsUrl: setupContext.documents_url,
473
+ framework: candidate.framework,
474
+ frameworks: detectedFrameworks,
475
+ }),
384
476
  service_identity: {
385
477
  canonical_field: "service_key",
386
478
  backend_env_name: "CLUE_SERVICE_KEY",
387
- frontend_env_name: "CLUE_SERVICE_KEY",
479
+ frontend_env_name: "framework_specific",
480
+ frontend_env_names_by_framework: {
481
+ nextjs: "NEXT_PUBLIC_CLUE_SERVICE_KEY",
482
+ default: "CLUE_SERVICE_KEY",
483
+ },
388
484
  producer_id_derivation: "producer_id defaults to service_key",
389
485
  },
390
486
  cli_invocation: CLUE_CLI_INVOCATION_CONTRACT,
@@ -411,7 +507,7 @@ export const runSetupPrepare = async ({
411
507
  watch_target_format:
412
508
  "frontend:<service-key>[init,identify,set-account,logout,event-sent]=<frontend-url>,backend:<service-key>[init,identify,set-account,logout,event-sent]=<backend-url>",
413
509
  rule: "setup-watch --local uses the structured watch_targets below, but it is user-operated verification. AI implementation agents must not run setup-watch automatically.",
414
- watch_targets: buildWatchTargets(detection, candidate),
510
+ watch_targets: watchTargets,
415
511
  },
416
512
  artifacts: {
417
513
  ci_workflow_path: workflow.ci_workflow_path,
@@ -469,24 +565,21 @@ export const runSetupPrepare = async ({
469
565
  },
470
566
  ],
471
567
  required_env_names: [
472
- "CLUE_SERVICE_KEY",
473
- "CLUE_API_KEY",
474
- "CLUE_AI_PROVIDER",
475
- "CLUE_AI_PROVIDER_API_KEY",
476
- "CLUE_AI_MODEL",
477
- "CLUE_PROJECT_KEY",
478
- "CLUE_ENVIRONMENT",
479
- "CLUE_INGEST_ENDPOINT",
480
- "CLUE_API_BASE_URL",
481
- ],
482
- required_env_scopes: {
483
- service_runtime: [
484
- "CLUE_SERVICE_KEY",
568
+ ...new Set([
569
+ ...serviceRuntimeEnvNames,
570
+ "CLUE_API_KEY",
571
+ "CLUE_AI_PROVIDER",
572
+ "CLUE_AI_PROVIDER_API_KEY",
573
+ "CLUE_AI_MODEL",
485
574
  "CLUE_PROJECT_KEY",
486
575
  "CLUE_ENVIRONMENT",
487
- "CLUE_INGEST_ENDPOINT",
488
- "CLUE_API_KEY",
489
- ],
576
+ "CLUE_API_BASE_URL",
577
+ ]),
578
+ ],
579
+ required_env_scopes: {
580
+ service_runtime: serviceRuntimeEnvNames,
581
+ frontend_runtime: frontendRuntimeEnvNames,
582
+ backend_runtime: backendRuntimeEnvNames,
490
583
  github_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
491
584
  github_variables: [
492
585
  "CLUE_PROJECT_KEY",