@clue-ai/cli 0.0.17 → 0.0.19

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.
@@ -39,11 +39,18 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
39
39
  "For Next.js browser/client code, use only `NEXT_PUBLIC_CLUE_PROJECT_KEY`",
40
40
  "Do not read `process.env.CLUE_PROJECT_KEY`",
41
41
  "CLUE_API_BASE_URL` is not part of backend SDK initialization",
42
- "Do not add `@clue-ai/browser-sdk` or backend SDK dependencies with `*` or `latest`",
42
+ "Install Clue SDK dependencies through the latest channel",
43
+ "`@clue-ai/browser-sdk` must use `latest`",
44
+ "Python backend SDK dependencies must not be pinned",
43
45
  "Whitespace-only changes are allowed only on lines directly changed for Clue SDK wiring",
44
46
  "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`",
47
+ "send the same service key used by `ClueInit`",
48
+ "NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
46
49
  "Do not forward `origin`, `projectKey`, or `environment` from JSON/body payload fields under server `CLUE_API_KEY`",
50
+ "Frontend SDK adapter code is contract-owned Clue setup wiring",
51
+ "Do not derive it from `NEXT_PUBLIC_API_URL`",
52
+ "Do not call `ClueInit` with empty-string fallbacks",
53
+ "do not set `initialized = true` before `ClueInit`",
47
54
  "For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
48
55
  ],
