@clue-ai/cli 0.0.16 → 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.
@@ -0,0 +1,114 @@
1
+ export const AI_SETUP_CONTRACT_VERSION =
2
+ "2026-05-10.frontend-adapter-contract.v8";
3
+
4
+ export const SETUP_DOCTRINE = {
5
+ purpose:
6
+ "Clue setup installs an external SDK integration so observed product facts can reach Clue's Customer Value Understanding Engine. It is not an opportunity to improve, refactor, redesign, or reinterpret the host application.",
7
+ minimal_diff_reason:
8
+ "The customer must be able to review and merge the setup diff with confidence. Extra formatting, refactors, auth rewrites, UI changes, or unrelated cleanup make the integration harder to trust.",
9
+ ai_decision_boundary:
10
+ "The AI should use repository understanding only to choose existing lifecycle boundaries for ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout.",
11
+ deterministic_control_boundary:
12
+ "Everything that can be controlled mechanically should be controlled by the CLI, generated skills, documentation contract, lifecycle plan schema, and setup-check static guards.",
13
+ documentation_reason:
14
+ "SDK signatures, environment variable names, browser token behavior, and verification ownership are contracts. The AI must read the setup documents instead of relying on memory.",
15
+ failure_posture:
16
+ "When a lifecycle point, SDK package, signature, env name, or verification step is unclear, the correct output is a blocker or user_verification_pending, not a guessed implementation or a completion claim.",
17
+ };
18
+
19
+ export const DETERMINISTIC_CONTROL_MODEL = {
20
+ ai_should_decide: [
21
+ "which existing bootstrap point owns ClueInit",
22
+ "which existing login/session success point owns ClueIdentify",
23
+ "which existing account/workspace/organization resolution point owns ClueSetAccount",
24
+ "which existing logout/session reset point owns ClueLogout",
25
+ "whether a lifecycle point is unclear and should be reported as blocked",
26
+ ],
27
+ cli_should_control: [
28
+ "official SDK package names",
29
+ "official public SDK function names and supported lifecycle API set",
30
+ "environment variable names produced by setup and consumed by setup code",
31
+ "machine-owned semantic workflow generation and verification",
32
+ "setup-watch ownership as user-operated verification",
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
+ "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",
36
+ ],
37
+ };
38
+
39
+ export const API_CONNECTIVITY_CONTRACT = {
40
+ purpose:
41
+ "Clue setup has four distinct HTTP hops. The AI must not collapse customer-owned proxy routes and Clue-owned ingest routes into one vague browser-token endpoint.",
42
+ hops: {
43
+ client_backend_browser_token_proxy: {
44
+ owner: "customer_backend",
45
+ method: "POST",
46
+ path: "/api/v1/clue/browser-tokens",
47
+ caller: "customer_frontend_browserTokenProvider",
48
+ purpose:
49
+ "Issue a short-lived browser token without exposing CLUE_API_KEY to browser code.",
50
+ },
51
+ clue_backend_browser_token_issue: {
52
+ owner: "clue_backend",
53
+ method: "POST",
54
+ path: "/api/v1/ingest/browser-tokens",
55
+ caller: "customer_backend_browser_token_proxy",
56
+ purpose:
57
+ "Clue backend validates CLUE_API_KEY, project, environment, service key, and origin, then returns a browser token.",
58
+ },
59
+ browser_ingest: {
60
+ owner: "clue_backend",
61
+ method: "POST",
62
+ path: "/api/v1/ingest/browser",
63
+ caller: "customer_frontend_browser_sdk",
64
+ env_name: "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT for Next.js browser code",
65
+ purpose:
66
+ "Browser SDK sends observation source events with x-clue-browser-token.",
67
+ },
68
+ backend_ingest: {
69
+ owner: "clue_backend",
70
+ method: "POST",
71
+ path: "/api/v1/ingest/backend",
72
+ caller: "customer_backend_sdk",
73
+ env_name: "CLUE_INGEST_ENDPOINT",
74
+ purpose:
75
+ "Backend SDK sends observation source events with server-side CLUE_API_KEY.",
76
+ },
77
+ },
78
+ preflight_command: "setup-doctor --local",
79
+ setup_watch_boundary:
80
+ "setup-doctor checks local API connectivity before user flows. setup-watch remains user-operated lifecycle and event-delivery verification.",
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
+
104
+ export const setupDoctrineSkillLines = () => [
105
+ `- Purpose: ${SETUP_DOCTRINE.purpose}`,
106
+ `- Minimal diff reason: ${SETUP_DOCTRINE.minimal_diff_reason}`,
107
+ `- AI decision boundary: ${SETUP_DOCTRINE.ai_decision_boundary}`,
108
+ `- Static control boundary: ${SETUP_DOCTRINE.deterministic_control_boundary}`,
109
+ `- Documentation reason: ${SETUP_DOCTRINE.documentation_reason}`,
110
+ `- Failure posture: ${SETUP_DOCTRINE.failure_posture}`,
111
+ `- API connectivity: ${API_CONNECTIVITY_CONTRACT.purpose}`,
112
+ `- Frontend adapter: ${FRONTEND_ADAPTER_CONTRACT.purpose}`,
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.`,
114
+ ];
@@ -12,6 +12,7 @@ import {
12
12
  stripSourceNoise,
13
13
  } from "./lifecycle-guard.mjs";
14
14
  import { listAllowedSourceFiles } from "./path-policy.mjs";
15
+ import { AI_SETUP_CONTRACT_VERSION } from "./setup-ai-contract.mjs";
15
16
  import { runSemanticInventory } from "./semantic-ci.mjs";
16
17
 
17
18
  const DEFAULT_WORKFLOW_PATH = ".github/workflows/clue-semantic-snapshot.yml";
@@ -27,7 +28,7 @@ const SETUP_SKILLS = [
27
28
  "clue-local-verification",
28
29
  "clue-setup-report",
29
30
  ];
30
- const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v4";
31
+ const SETUP_SKILL_CONTENT_VERSION = AI_SETUP_CONTRACT_VERSION;
31
32
  const REQUIRED_SETUP_SKILL_PHRASES = {
32
33
  "clue-sdk-instrumentation": [
33
34
  "Do not create no-op wrappers",
@@ -41,7 +42,13 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
41
42
  "Do not add `@clue-ai/browser-sdk` or backend SDK dependencies with `*` or `latest`",
42
43
  "Whitespace-only changes are allowed only on lines directly changed for Clue SDK wiring",
43
44
  "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`",
45
+ "send the same service key used by `ClueInit`",
46
+ "NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
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`",
45
52
  "For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
46
53
  ],
47
54
  "clue-setup-audit": [
@@ -49,12 +56,18 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
49
56
  "Reject Django SDK setup when `clue-django-sdk` installability has not been verified",
50
57
  "Reject ClueTrack instrumentation unless the user explicitly requested product event tracking",
51
58
  "Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables",
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",
52
64
  "Reject whitespace-only edits, import sorting, formatter churn",
53
65
  "Reject unrelated refactors, renames, file moves",
54
66
  "Execution agents must not approve, certify, or mark their own work complete",
55
67
  ],
56
68
  "clue-local-verification": [
57
69
  "`setup-check --require-sdk-lifecycle` is a static source check only",
70
+ "`setup-doctor --local` API connectivity preflight",
58
71
  "Verify frontend SDK installability/import",
59
72
  "Verify backend SDK installability/import",
60
73
  "Do not run `npx -y @clue-ai/cli setup-watch --local` automatically",
@@ -732,7 +745,13 @@ const NEXT_PUBLIC_CLUE_NAMES = [
732
745
  "CLUE_ENVIRONMENT",
733
746
  "CLUE_SERVICE_KEY",
734
747
  "CLUE_INGEST_ENDPOINT",
748
+ "CLUE_BROWSER_TOKEN_ENDPOINT",
735
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/;
736
755
 
737
756
  const findNextLifecycleNonPublicEnvFiles = ({
738
757
  dependencySources,
@@ -806,6 +825,86 @@ const findNextBrowserTokenProviderMissingPublicServiceKeyFiles = ({
806
825
  .map((source) => source.file_path);
807
826
  };
808
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
+
853
+ const findBrowserTokenProviderWrongProxyPathFiles = (frontendSources) =>
854
+ frontendSources
855
+ .filter(sourceLooksLikeBrowserTokenProvider)
856
+ .filter((source) => {
857
+ const text = stripSourceNoise(source.text);
858
+ if (!/\bfetch\s*\(/.test(text)) return false;
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
+ );
905
+ })
906
+ .map((source) => source.file_path);
907
+
809
908
  const sourceLooksLikeBrowserTokenProxy = (source) => {
810
909
  const text = stripSourceNoise(source.text);
811
910
  return (
@@ -827,6 +926,43 @@ const findBrowserTokenProxyUsingBackendServiceKeyFiles = (backendSources) =>
827
926
  })
828
927
  .map((source) => source.file_path);
829
928
 
929
+ const bodyOriginPatterns = [
930
+ /\b(?:payload|body|requestBody|request_body|data|input)\.origin\b/i,
931
+ /\b(?:payload|body|requestBody|request_body|data|input)\s*\[\s*["']origin["']\s*\]/i,
932
+ /\b(?:payload|body|requestBody|request_body|data|input)\.get\s*\(\s*["']origin["']/i,
933
+ /["']origin["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
934
+ /\borigin\s*=\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
935
+ ];
936
+
937
+ const bodyProjectEnvironmentPatterns = [
938
+ /\b(?:projectKey|project_key)\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
939
+ /["'](?:projectKey|project_key)["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
940
+ /\benvironment\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
941
+ /["']environment["']\s*:\s*(?:payload|body|requestBody|request_body|data|input)\b/i,
942
+ ];
943
+
944
+ const findBrowserTokenProxyTrustingBodyOriginFiles = (backendSources) =>
945
+ backendSources
946
+ .filter(sourceLooksLikeBrowserTokenProxy)
947
+ .filter((source) => {
948
+ const text = stripSourceNoise(source.text);
949
+ return bodyOriginPatterns.some((pattern) => pattern.test(text));
950
+ })
951
+ .map((source) => source.file_path);
952
+
953
+ const findBrowserTokenProxyTrustingBodyProjectEnvironmentFiles = (
954
+ backendSources,
955
+ ) =>
956
+ backendSources
957
+ .filter(sourceLooksLikeBrowserTokenProxy)
958
+ .filter((source) => {
959
+ const text = stripSourceNoise(source.text);
960
+ return bodyProjectEnvironmentPatterns.some((pattern) =>
961
+ pattern.test(text),
962
+ );
963
+ })
964
+ .map((source) => source.file_path);
965
+
830
966
  const backendSdkSpec = (framework) =>
831
967
  BACKEND_SDK_BY_FRAMEWORK[String(framework ?? "").toLowerCase()] ?? {
832
968
  packages: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
@@ -947,13 +1083,30 @@ const checkSdkLifecycle = ({
947
1083
  findWildcardFrontendSdkDependencyFiles(dependencySources);
948
1084
  const browserTokenProviderMissingServiceKeyFiles =
949
1085
  findBrowserTokenProviderMissingServiceKeyFiles(frontendSources);
1086
+ const browserTokenProviderWrongProxyPathFiles =
1087
+ findBrowserTokenProviderWrongProxyPathFiles(frontendSources);
1088
+ const browserTokenProviderNonClueEndpointEnvFiles =
1089
+ findBrowserTokenProviderNonClueEndpointEnvFiles(frontendSources);
1090
+ const clueInitEmptyEnvFallbackFiles =
1091
+ findClueInitEmptyEnvFallbackFiles(frontendSources);
1092
+ const frontendInitBeforeClueInitFiles =
1093
+ findFrontendInitBeforeClueInitFiles(frontendSources);
950
1094
  const nextBrowserTokenProviderMissingPublicServiceKeyFiles =
951
1095
  findNextBrowserTokenProviderMissingPublicServiceKeyFiles({
952
1096
  dependencySources,
953
1097
  frontendSources,
954
1098
  });
1099
+ const nextBrowserTokenProviderMissingPublicEndpointFiles =
1100
+ findNextBrowserTokenProviderMissingPublicEndpointFiles({
1101
+ dependencySources,
1102
+ frontendSources,
1103
+ });
955
1104
  const browserTokenProxyUsingBackendServiceKeyFiles =
956
1105
  findBrowserTokenProxyUsingBackendServiceKeyFiles(backendSources);
1106
+ const browserTokenProxyTrustingBodyOriginFiles =
1107
+ findBrowserTokenProxyTrustingBodyOriginFiles(backendSources);
1108
+ const browserTokenProxyTrustingBodyProjectEnvironmentFiles =
1109
+ findBrowserTokenProxyTrustingBodyProjectEnvironmentFiles(backendSources);
957
1110
  const backendIdentityRequired =
958
1111
  backendPresent &&
959
1112
  /\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
@@ -1014,10 +1167,22 @@ const checkSdkLifecycle = ({
1014
1167
  wildcard_sdk_dependency_files: wildcardFrontendSdkDependencyFiles,
1015
1168
  browser_token_provider_missing_service_key_files:
1016
1169
  browserTokenProviderMissingServiceKeyFiles,
1170
+ browser_token_provider_wrong_proxy_path_files:
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,
1017
1176
  next_browser_token_provider_missing_public_service_key_files:
1018
1177
  nextBrowserTokenProviderMissingPublicServiceKeyFiles,
1178
+ next_browser_token_provider_missing_public_endpoint_files:
1179
+ nextBrowserTokenProviderMissingPublicEndpointFiles,
1019
1180
  browser_token_proxy_uses_backend_service_key_files:
1020
1181
  browserTokenProxyUsingBackendServiceKeyFiles,
1182
+ browser_token_proxy_trusts_body_origin_files:
1183
+ browserTokenProxyTrustingBodyOriginFiles,
1184
+ browser_token_proxy_trusts_body_project_environment_files:
1185
+ browserTokenProxyTrustingBodyProjectEnvironmentFiles,
1021
1186
  },
1022
1187
  has_noop_wrapper: noOpPattern.test(combined),
1023
1188
  component_lifecycle_init_files: componentLifecycleInitFiles,
@@ -1037,8 +1202,15 @@ const checkSdkLifecycle = ({
1037
1202
  nextLifecycleNonPublicEnvFiles.length === 0 &&
1038
1203
  wildcardFrontendSdkDependencyFiles.length === 0 &&
1039
1204
  browserTokenProviderMissingServiceKeyFiles.length === 0 &&
1205
+ browserTokenProviderWrongProxyPathFiles.length === 0 &&
1206
+ browserTokenProviderNonClueEndpointEnvFiles.length === 0 &&
1207
+ clueInitEmptyEnvFallbackFiles.length === 0 &&
1208
+ frontendInitBeforeClueInitFiles.length === 0 &&
1040
1209
  nextBrowserTokenProviderMissingPublicServiceKeyFiles.length === 0 &&
1210
+ nextBrowserTokenProviderMissingPublicEndpointFiles.length === 0 &&
1041
1211
  browserTokenProxyUsingBackendServiceKeyFiles.length === 0 &&
1212
+ browserTokenProxyTrustingBodyOriginFiles.length === 0 &&
1213
+ browserTokenProxyTrustingBodyProjectEnvironmentFiles.length === 0 &&
1042
1214
  !noOpPattern.test(combined) &&
1043
1215
  componentLifecycleInitFiles.length === 0 &&
1044
1216
  blockingLifecycleFiles.length === 0,