@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.
- package/README.md +12 -4
- package/bin/clue-cli.mjs +133 -36
- package/package.json +1 -1
- package/src/lifecycle-init.mjs +16 -5
- package/src/public-schema.cjs +15 -0
- package/src/semantic-ci.mjs +128 -17
- package/src/setup-check.mjs +227 -3
- package/src/setup-documents.mjs +99 -0
- package/src/setup-help.mjs +18 -1
- package/src/setup-prepare.mjs +117 -24
- package/src/setup-tool.mjs +75 -7
package/src/setup-check.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
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")
|
|
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
|
+
|
package/src/setup-help.mjs
CHANGED
|
@@ -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.
|
|
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,
|
package/src/setup-prepare.mjs
CHANGED
|
@@ -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
|
|
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
|
-
{
|
|
177
|
-
|
|
178
|
-
|
|
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({
|
|
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: "
|
|
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:
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
"
|
|
488
|
-
|
|
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",
|