49
56
  "clue-setup-audit": [
@@ -52,6 +59,11 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
52
59
  "Reject ClueTrack instrumentation unless the user explicitly requested product event tracking",
53
60
  "Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables",
54
61
  "Reject browser token proxy code that forwards origin, projectKey, or environment from request JSON/body under server `CLUE_API_KEY`",
62
+ "Reject frontend browser token providers that derive the Clue proxy URL from `NEXT_PUBLIC_API_URL`",
63
+ "Reject frontend adapters that mix stale browser-token paths",
64
+ "Reject frontend adapters that set `initialized = true` before calling `ClueInit`",
65
+ "Reject Clue SDK dependency entries that pin stale fixed versions",
66
+ "Audit the setup diff against the Clue setup contract even when the code was written by another agent",
55
67
  "Reject whitespace-only edits, import sorting, formatter churn",
56
68
  "Reject unrelated refactors, renames, file moves",
57
69
  "Execution agents must not approve, certify, or mark their own work complete",
@@ -138,6 +150,12 @@ const DEPENDENCY_FILE_CANDIDATES = [
138
150
  "poetry.lock",
139
151
  "Pipfile",
140
152
  ];
153
+ const PYTHON_BACKEND_DEPENDENCY_DECLARATION_FILES = [
154
+ "requirements.txt",
155
+ "requirements-dev.txt",
156
+ "pyproject.toml",
157
+ "Pipfile",
158
+ ];
141
159
  const exists = async (path) => {
142
160
  try {
143
161
  await access(path);
@@ -385,6 +403,20 @@ const setupSourcePaths = async ({ repoRoot, request, includeFrontend }) => {
385
403
  return existing.length ? existing : requested;
386
404
  };
387
405
 
406
+ const setupBackendRootPaths = (request) => {
407
+ const configuredRoots = [
408
+ ...(Array.isArray(request?.backend_root_paths)
409
+ ? request.backend_root_paths
410
+ : []),
411
+ request?.backend_root_path,
412
+ ]
413
+ .filter((root) => typeof root === "string" && root.trim())
414
+ .map((root) => root.trim());
415
+ return configuredRoots.length
416
+ ? [...new Set(configuredRoots)]
417
+ : (request?.allowed_source_paths ?? []);
418
+ };
419
+
388
420
  const secretLeakPatterns = [
389
421
  /pk_(live|test)_[A-Za-z0-9_-]+/,
390
422
  /sk_(live|test)_[A-Za-z0-9_-]+/,
@@ -736,7 +768,13 @@ const NEXT_PUBLIC_CLUE_NAMES = [
736
768
  "CLUE_ENVIRONMENT",
737
769
  "CLUE_SERVICE_KEY",
738
770
  "CLUE_INGEST_ENDPOINT",
771
+ "CLUE_BROWSER_TOKEN_ENDPOINT",
739
772
  ];
773
+ const CANONICAL_BROWSER_TOKEN_PROXY_PATH = "/api/v1/clue/browser-tokens";
774
+ const BROWSER_TOKEN_PATH_PATTERN =
775
+ /\/[^"'`\s]*(?:browser[-_]?tokens?|browser[-_]?token)\b/i;
776
+ const GENERIC_PUBLIC_API_ENV_PATTERN =
777
+ /\bprocess\.env\.(?:(?:NEXT_PUBLIC|VITE|REACT_APP)_(?!CLUE_)[A-Z0-9_]*(?:API|BACKEND|BASE|URL)[A-Z0-9_]*)\b/;
740
778
 
741
779
  const findNextLifecycleNonPublicEnvFiles = ({
742
780
  dependencySources,
@@ -761,17 +799,71 @@ const findNextLifecycleNonPublicEnvFiles = ({
761
799
  .map((source) => source.file_path);
762
800
  };
763
801
 
764
- const findWildcardFrontendSdkDependencyFiles = (dependencySources) =>
802
+ const findNonLatestFrontendSdkDependencyFiles = (dependencySources) =>
765
803
  packageJsonSourcesWithDependency(dependencySources, FRONTEND_SDK_PACKAGE)
766
804
  .filter((source) => {
767
805
  const version = packageJsonDependencyVersion(
768
806
  source.text,
769
807
  FRONTEND_SDK_PACKAGE,
770
808
  );
771
- return version === "*" || version === "latest";
809
+ return version !== "latest";
772
810
  })
773
811
  .map((source) => source.file_path);
774
812
 
813
+ const sourceIsPythonBackendDependencyDeclaration = (source) =>
814
+ PYTHON_BACKEND_DEPENDENCY_DECLARATION_FILES.some((fileName) =>
815
+ source.file_path.endsWith(fileName),
816
+ );
817
+
818
+ const backendSdkDependencyLineHasPinnedVersion = (line, packageName) => {
819
+ const stripped = line.trim();
820
+ if (!stripped || stripped.startsWith("#")) return false;
821
+ const withoutInlineComment = stripped.replace(/\s+#.*$/, "");
822
+ const packagePattern = new RegExp(
823
+ `(^|[\\s"'=,{\\[]+)${escapeRegex(packageName)}($|[\\s"'=<>~!@,;)}\\]]+)`,
824
+ "i",
825
+ );
826
+ if (!packagePattern.test(withoutInlineComment)) return false;
827
+
828
+ const requirementPattern = new RegExp(
829
+ `${escapeRegex(packageName)}(?:\\[[^\\]]+\\])?([^\\s"',;}\\]]*)`,
830
+ "i",
831
+ );
832
+ const requirementMatch = requirementPattern.exec(withoutInlineComment);
833
+ const suffix = requirementMatch?.[1] ?? "";
834
+ if (suffix && suffix !== ";") {
835
+ return true;
836
+ }
837
+
838
+ const keyPattern = new RegExp(
839
+ `^\\s*["']?${escapeRegex(packageName)}["']?\\s*=\\s*(.+)$`,
840
+ "i",
841
+ );
842
+ const keyMatch = keyPattern.exec(withoutInlineComment);
843
+ if (!keyMatch) return false;
844
+ const value = keyMatch[1].trim();
845
+ if (/^["']\*["']$/.test(value)) return false;
846
+ if (/version\s*=\s*["']\*["']/.test(value)) return false;
847
+ return true;
848
+ };
849
+
850
+ const findPinnedBackendSdkDependencyFiles = (
851
+ dependencySources,
852
+ packageNames,
853
+ ) =>
854
+ dependencySources
855
+ .filter(sourceIsPythonBackendDependencyDeclaration)
856
+ .filter((source) =>
857
+ source.text
858
+ .split(/\r?\n/)
859
+ .some((line) =>
860
+ packageNames.some((packageName) =>
861
+ backendSdkDependencyLineHasPinnedVersion(line, packageName),
862
+ ),
863
+ ),
864
+ )
865
+ .map((source) => source.file_path);
866
+
775
867
  const sourceLooksLikeBrowserTokenProvider = (source) => {
776
868
  const text = stripSourceNoise(source.text);
777
869
  return (
@@ -810,14 +902,83 @@ const findNextBrowserTokenProviderMissingPublicServiceKeyFiles = ({
810
902
  .map((source) => source.file_path);
811
903
  };
812
904
 
905
+ const findNextBrowserTokenProviderMissingPublicEndpointFiles = ({
906
+ dependencySources,
907
+ frontendSources,
908
+ }) => {
909
+ const roots = nextPackageRoots(dependencySources);
910
+ if (roots.length === 0) return [];
911
+ return frontendSources
912
+ .filter((source) => sourceIsUnderAnyRoot(source, roots))
913
+ .filter(sourceLooksLikeBrowserTokenProvider)
914
+ .filter(
915
+ (source) =>
916
+ !/\bprocess\.env\.NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT\b/.test(
917
+ source.text,
918
+ ),
919
+ )
920
+ .map((source) => source.file_path);
921
+ };
922
+
923
+ const browserTokenPathLiterals = (text) =>
924
+ [
925
+ ...stripSourceNoise(text).matchAll(
926
+ /(["'`])([^"'`]*(?:browser[-_]?tokens?|browser[-_]?token)[^"'`]*)\1/gi,
927
+ ),
928
+ ].map((match) => match[2]);
929
+
813
930
  const findBrowserTokenProviderWrongProxyPathFiles = (frontendSources) =>
814
931
  frontendSources
815
932
  .filter(sourceLooksLikeBrowserTokenProvider)
816
933
  .filter((source) => {
817
934
  const text = stripSourceNoise(source.text);
818
935
  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);
936
+ const tokenPathLiterals = browserTokenPathLiterals(text).filter(
937
+ (literal) => BROWSER_TOKEN_PATH_PATTERN.test(literal),
938
+ );
939
+ if (tokenPathLiterals.length === 0) {
940
+ return false;
941
+ }
942
+ return tokenPathLiterals.some(
943
+ (literal) => !literal.includes(CANONICAL_BROWSER_TOKEN_PROXY_PATH),
944
+ );
945
+ })
946
+ .map((source) => source.file_path);
947
+
948
+ const findBrowserTokenProviderNonClueEndpointEnvFiles = (frontendSources) =>
949
+ frontendSources
950
+ .filter(sourceLooksLikeBrowserTokenProvider)
951
+ .filter((source) =>
952
+ GENERIC_PUBLIC_API_ENV_PATTERN.test(stripSourceNoise(source.text)),
953
+ )
954
+ .map((source) => source.file_path);
955
+
956
+ const findClueInitEmptyEnvFallbackFiles = (frontendSources) =>
957
+ frontendSources
958
+ .filter(
959
+ (source) =>
960
+ sourceImportsFrontendSdk(source) ||
961
+ sourceLooksLikeBrowserTokenProvider(source),
962
+ )
963
+ .filter((source) =>
964
+ /process\.env\.NEXT_PUBLIC_CLUE_[A-Z0-9_]+\s*(?:\?\?|\|\|)\s*(["'])\1/.test(
965
+ stripSourceNoise(source.text),
966
+ ),
967
+ )
968
+ .map((source) => source.file_path);
969
+
970
+ const findFrontendInitBeforeClueInitFiles = (frontendSources) =>
971
+ frontendSources
972
+ .filter((source) => sourceImportsFrontendSdk(source))
973
+ .filter((source) => {
974
+ const text = stripSourceNoise(source.text, { stripStrings: true });
975
+ const initializedIndex = text.search(/\binitialized\s*=\s*true\b/);
976
+ const clueInitIndex = text.search(/\bClueInit\s*\(/);
977
+ return (
978
+ initializedIndex >= 0 &&
979
+ clueInitIndex >= 0 &&
980
+ initializedIndex < clueInitIndex
981
+ );
821
982
  })
822
983
  .map((source) => source.file_path);
823
984
 
@@ -969,6 +1130,10 @@ const checkSdkLifecycle = ({
969
1130
  const backendSdkDependencyPresent =
970
1131
  !backendPresent ||
971
1132
  dependencyHasAnyPackage(dependencySources, backendSpec.packages);
1133
+ const pinnedBackendSdkDependencyFiles = findPinnedBackendSdkDependencyFiles(
1134
+ dependencySources,
1135
+ backendSpec.packages,
1136
+ );
972
1137
  const backendSdkImportPresent =
973
1138
  !backendPresent ||
974
1139
  sourcesHavePythonImport(backendSources, backendSpec.imports);
@@ -995,17 +1160,28 @@ const checkSdkLifecycle = ({
995
1160
  dependencySources,
996
1161
  frontendSources,
997
1162
  });
998
- const wildcardFrontendSdkDependencyFiles =
999
- findWildcardFrontendSdkDependencyFiles(dependencySources);
1163
+ const nonLatestFrontendSdkDependencyFiles =
1164
+ findNonLatestFrontendSdkDependencyFiles(dependencySources);
1000
1165
  const browserTokenProviderMissingServiceKeyFiles =
1001
1166
  findBrowserTokenProviderMissingServiceKeyFiles(frontendSources);
1002
1167
  const browserTokenProviderWrongProxyPathFiles =
1003
1168
  findBrowserTokenProviderWrongProxyPathFiles(frontendSources);
1169
+ const browserTokenProviderNonClueEndpointEnvFiles =
1170
+ findBrowserTokenProviderNonClueEndpointEnvFiles(frontendSources);
1171
+ const clueInitEmptyEnvFallbackFiles =
1172
+ findClueInitEmptyEnvFallbackFiles(frontendSources);
1173
+ const frontendInitBeforeClueInitFiles =
1174
+ findFrontendInitBeforeClueInitFiles(frontendSources);
1004
1175
  const nextBrowserTokenProviderMissingPublicServiceKeyFiles =
1005
1176
  findNextBrowserTokenProviderMissingPublicServiceKeyFiles({
1006
1177
  dependencySources,
1007
1178
  frontendSources,
1008
1179
  });
1180
+ const nextBrowserTokenProviderMissingPublicEndpointFiles =
1181
+ findNextBrowserTokenProviderMissingPublicEndpointFiles({
1182
+ dependencySources,
1183
+ frontendSources,
1184
+ });
1009
1185
  const browserTokenProxyUsingBackendServiceKeyFiles =
1010
1186
  findBrowserTokenProxyUsingBackendServiceKeyFiles(backendSources);
1011
1187
  const browserTokenProxyTrustingBodyOriginFiles =
@@ -1048,6 +1224,7 @@ const checkSdkLifecycle = ({
1048
1224
  ? null
1049
1225
  : backendSpec.blocker,
1050
1226
  sdk_dependency_present: backendSdkDependencyPresent,
1227
+ pinned_sdk_dependency_files: pinnedBackendSdkDependencyFiles,
1051
1228
  sdk_import_present: backendSdkImportPresent,
1052
1229
  sdk_dependency_or_import_present: backendSdkPresent,
1053
1230
  sdk_init_present: backendInitPresent,
@@ -1069,13 +1246,19 @@ const checkSdkLifecycle = ({
1069
1246
  frontendLifecycleFilesWithoutVerifiedSdk,
1070
1247
  wrong_sdk_packages: wrongFrontendSdkPackages,
1071
1248
  next_lifecycle_non_public_env_files: nextLifecycleNonPublicEnvFiles,
1072
- wildcard_sdk_dependency_files: wildcardFrontendSdkDependencyFiles,
1249
+ non_latest_sdk_dependency_files: nonLatestFrontendSdkDependencyFiles,
1073
1250
  browser_token_provider_missing_service_key_files:
1074
1251
  browserTokenProviderMissingServiceKeyFiles,
1075
1252
  browser_token_provider_wrong_proxy_path_files:
1076
1253
  browserTokenProviderWrongProxyPathFiles,
1254
+ browser_token_provider_non_clue_endpoint_env_files:
1255
+ browserTokenProviderNonClueEndpointEnvFiles,
1256
+ clue_init_empty_env_fallback_files: clueInitEmptyEnvFallbackFiles,
1257
+ frontend_init_before_clue_init_files: frontendInitBeforeClueInitFiles,
1077
1258
  next_browser_token_provider_missing_public_service_key_files:
1078
1259
  nextBrowserTokenProviderMissingPublicServiceKeyFiles,
1260
+ next_browser_token_provider_missing_public_endpoint_files:
1261
+ nextBrowserTokenProviderMissingPublicEndpointFiles,
1079
1262
  browser_token_proxy_uses_backend_service_key_files:
1080
1263
  browserTokenProxyUsingBackendServiceKeyFiles,
1081
1264
  browser_token_proxy_trusts_body_origin_files:
@@ -1093,16 +1276,21 @@ const checkSdkLifecycle = ({
1093
1276
  missingApis.length === 0 &&
1094
1277
  backendSdkPresent &&
1095
1278
  backendSdkInstallabilityVerified &&
1279
+ pinnedBackendSdkDependencyFiles.length === 0 &&
1096
1280
  backendInitPresent &&
1097
1281
  backendMissingApis.length === 0 &&
1098
1282
  frontendSdkPresent &&
1099
1283
  frontendLifecycleFilesWithoutVerifiedSdk.length === 0 &&
1100
1284
  wrongFrontendSdkPackages.length === 0 &&
1101
1285
  nextLifecycleNonPublicEnvFiles.length === 0 &&
1102
- wildcardFrontendSdkDependencyFiles.length === 0 &&
1286
+ nonLatestFrontendSdkDependencyFiles.length === 0 &&
1103
1287
  browserTokenProviderMissingServiceKeyFiles.length === 0 &&
1104
1288
  browserTokenProviderWrongProxyPathFiles.length === 0 &&
1289
+ browserTokenProviderNonClueEndpointEnvFiles.length === 0 &&
1290
+ clueInitEmptyEnvFallbackFiles.length === 0 &&
1291
+ frontendInitBeforeClueInitFiles.length === 0 &&
1105
1292
  nextBrowserTokenProviderMissingPublicServiceKeyFiles.length === 0 &&
1293
+ nextBrowserTokenProviderMissingPublicEndpointFiles.length === 0 &&
1106
1294
  browserTokenProxyUsingBackendServiceKeyFiles.length === 0 &&
1107
1295
  browserTokenProxyTrustingBodyOriginFiles.length === 0 &&
1108
1296
  browserTokenProxyTrustingBodyProjectEnvironmentFiles.length === 0 &&
@@ -1309,7 +1497,7 @@ export const runSetupCheck = async ({
1309
1497
 
1310
1498
  if (requireSdkLifecycle) {
1311
1499
  const sdkLifecycle = checkSdkLifecycle({
1312
- backendRootPaths: request?.allowed_source_paths ?? [],
1500
+ backendRootPaths: setupBackendRootPaths(request),
1313
1501
  dependencySources,
1314
1502
  framework: request?.framework,
1315
1503
  sources,
@@ -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"],
@@ -53,6 +55,21 @@ export const buildAiSetupHelp = () => ({
53
55
  "whitespace-only edits, import sorting, formatter churn, or comment/style cleanup outside the exact Clue SDK wiring lines",
54
56
  ],
55
57
  },
58
+ sdk_dependency_contract: {
59
+ frontend: {
60
+ package: "@clue-ai/browser-sdk",
61
+ install: "package-manager add @clue-ai/browser-sdk@latest",
62
+ rule:
63
+ "Frontend package.json must declare @clue-ai/browser-sdk as latest. Fixed versions and * are rejected by setup-check because stale SDKs break setup contracts.",
64
+ },
65
+ backend: {
66
+ fastapi_package: "clue-fastapi-sdk",
67
+ django_package: "clue-django-sdk",
68
+ install: "pip install --upgrade <backend-sdk-package>",
69
+ rule:
70
+ "Python backend SDK dependency declarations must be unpinned so pip resolves the latest release. Fixed ==, ~=, <=, >=, <, >, local path, and exact-version declarations are rejected by setup-check.",
71
+ },
72
+ },
56
73
  environment_contract: {
57
74
  nextjs_frontend_client_env: {
58
75
  variables: [
@@ -60,8 +77,9 @@ export const buildAiSetupHelp = () => ({
60
77
  "NEXT_PUBLIC_CLUE_ENVIRONMENT",
61
78
  "NEXT_PUBLIC_CLUE_SERVICE_KEY",
62
79
  "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
80
+ "NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
63
81
  ],
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.",
82
+ 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
83
  },
66
84
  backend_browser_token_proxy_env: {
67
85
  variables: ["CLUE_API_KEY", "CLUE_API_BASE_URL"],
@@ -83,9 +101,21 @@ export const buildAiSetupHelp = () => ({
83
101
  owner: "ai_or_user",
84
102
  ai_agent_may_run: true,
85
103
  command: clueCliCommand("setup-doctor --local"),
86
- rule: "Run setup-doctor after local frontend/backend services and required env are available. It checks API connectivity only and does not replace user-operated setup-watch.",
104
+ rule: "Run setup-doctor after local frontend/backend services and required env are available. setup-agent blocks when required setup-doctor inputs are missing or connectivity fails, unless setup-doctor is explicitly skipped. It checks API connectivity only and does not replace user-operated setup-watch.",
87
105
  checked_hops: Object.keys(API_CONNECTIVITY_CONTRACT.hops),
88
106
  },
107
+ setup_agent: {
108
+ owner: "clue_cli",
109
+ command: clueCliCommand("setup-agent --repo ."),
110
+ providers: ["openai", "anthropic"],
111
+ env:
112
+ "Set exactly CLUE_AI_PROVIDER, CLUE_AI_PROVIDER_API_KEY, and CLUE_AI_MODEL. setup-agent uses the same AI provider configuration as the rest of the Clue CLI. Optional CLI overrides are --ai-provider, --ai-provider-api-key, and --ai-model; do not use ambiguous --api_key flags.",
113
+ model_role:
114
+ "Return a strict structured lifecycle plan only; the CLI owns scanning, edit application, checks, doctor preflight, retry, and final status.",
115
+ tool_calling:
116
+ "OpenAI uses Responses API forced function tools with strict schema and parallel_tool_calls false. Anthropic uses Messages API tool_choice with the same input schema.",
117
+ setup_watch_auto_run: false,
118
+ },
89
119
  completion_boundary: {
90
120
  ai_may_claim: [
91
121
  "Clue setup code changes were applied",
@@ -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,
@@ -316,27 +316,34 @@ const skillBody = (name, { documentsUrl } = {}) => {
316
316
  "Do not add per-call try/catch, try/except, `.catch`, or custom safe wrappers solely around official Clue SDK public lifecycle calls.",
317
317
  "Never await a Clue lifecycle call in a way that can block login, logout, account selection, request handling, page rendering, or API responses.",
318
318
  "Add or report the required SDK dependency instead of fabricating lifecycle APIs.",
319
- "For frontend code, add the real `@clue-ai/browser-sdk` dependency when missing. Do not invent `clue-js-sdk`, `@clue/browser-sdk`, local placeholder modules, or dynamic imports that hide a missing SDK.",
319
+ "For frontend code, add the real `@clue-ai/browser-sdk` dependency when missing. Use the latest channel (`@clue-ai/browser-sdk@latest`) so setup receives current SDK fixes. Do not invent `clue-js-sdk`, `@clue/browser-sdk`, local placeholder modules, or dynamic imports that hide a missing SDK.",
320
320
  `When lifecycle edits are clear, write an exact replacement plan to a temporary local JSON file and apply it with \`${clueCliCommand("lifecycle-apply --plan <plan-file> --repo .")}\`.`,
321
321
  "If npm/npx cannot fetch the Clue CLI package, report a blocker with the exact command and error instead of manually applying replacement plans.",
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`.",
336
341
  "If a backend-owned browser token endpoint is implemented, read `CLUE_API_BASE_URL` from the backend env block and normalize it so values with or without a trailing `/api/v1` do not produce duplicate paths.",
337
- "For FastAPI code, add `clue-fastapi-sdk` to the backend dependency file when missing, import `clue_init_fastapi` plus `ClueIdentify`, `ClueSetAccount`, and `ClueLogout` where needed, and use `CLUE_PROJECT_KEY`, `CLUE_ENVIRONMENT`, `CLUE_API_KEY`, and `CLUE_INGEST_ENDPOINT` from the backend env block.",
342
+ "For FastAPI code, add unpinned `clue-fastapi-sdk` to the backend dependency file when missing so pip resolves the latest release, import `clue_init_fastapi` plus `ClueIdentify`, `ClueSetAccount`, and `ClueLogout` where needed, and use `CLUE_PROJECT_KEY`, `CLUE_ENVIRONMENT`, `CLUE_API_KEY`, and `CLUE_INGEST_ENDPOINT` from the backend env block.",
338
343
  "`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.",
339
- "Do not add `@clue-ai/browser-sdk` or backend SDK dependencies with `*` or `latest`; use a concrete published version or package-manager-resolved semver range and update the repository lockfile when one exists.",
344
+ "Python backend SDK dependencies must not be pinned.",
345
+ "`@clue-ai/browser-sdk` must use `latest`.",
346
+ "Install Clue SDK dependencies through the latest channel. Frontend package managers must use `@clue-ai/browser-sdk@latest`; Python backend dependency declarations must not pin `clue-fastapi-sdk` or `clue-django-sdk` to a fixed version.",
340
347
  "Use `CLUE_SERVICE_KEY` as the canonical local service identifier. Do not ask the user to manage a separate producer id; SDKs should send producer id as the service key for setup verification compatibility.",
341
348
  "For frontend code, pass `serviceKey` from the frontend service env name written by `.env.clue` to `ClueInit`; in Next.js browser/client code that name is `NEXT_PUBLIC_CLUE_SERVICE_KEY`.",
342
349
  "For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable; if it is not published or cannot be verified, report a blocker instead of adding a guessed dependency or import.",
@@ -367,12 +374,17 @@ const skillBody = (name, { documentsUrl } = {}) => {
367
374
  "Reject backend setup when backend routes exist but no backend Clue SDK dependency/import/init was added.",
368
375
  "Reject awaited lifecycle calls that can block host service behavior.",
369
376
  "Reject browser token proxy code that forwards origin, projectKey, or environment from request JSON/body under server `CLUE_API_KEY`.",
377
+ "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.",
378
+ "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.",
379
+ "Reject frontend adapters that set `initialized = true` before calling `ClueInit`, or pass empty-string fallbacks for required `NEXT_PUBLIC_CLUE_*` values into `ClueInit`.",
380
+ "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
381
  "Reject setup that covers only one login path when multiple login success paths are clearly present.",
371
382
  "Reject ClueInit inside React component lifecycle hooks, page components, sidebars, login/register success callbacks, or any repeated user interaction path.",
372
383
  "Reject broad ClueTrack instrumentation and DOM clue tags.",
373
384
  "Reject ClueTrack instrumentation unless the user explicitly requested product event tracking.",
374
385
  "Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables.",
375
- "Reject Clue SDK dependency entries that use `*` or `latest` instead of a concrete published version or package-manager-resolved semver range.",
386
+ "Reject Clue SDK dependency entries that pin stale fixed versions. Frontend package.json must use `@clue-ai/browser-sdk: latest`; Python backend SDK dependency declarations must be unpinned or package-manager wildcard-latest.",
387
+ "Reject Next.js browser token providers that do not read `NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT`.",
376
388
  "Confirm no project key, API key, secret, or env value appears in diff or report.",
377
389
  "Confirm lifecycle insertions are minimal and reviewable.",
378
390
  "Reject whitespace-only edits, import sorting, formatter churn, or comment/style cleanup outside the exact Clue SDK wiring lines.",
@@ -392,7 +404,7 @@ const skillBody = (name, { documentsUrl } = {}) => {
392
404
  "Confirm the semantic workflow does not send GitHub actor, triggering_actor, sender, repository owner, repository name, or default branch to Clue.",
393
405
  "Confirm `.clue/semantic-request.runtime.json` is not created, committed, or staged.",
394
406
  `Run \`${clueCliCommand("setup-check --framework <framework> --backend-root-path <path> --repo . --target <codex|claude_code> --require-sdk-lifecycle")}\` when possible.`,
395
- `Run \`${clueCliCommand("setup-doctor --local")}\` when local frontend/backend services are running and required env values are available. Report skipped_setup_doctor with the missing service URL or env name when it cannot run.`,
407
+ `Run \`${clueCliCommand("setup-doctor --local")}\` when local frontend/backend services are running. Missing setup-doctor inputs or failed API hops are blockers unless the user explicitly skipped setup-doctor.`,
396
408
  `Do not run \`${clueCliCommand("setup-watch --local")}\` automatically. setup-watch requires the user to operate real local frontend/backend services and login/logout/account flows.`,
397
409
  "If the user has not provided setup-watch or setup-screen evidence, report event delivery verification as `user_verification_pending` and do not state `setup completed`.",
398
410
  "Local static verification passed does not mean setup complete unless dependency install, SDK imports, app startup, and user-provided setup-watch or setup-screen event delivery were all verified.",
@@ -465,7 +477,7 @@ const skillBody = (name, { documentsUrl } = {}) => {
465
477
  `- Setup API connectivity has four distinct hops: ${Object.entries(API_CONNECTIVITY_CONTRACT.hops)
466
478
  .map(([name, hop]) => `${name}=${hop.method} ${hop.path}`)
467
479
  .join(", ")}.`,
468
- `- Run \`${clueCliCommand("setup-doctor --local")}\` when local services and required env are available. This checks API connectivity only; it does not replace user-operated setup-watch.`,
480
+ `- Run \`${clueCliCommand("setup-doctor --local")}\` when local services are running. Missing setup-doctor inputs or failed API hops are blockers unless the user explicitly skipped setup-doctor. This checks API connectivity only; it does not replace user-operated setup-watch.`,
469
481
  "- Do not implement or refresh semantic snapshot CI during lifecycle placement; report a blocker if generated semantic artifacts are missing or stale.",
470
482
  `- Do not run \`${clueCliCommand("setup-watch --local")}\` automatically. setup-watch and the Clue setup screen are user-operated verification steps, not implementation-agent responsibility.`,
471
483
  "- The full setup must start with `clue-setup-orchestrator`.",