@clue-ai/cli 0.0.13 → 0.0.14
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 +3 -0
- package/bin/clue-cli.mjs +101 -25
- package/package.json +1 -1
- package/src/lifecycle-init.mjs +13 -5
- package/src/setup-check.mjs +145 -2
- package/src/setup-help.mjs +16 -1
- package/src/setup-prepare.mjs +94 -24
- package/src/setup-tool.mjs +11 -5
package/README.md
CHANGED
|
@@ -69,6 +69,9 @@ service key and the lifecycle checks expected for that service, for example
|
|
|
69
69
|
`frontend:web[init,identify,set-account,event-sent]=<frontend-url>,backend:api[init,identify,set-account,logout,event-sent]=<backend-url>`.
|
|
70
70
|
Do not assume localhost ports; use the actual URLs printed by the repository's
|
|
71
71
|
dev scripts or configured in its local env.
|
|
72
|
+
In `--local` mode, the watcher stays open until the expected lifecycle checks
|
|
73
|
+
pass or you stop it with Ctrl+C. It prints a per-service Clue lifecycle
|
|
74
|
+
checklist and only prints a new snapshot when the observed state changes.
|
|
72
75
|
|
|
73
76
|
`npx -y @clue-ai/cli setup` reads Clue values from the setup screen flags, detects local
|
|
74
77
|
services, writes `.clue/setup-manifest.json`, and prints service-specific env
|
package/bin/clue-cli.mjs
CHANGED
|
@@ -135,6 +135,15 @@ const DEFAULT_WATCH_LIFECYCLE = [
|
|
|
135
135
|
"event-sent",
|
|
136
136
|
];
|
|
137
137
|
const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
138
|
+
const DEFAULT_REMOTE_SETUP_WATCH_TIMEOUT_MS = 120_000;
|
|
139
|
+
|
|
140
|
+
const WATCH_LIFECYCLE_LABELS = new Map([
|
|
141
|
+
["init", "ClueInit"],
|
|
142
|
+
["identify", "ClueIdentify"],
|
|
143
|
+
["set-account", "ClueSetAccount"],
|
|
144
|
+
["logout", "ClueLogout"],
|
|
145
|
+
["event-sent", "Event delivery"],
|
|
146
|
+
]);
|
|
138
147
|
|
|
139
148
|
const parseExpectedLifecycle = (value) => {
|
|
140
149
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -333,6 +342,20 @@ const buildWatchProducerIds = ({ explicitProducerIds, watchTargets }) =>
|
|
|
333
342
|
...watchTargets.map((target) => target.producerId),
|
|
334
343
|
]);
|
|
335
344
|
|
|
345
|
+
const parseSetupWatchTimeoutMs = ({ flags, localMode }) => {
|
|
346
|
+
if (!flags.has("timeout-ms")) {
|
|
347
|
+
return localMode ? null : DEFAULT_REMOTE_SETUP_WATCH_TIMEOUT_MS;
|
|
348
|
+
}
|
|
349
|
+
const timeoutMs = Number(flags.get("timeout-ms"));
|
|
350
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
351
|
+
throw new Error("--timeout-ms must be a non-negative number");
|
|
352
|
+
}
|
|
353
|
+
return timeoutMs;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const isSetupWatchTimedOut = ({ started, timeoutMs }) =>
|
|
357
|
+
timeoutMs !== null && Date.now() - started > timeoutMs;
|
|
358
|
+
|
|
336
359
|
const checkTargetUrl = async (url) => {
|
|
337
360
|
if (!url) return { checked: true, reachable: false, status: null };
|
|
338
361
|
try {
|
|
@@ -543,25 +566,60 @@ const startLocalSetupReceiver = async ({ host, port }) => {
|
|
|
543
566
|
};
|
|
544
567
|
};
|
|
545
568
|
|
|
569
|
+
const lifecycleLabel = (name) => WATCH_LIFECYCLE_LABELS.get(name) ?? name;
|
|
570
|
+
|
|
571
|
+
const statusMark = (passed) => (passed ? "x" : " ");
|
|
572
|
+
|
|
573
|
+
const lifecycleStatusText = (passed) => (passed ? "done!" : "pending...");
|
|
574
|
+
|
|
575
|
+
const targetUrlStatusText = (target) => {
|
|
576
|
+
if (!target.urlChecked) return null;
|
|
577
|
+
return String(
|
|
578
|
+
target.urlReachable
|
|
579
|
+
? target.urlStatus
|
|
580
|
+
: (target.urlStatus ?? "unreachable"),
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
|
|
546
584
|
const renderWatchTargets = (targetChecks) =>
|
|
547
585
|
targetChecks
|
|
548
586
|
.map((target) => {
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
587
|
+
const lifecycleLines = target.expectedLifecycle.map((name) => {
|
|
588
|
+
const passed = Boolean(target.lifecycleStatus[name]);
|
|
589
|
+
return ` [${statusMark(passed)}] ${lifecycleLabel(name)} ${lifecycleStatusText(passed)}`;
|
|
590
|
+
});
|
|
591
|
+
const urlStatus = targetUrlStatusText(target);
|
|
592
|
+
const detailLines = [` events: ${target.eventCount}`];
|
|
593
|
+
if (urlStatus !== null) {
|
|
594
|
+
detailLines.push(` url: ${urlStatus}`);
|
|
595
|
+
}
|
|
596
|
+
if (target.unexpectedLifecycle.length) {
|
|
597
|
+
detailLines.push(
|
|
598
|
+
` unexpected: ${target.unexpectedLifecycle.map(lifecycleLabel).join(", ")}`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
return [
|
|
602
|
+
`[${statusMark(target.passed)}] ${target.kind}:${target.serviceKey}`,
|
|
603
|
+
...lifecycleLines,
|
|
604
|
+
...detailLines,
|
|
605
|
+
].join("\n");
|
|
562
606
|
})
|
|
607
|
+
.join("\n\n");
|
|
608
|
+
|
|
609
|
+
const renderSetupCheckEntries = (entries) =>
|
|
610
|
+
entries
|
|
611
|
+
.map(([id, status]) => `${status === "passed" ? "[x]" : "[ ]"} ${id}`)
|
|
563
612
|
.join("\n");
|
|
564
613
|
|
|
614
|
+
const joinRenderedSections = (...sections) =>
|
|
615
|
+
sections.filter((section) => section.trim()).join("\n\n");
|
|
616
|
+
|
|
617
|
+
const writeChangedSetupWatchSnapshot = ({ rendered, state }) => {
|
|
618
|
+
if (!rendered || rendered === state.lastRendered) return;
|
|
619
|
+
process.stdout.write(`${rendered}\n\n`);
|
|
620
|
+
state.lastRendered = rendered;
|
|
621
|
+
};
|
|
622
|
+
|
|
565
623
|
const setupCheckUrl = ({
|
|
566
624
|
clueApiBaseUrl,
|
|
567
625
|
environment,
|
|
@@ -621,7 +679,7 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
621
679
|
const startedAt = String(
|
|
622
680
|
flags.get("started-at") || new Date().toISOString(),
|
|
623
681
|
).trim();
|
|
624
|
-
const timeoutMs =
|
|
682
|
+
const timeoutMs = parseSetupWatchTimeoutMs({ flags, localMode });
|
|
625
683
|
const pollIntervalMs = Number(flags.get("poll-interval-ms") || 3000);
|
|
626
684
|
const limit = Number(flags.get("limit") || 200);
|
|
627
685
|
const projectId = flags.get("project-id");
|
|
@@ -639,6 +697,11 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
639
697
|
"setup-watch requires --watch-targets or .clue/setup-manifest.json lifecycle_verification.watch_targets before setup can be treated as verified",
|
|
640
698
|
);
|
|
641
699
|
}
|
|
700
|
+
if (localMode && watchTargets.length === 0) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
"setup-watch --local requires .clue/setup-manifest.json lifecycle_verification.watch_targets",
|
|
703
|
+
);
|
|
704
|
+
}
|
|
642
705
|
const producerIds = buildWatchProducerIds({
|
|
643
706
|
explicitProducerIds: flags.get("producer-ids"),
|
|
644
707
|
watchTargets,
|
|
@@ -664,7 +727,13 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
664
727
|
`Watching targets: ${watchTargets.map((target) => `${target.kind}:${target.serviceKey}`).join(", ")}\n`,
|
|
665
728
|
);
|
|
666
729
|
}
|
|
667
|
-
|
|
730
|
+
process.stdout.write(
|
|
731
|
+
timeoutMs === null
|
|
732
|
+
? "No automatic timeout in local mode. Operate your local app, then press Ctrl+C to stop if needed.\n"
|
|
733
|
+
: `Timeout: ${timeoutMs}ms\n`,
|
|
734
|
+
);
|
|
735
|
+
const renderState = { lastRendered: "" };
|
|
736
|
+
while (!isSetupWatchTimedOut({ started, timeoutMs })) {
|
|
668
737
|
latest = localSetupCheckSnapshot({
|
|
669
738
|
receivedBatches: receiver.receivedBatches,
|
|
670
739
|
});
|
|
@@ -676,14 +745,19 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
676
745
|
targetChecks.length > 0 &&
|
|
677
746
|
targetChecks.every((target) => target.passed);
|
|
678
747
|
const renderedTargets = renderWatchTargets(targetChecks);
|
|
679
|
-
|
|
748
|
+
writeChangedSetupWatchSnapshot({
|
|
749
|
+
rendered: renderedTargets,
|
|
750
|
+
state: renderState,
|
|
751
|
+
});
|
|
680
752
|
if (targetChecksPassed) {
|
|
681
753
|
process.stdout.write("Clue local setup checks passed.\n");
|
|
682
754
|
return { ...latest, local: true, watchTargets: targetChecks };
|
|
683
755
|
}
|
|
684
756
|
await sleep(pollIntervalMs);
|
|
685
757
|
}
|
|
686
|
-
|
|
758
|
+
if (flags.has("json")) {
|
|
759
|
+
process.stdout.write(`${JSON.stringify(latest, null, 2)}\n`);
|
|
760
|
+
}
|
|
687
761
|
throw new Error(
|
|
688
762
|
"setup-watch --local timed out before all Clue setup checks passed",
|
|
689
763
|
);
|
|
@@ -701,7 +775,8 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
701
775
|
);
|
|
702
776
|
}
|
|
703
777
|
|
|
704
|
-
|
|
778
|
+
const renderState = { lastRendered: "" };
|
|
779
|
+
while (!isSetupWatchTimedOut({ started, timeoutMs })) {
|
|
705
780
|
const response = await fetch(
|
|
706
781
|
setupCheckUrl({
|
|
707
782
|
clueApiBaseUrl,
|
|
@@ -724,13 +799,12 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
724
799
|
const targetChecks = await evaluateWatchTargets({ latest, watchTargets });
|
|
725
800
|
const targetChecksPassed = targetChecks.every((target) => target.passed);
|
|
726
801
|
const passed = setupChecksPassed && targetChecksPassed;
|
|
727
|
-
const rendered = entries
|
|
728
|
-
.map(([id, status]) => `${status === "passed" ? "[x]" : "[ ]"} ${id}`)
|
|
729
|
-
.join("\n");
|
|
802
|
+
const rendered = renderSetupCheckEntries(entries);
|
|
730
803
|
const renderedTargets = renderWatchTargets(targetChecks);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
804
|
+
writeChangedSetupWatchSnapshot({
|
|
805
|
+
rendered: joinRenderedSections(rendered, renderedTargets),
|
|
806
|
+
state: renderState,
|
|
807
|
+
});
|
|
734
808
|
if (passed) {
|
|
735
809
|
process.stdout.write("Clue setup checks passed.\n");
|
|
736
810
|
return { ...latest, watchTargets: targetChecks };
|
|
@@ -738,7 +812,9 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
738
812
|
await sleep(pollIntervalMs);
|
|
739
813
|
}
|
|
740
814
|
|
|
741
|
-
|
|
815
|
+
if (flags.has("json")) {
|
|
816
|
+
process.stdout.write(`${JSON.stringify(latest, null, 2)}\n`);
|
|
817
|
+
}
|
|
742
818
|
throw new Error("setup-watch timed out before all Clue setup checks passed");
|
|
743
819
|
};
|
|
744
820
|
|
package/package.json
CHANGED
package/src/lifecycle-init.mjs
CHANGED
|
@@ -568,12 +568,16 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
568
568
|
"Prefer stable ids and non-PII booleans/counts for ClueIdentify and ClueSetAccount traits.",
|
|
569
569
|
"Use environment variable names for Clue configuration values.",
|
|
570
570
|
"For Python/FastAPI code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_API_KEY, and CLUE_INGEST_ENDPOINT from the backend env block.",
|
|
571
|
-
"CLUE_API_BASE_URL is
|
|
572
|
-
"For browser code, read
|
|
571
|
+
"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.",
|
|
572
|
+
"For Next.js browser/client code, read NEXT_PUBLIC_CLUE_PROJECT_KEY, NEXT_PUBLIC_CLUE_ENVIRONMENT, NEXT_PUBLIC_CLUE_SERVICE_KEY, and NEXT_PUBLIC_CLUE_INGEST_ENDPOINT from the frontend .env.local block.",
|
|
573
|
+
"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.",
|
|
574
|
+
"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.",
|
|
573
575
|
"Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
574
576
|
"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.",
|
|
575
577
|
"Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
|
|
576
578
|
"The browser token request must include project key, environment, service key, and the current browser origin; the backend must attach x-clue-api-key server-side when calling Clue.",
|
|
579
|
+
"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.",
|
|
580
|
+
"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.",
|
|
577
581
|
"Prefer minimal edits that engineers can review in one PR.",
|
|
578
582
|
"Do not run broad formatters, import sorters, cleanup tools, or style-only edits. Whitespace-only changes are allowed only on lines directly changed for Clue SDK wiring.",
|
|
579
583
|
"If a lifecycle point is unclear, skip that edit and include a warning.",
|
|
@@ -584,13 +588,17 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
584
588
|
target_tool: request.target_tool,
|
|
585
589
|
framework: request.framework,
|
|
586
590
|
project_key_env: "CLUE_PROJECT_KEY",
|
|
587
|
-
browser_project_key_env: "
|
|
591
|
+
browser_project_key_env: "framework_specific",
|
|
592
|
+
nextjs_browser_project_key_env: "NEXT_PUBLIC_CLUE_PROJECT_KEY",
|
|
588
593
|
environment_env: "CLUE_ENVIRONMENT",
|
|
589
|
-
browser_environment_env: "
|
|
594
|
+
browser_environment_env: "framework_specific",
|
|
595
|
+
nextjs_browser_environment_env: "NEXT_PUBLIC_CLUE_ENVIRONMENT",
|
|
590
596
|
clue_ingest_endpoint_env: "CLUE_INGEST_ENDPOINT",
|
|
591
597
|
tooling_api_base_url_env: "CLUE_API_BASE_URL",
|
|
592
|
-
browser_ingest_endpoint_env: "
|
|
598
|
+
browser_ingest_endpoint_env: "framework_specific",
|
|
599
|
+
nextjs_browser_ingest_endpoint_env: "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
593
600
|
browser_token_endpoint_path: "/api/v1/ingest/browser-tokens",
|
|
601
|
+
nextjs_browser_service_key_env: "NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
594
602
|
service_key: request.service_key,
|
|
595
603
|
},
|
|
596
604
|
output_shape: {
|
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.v3";
|
|
31
31
|
const REQUIRED_SETUP_SKILL_PHRASES = {
|
|
32
32
|
"clue-sdk-instrumentation": [
|
|
33
33
|
"Do not create no-op wrappers",
|
|
@@ -35,7 +35,10 @@ 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",
|
|
40
43
|
"For Django code, use `clue-django-sdk` only after package-manager or registry verification confirms it is installable",
|
|
41
44
|
],
|
|
@@ -43,6 +46,7 @@ const REQUIRED_SETUP_SKILL_PHRASES = {
|
|
|
43
46
|
"Reject wrong SDK package names",
|
|
44
47
|
"Reject Django SDK setup when `clue-django-sdk` installability has not been verified",
|
|
45
48
|
"Reject ClueTrack instrumentation unless the user explicitly requested product event tracking",
|
|
49
|
+
"Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables",
|
|
46
50
|
"Reject whitespace-only edits, import sorting, formatter churn",
|
|
47
51
|
"Reject unrelated refactors, renames, file moves",
|
|
48
52
|
"Execution agents must not approve, certify, or mark their own work complete",
|
|
@@ -225,6 +229,43 @@ const validateSetupManifestContract = (manifest) => {
|
|
|
225
229
|
"required_final_verification.local_event_delivery must report user_verification_pending without user evidence",
|
|
226
230
|
);
|
|
227
231
|
}
|
|
232
|
+
const watchTargets = Array.isArray(
|
|
233
|
+
manifest.lifecycle_verification?.watch_targets,
|
|
234
|
+
)
|
|
235
|
+
? manifest.lifecycle_verification.watch_targets
|
|
236
|
+
: [];
|
|
237
|
+
const hasNextFrontend = watchTargets.some(
|
|
238
|
+
(target) => target?.kind === "frontend" && target?.framework === "nextjs",
|
|
239
|
+
);
|
|
240
|
+
const hasFrontend = watchTargets.some((target) => target?.kind === "frontend");
|
|
241
|
+
const frontendRuntime = Array.isArray(
|
|
242
|
+
manifest.required_env_scopes?.frontend_runtime,
|
|
243
|
+
)
|
|
244
|
+
? manifest.required_env_scopes.frontend_runtime
|
|
245
|
+
: [];
|
|
246
|
+
const backendRuntime = Array.isArray(
|
|
247
|
+
manifest.required_env_scopes?.backend_runtime,
|
|
248
|
+
)
|
|
249
|
+
? manifest.required_env_scopes.backend_runtime
|
|
250
|
+
: [];
|
|
251
|
+
if (
|
|
252
|
+
hasNextFrontend &&
|
|
253
|
+
![
|
|
254
|
+
"NEXT_PUBLIC_CLUE_PROJECT_KEY",
|
|
255
|
+
"NEXT_PUBLIC_CLUE_ENVIRONMENT",
|
|
256
|
+
"NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
257
|
+
"NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
258
|
+
].every((name) => frontendRuntime.includes(name))
|
|
259
|
+
) {
|
|
260
|
+
findings.push(
|
|
261
|
+
"Next.js frontend runtime env must use NEXT_PUBLIC_CLUE_* names",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (hasFrontend && !backendRuntime.includes("CLUE_API_BASE_URL")) {
|
|
265
|
+
findings.push(
|
|
266
|
+
"backend_runtime must include CLUE_API_BASE_URL when a frontend browser token proxy can be required",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
228
269
|
return {
|
|
229
270
|
checked: true,
|
|
230
271
|
findings,
|
|
@@ -372,6 +413,57 @@ const packageJsonDependencyNames = (text) => {
|
|
|
372
413
|
}
|
|
373
414
|
};
|
|
374
415
|
|
|
416
|
+
const packageJsonDependencies = (text) => {
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(text);
|
|
419
|
+
return [
|
|
420
|
+
"dependencies",
|
|
421
|
+
"devDependencies",
|
|
422
|
+
"optionalDependencies",
|
|
423
|
+
"peerDependencies",
|
|
424
|
+
].reduce((result, field) => {
|
|
425
|
+
if (parsed && typeof parsed[field] === "object" && parsed[field] !== null) {
|
|
426
|
+
for (const [name, version] of Object.entries(parsed[field])) {
|
|
427
|
+
if (typeof version === "string") {
|
|
428
|
+
result.set(name, version);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}, new Map());
|
|
434
|
+
} catch {
|
|
435
|
+
return new Map();
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const packageJsonHasDependency = (text, packageName) =>
|
|
440
|
+
packageJsonDependencies(text).has(packageName);
|
|
441
|
+
|
|
442
|
+
const packageJsonDependencyVersion = (text, packageName) =>
|
|
443
|
+
packageJsonDependencies(text).get(packageName) ?? null;
|
|
444
|
+
|
|
445
|
+
const packageJsonSourcesWithDependency = (dependencySources, packageName) =>
|
|
446
|
+
dependencySources.filter(
|
|
447
|
+
(source) =>
|
|
448
|
+
source.file_path.endsWith("package.json") &&
|
|
449
|
+
packageJsonHasDependency(source.text, packageName),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const nextPackageRoots = (dependencySources) =>
|
|
453
|
+
packageJsonSourcesWithDependency(dependencySources, "next").map((source) =>
|
|
454
|
+
dirname(source.file_path),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const sourceIsUnderAnyRoot = (source, roots) =>
|
|
458
|
+
roots.some((root) => {
|
|
459
|
+
const normalizedRoot = root.replace(/\/+$/, "");
|
|
460
|
+
return (
|
|
461
|
+
normalizedRoot === "." ||
|
|
462
|
+
normalizedRoot === "" ||
|
|
463
|
+
startsWithRoot(source.file_path, normalizedRoot)
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
375
467
|
const dependencySourceHasPackage = (source, packageName) => {
|
|
376
468
|
if (source.file_path.endsWith("package.json")) {
|
|
377
469
|
return packageJsonDependencyNames(source.text).includes(packageName);
|
|
@@ -631,6 +723,47 @@ const findWrongFrontendSdkPackages = ({ sources, dependencySources }) => {
|
|
|
631
723
|
);
|
|
632
724
|
};
|
|
633
725
|
|
|
726
|
+
const NEXT_PUBLIC_CLUE_NAMES = [
|
|
727
|
+
"CLUE_PROJECT_KEY",
|
|
728
|
+
"CLUE_ENVIRONMENT",
|
|
729
|
+
"CLUE_SERVICE_KEY",
|
|
730
|
+
"CLUE_INGEST_ENDPOINT",
|
|
731
|
+
];
|
|
732
|
+
|
|
733
|
+
const findNextLifecycleNonPublicEnvFiles = ({
|
|
734
|
+
dependencySources,
|
|
735
|
+
frontendSources,
|
|
736
|
+
}) => {
|
|
737
|
+
const roots = nextPackageRoots(dependencySources);
|
|
738
|
+
if (roots.length === 0) return [];
|
|
739
|
+
return frontendSources
|
|
740
|
+
.filter((source) => sourceIsUnderAnyRoot(source, roots))
|
|
741
|
+
.filter(
|
|
742
|
+
(source) =>
|
|
743
|
+
sourceImportsFrontendSdk(source) ||
|
|
744
|
+
findLifecycleCallApiNames(
|
|
745
|
+
stripSourceNoise(source.text, { stripStrings: true }),
|
|
746
|
+
).length > 0,
|
|
747
|
+
)
|
|
748
|
+
.filter((source) =>
|
|
749
|
+
NEXT_PUBLIC_CLUE_NAMES.some((name) =>
|
|
750
|
+
new RegExp(`process\\.env\\.${name}\\b`).test(source.text),
|
|
751
|
+
),
|
|
752
|
+
)
|
|
753
|
+
.map((source) => source.file_path);
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const findWildcardFrontendSdkDependencyFiles = (dependencySources) =>
|
|
757
|
+
packageJsonSourcesWithDependency(dependencySources, FRONTEND_SDK_PACKAGE)
|
|
758
|
+
.filter((source) => {
|
|
759
|
+
const version = packageJsonDependencyVersion(
|
|
760
|
+
source.text,
|
|
761
|
+
FRONTEND_SDK_PACKAGE,
|
|
762
|
+
);
|
|
763
|
+
return version === "*" || version === "latest";
|
|
764
|
+
})
|
|
765
|
+
.map((source) => source.file_path);
|
|
766
|
+
|
|
634
767
|
const backendSdkSpec = (framework) =>
|
|
635
768
|
BACKEND_SDK_BY_FRAMEWORK[String(framework ?? "").toLowerCase()] ?? {
|
|
636
769
|
packages: Object.values(BACKEND_SDK_BY_FRAMEWORK).flatMap(
|
|
@@ -743,6 +876,12 @@ const checkSdkLifecycle = ({
|
|
|
743
876
|
sources: frontendSources,
|
|
744
877
|
dependencySources,
|
|
745
878
|
});
|
|
879
|
+
const nextLifecycleNonPublicEnvFiles = findNextLifecycleNonPublicEnvFiles({
|
|
880
|
+
dependencySources,
|
|
881
|
+
frontendSources,
|
|
882
|
+
});
|
|
883
|
+
const wildcardFrontendSdkDependencyFiles =
|
|
884
|
+
findWildcardFrontendSdkDependencyFiles(dependencySources);
|
|
746
885
|
const backendIdentityRequired =
|
|
747
886
|
backendPresent &&
|
|
748
887
|
/\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
|
|
@@ -799,6 +938,8 @@ const checkSdkLifecycle = ({
|
|
|
799
938
|
lifecycle_files_without_verified_sdk:
|
|
800
939
|
frontendLifecycleFilesWithoutVerifiedSdk,
|
|
801
940
|
wrong_sdk_packages: wrongFrontendSdkPackages,
|
|
941
|
+
next_lifecycle_non_public_env_files: nextLifecycleNonPublicEnvFiles,
|
|
942
|
+
wildcard_sdk_dependency_files: wildcardFrontendSdkDependencyFiles,
|
|
802
943
|
},
|
|
803
944
|
has_noop_wrapper: noOpPattern.test(combined),
|
|
804
945
|
component_lifecycle_init_files: componentLifecycleInitFiles,
|
|
@@ -815,6 +956,8 @@ const checkSdkLifecycle = ({
|
|
|
815
956
|
frontendSdkPresent &&
|
|
816
957
|
frontendLifecycleFilesWithoutVerifiedSdk.length === 0 &&
|
|
817
958
|
wrongFrontendSdkPackages.length === 0 &&
|
|
959
|
+
nextLifecycleNonPublicEnvFiles.length === 0 &&
|
|
960
|
+
wildcardFrontendSdkDependencyFiles.length === 0 &&
|
|
818
961
|
!noOpPattern.test(combined) &&
|
|
819
962
|
componentLifecycleInitFiles.length === 0 &&
|
|
820
963
|
blockingLifecycleFiles.length === 0,
|
package/src/setup-help.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
clueCliCommand,
|
|
4
4
|
} from "./cli-invocation.mjs";
|
|
5
5
|
|
|
6
|
-
export const AI_SETUP_HELP_VERSION = "2026-05-10.lifecycle-placement-only.
|
|
6
|
+
export const AI_SETUP_HELP_VERSION = "2026-05-10.lifecycle-placement-only.v3";
|
|
7
7
|
|
|
8
8
|
export const buildAiSetupHelp = () => ({
|
|
9
9
|
name: "@clue-ai/cli AI setup help",
|
|
@@ -43,6 +43,21 @@ export const buildAiSetupHelp = () => ({
|
|
|
43
43
|
"whitespace-only edits, import sorting, formatter churn, or comment/style cleanup outside the exact Clue SDK wiring lines",
|
|
44
44
|
],
|
|
45
45
|
},
|
|
46
|
+
environment_contract: {
|
|
47
|
+
nextjs_frontend_client_env: {
|
|
48
|
+
variables: [
|
|
49
|
+
"NEXT_PUBLIC_CLUE_PROJECT_KEY",
|
|
50
|
+
"NEXT_PUBLIC_CLUE_ENVIRONMENT",
|
|
51
|
+
"NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
52
|
+
"NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
53
|
+
],
|
|
54
|
+
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.",
|
|
55
|
+
},
|
|
56
|
+
backend_browser_token_proxy_env: {
|
|
57
|
+
variables: ["CLUE_API_KEY", "CLUE_API_BASE_URL"],
|
|
58
|
+
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.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
46
61
|
setup_watch: {
|
|
47
62
|
owner: "user",
|
|
48
63
|
ai_agent_must_run: false,
|
package/src/setup-prepare.mjs
CHANGED
|
@@ -14,6 +14,19 @@ const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
|
14
14
|
const DEFAULT_ENV_GUIDE_PATH = ".env.clue";
|
|
15
15
|
const BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
|
|
16
16
|
const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
|
|
17
|
+
const FRONTEND_PUBLIC_ENV_NAMES = [
|
|
18
|
+
"CLUE_INGEST_ENDPOINT",
|
|
19
|
+
"CLUE_PROJECT_KEY",
|
|
20
|
+
"CLUE_ENVIRONMENT",
|
|
21
|
+
"CLUE_SERVICE_KEY",
|
|
22
|
+
];
|
|
23
|
+
const BACKEND_RUNTIME_ENV_NAMES = [
|
|
24
|
+
"CLUE_SERVICE_KEY",
|
|
25
|
+
"CLUE_PROJECT_KEY",
|
|
26
|
+
"CLUE_ENVIRONMENT",
|
|
27
|
+
"CLUE_INGEST_ENDPOINT",
|
|
28
|
+
"CLUE_API_KEY",
|
|
29
|
+
];
|
|
17
30
|
const AI_PROVIDER_GUIDES = {
|
|
18
31
|
codex: {
|
|
19
32
|
provider: "openai",
|
|
@@ -165,24 +178,67 @@ const envFileCandidates = (target) => {
|
|
|
165
178
|
return [".env"];
|
|
166
179
|
};
|
|
167
180
|
|
|
168
|
-
const
|
|
181
|
+
const frontendEnvName = ({ target, name }) =>
|
|
182
|
+
target.kind === "frontend" && target.framework === "nextjs"
|
|
183
|
+
? `NEXT_PUBLIC_${name}`
|
|
184
|
+
: name;
|
|
185
|
+
|
|
186
|
+
const buildServiceEnvBlock = ({
|
|
187
|
+
target,
|
|
188
|
+
setupContext,
|
|
189
|
+
includeBrowserTokenProxyConfig = false,
|
|
190
|
+
}) => {
|
|
169
191
|
const ingestPath =
|
|
170
192
|
target.kind === "frontend" ? BROWSER_INGEST_PATH : BACKEND_INGEST_PATH;
|
|
171
193
|
const variables = [
|
|
172
194
|
{
|
|
173
|
-
name: "CLUE_INGEST_ENDPOINT",
|
|
195
|
+
name: frontendEnvName({ target, name: "CLUE_INGEST_ENDPOINT" }),
|
|
174
196
|
value: buildEndpoint(setupContext.clue_api_base_url, ingestPath),
|
|
175
197
|
},
|
|
176
|
-
{
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
{
|
|
199
|
+
name: frontendEnvName({ target, name: "CLUE_PROJECT_KEY" }),
|
|
200
|
+
value: setupContext.project_key,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: frontendEnvName({ target, name: "CLUE_ENVIRONMENT" }),
|
|
204
|
+
value: setupContext.environment,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: frontendEnvName({ target, name: "CLUE_SERVICE_KEY" }),
|
|
208
|
+
value: target.service_key,
|
|
209
|
+
},
|
|
179
210
|
];
|
|
180
211
|
if (target.kind === "backend") {
|
|
181
212
|
variables.push({ name: "CLUE_API_KEY", value: setupContext.clue_api_key });
|
|
213
|
+
if (includeBrowserTokenProxyConfig) {
|
|
214
|
+
variables.push({
|
|
215
|
+
name: "CLUE_API_BASE_URL",
|
|
216
|
+
value: setupContext.clue_api_base_url,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
182
219
|
}
|
|
183
220
|
return variables.map(({ name, value }) => `${name}=${value}`).join("\n");
|
|
184
221
|
};
|
|
185
222
|
|
|
223
|
+
const requiredFrontendEnvNames = (watchTargets) => [
|
|
224
|
+
...new Set(
|
|
225
|
+
watchTargets
|
|
226
|
+
.filter((target) => target.kind === "frontend")
|
|
227
|
+
.flatMap((target) =>
|
|
228
|
+
FRONTEND_PUBLIC_ENV_NAMES.map((name) =>
|
|
229
|
+
frontendEnvName({ target, name }),
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
),
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
const requiredBackendEnvNames = (watchTargets) => [
|
|
236
|
+
...BACKEND_RUNTIME_ENV_NAMES,
|
|
237
|
+
...(watchTargets.some((target) => target.kind === "frontend")
|
|
238
|
+
? ["CLUE_API_BASE_URL"]
|
|
239
|
+
: []),
|
|
240
|
+
];
|
|
241
|
+
|
|
186
242
|
const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
|
|
187
243
|
const missingFlags = [
|
|
188
244
|
["clue_api_key", "--clue-api-key"],
|
|
@@ -204,6 +260,9 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
|
|
|
204
260
|
|
|
205
261
|
const watchTargets = manifest.lifecycle_verification.watch_targets;
|
|
206
262
|
const aiProviderGuide = aiProviderGuideForTarget(manifest.target);
|
|
263
|
+
const includeBrowserTokenProxyConfig = watchTargets.some(
|
|
264
|
+
(target) => target.kind === "frontend",
|
|
265
|
+
);
|
|
207
266
|
return {
|
|
208
267
|
status: "ready",
|
|
209
268
|
env_file_path: DEFAULT_ENV_GUIDE_PATH,
|
|
@@ -214,7 +273,11 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
|
|
|
214
273
|
root_path: target.root_path,
|
|
215
274
|
service_key: target.service_key,
|
|
216
275
|
env_file_candidates: envFileCandidates(target),
|
|
217
|
-
env_block: buildServiceEnvBlock({
|
|
276
|
+
env_block: buildServiceEnvBlock({
|
|
277
|
+
target,
|
|
278
|
+
setupContext,
|
|
279
|
+
includeBrowserTokenProxyConfig,
|
|
280
|
+
}),
|
|
218
281
|
})),
|
|
219
282
|
ci_github: {
|
|
220
283
|
secrets: [
|
|
@@ -371,6 +434,12 @@ export const runSetupPrepare = async ({
|
|
|
371
434
|
repoRoot: resolvedRepoRoot,
|
|
372
435
|
request,
|
|
373
436
|
});
|
|
437
|
+
const watchTargets = buildWatchTargets(detection, candidate);
|
|
438
|
+
const frontendRuntimeEnvNames = requiredFrontendEnvNames(watchTargets);
|
|
439
|
+
const backendRuntimeEnvNames = requiredBackendEnvNames(watchTargets);
|
|
440
|
+
const serviceRuntimeEnvNames = [
|
|
441
|
+
...new Set([...frontendRuntimeEnvNames, ...backendRuntimeEnvNames]),
|
|
442
|
+
];
|
|
374
443
|
|
|
375
444
|
const manifest = {
|
|
376
445
|
status: "ready_for_ai",
|
|
@@ -384,7 +453,11 @@ export const runSetupPrepare = async ({
|
|
|
384
453
|
service_identity: {
|
|
385
454
|
canonical_field: "service_key",
|
|
386
455
|
backend_env_name: "CLUE_SERVICE_KEY",
|
|
387
|
-
frontend_env_name: "
|
|
456
|
+
frontend_env_name: "framework_specific",
|
|
457
|
+
frontend_env_names_by_framework: {
|
|
458
|
+
nextjs: "NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
459
|
+
default: "CLUE_SERVICE_KEY",
|
|
460
|
+
},
|
|
388
461
|
producer_id_derivation: "producer_id defaults to service_key",
|
|
389
462
|
},
|
|
390
463
|
cli_invocation: CLUE_CLI_INVOCATION_CONTRACT,
|
|
@@ -411,7 +484,7 @@ export const runSetupPrepare = async ({
|
|
|
411
484
|
watch_target_format:
|
|
412
485
|
"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
486
|
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:
|
|
487
|
+
watch_targets: watchTargets,
|
|
415
488
|
},
|
|
416
489
|
artifacts: {
|
|
417
490
|
ci_workflow_path: workflow.ci_workflow_path,
|
|
@@ -469,24 +542,21 @@ export const runSetupPrepare = async ({
|
|
|
469
542
|
},
|
|
470
543
|
],
|
|
471
544
|
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",
|
|
545
|
+
...new Set([
|
|
546
|
+
...serviceRuntimeEnvNames,
|
|
547
|
+
"CLUE_API_KEY",
|
|
548
|
+
"CLUE_AI_PROVIDER",
|
|
549
|
+
"CLUE_AI_PROVIDER_API_KEY",
|
|
550
|
+
"CLUE_AI_MODEL",
|
|
485
551
|
"CLUE_PROJECT_KEY",
|
|
486
552
|
"CLUE_ENVIRONMENT",
|
|
487
|
-
"
|
|
488
|
-
|
|
489
|
-
|
|
553
|
+
"CLUE_API_BASE_URL",
|
|
554
|
+
]),
|
|
555
|
+
],
|
|
556
|
+
required_env_scopes: {
|
|
557
|
+
service_runtime: serviceRuntimeEnvNames,
|
|
558
|
+
frontend_runtime: frontendRuntimeEnvNames,
|
|
559
|
+
backend_runtime: backendRuntimeEnvNames,
|
|
490
560
|
github_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
|
|
491
561
|
github_variables: [
|
|
492
562
|
"CLUE_PROJECT_KEY",
|
package/src/setup-tool.mjs
CHANGED
|
@@ -17,7 +17,7 @@ const SKILL_NAMES = [
|
|
|
17
17
|
"clue-local-verification",
|
|
18
18
|
"clue-setup-report",
|
|
19
19
|
];
|
|
20
|
-
const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.
|
|
20
|
+
const SETUP_SKILL_CONTENT_VERSION = "2026-05-10.lifecycle-placement-only.v3";
|
|
21
21
|
|
|
22
22
|
const TARGETS = new Set(["codex", "claude_code"]);
|
|
23
23
|
|
|
@@ -266,15 +266,19 @@ const skillBody = (name) => {
|
|
|
266
266
|
"Delete the temporary lifecycle plan file after applying it unless the user explicitly asks to keep it for review.",
|
|
267
267
|
"Use environment variable names for Clue configuration values; do not paste project keys or API keys into code.",
|
|
268
268
|
`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\`.`,
|
|
269
|
-
"For browser code, use `
|
|
269
|
+
"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.",
|
|
270
|
+
"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.",
|
|
271
|
+
"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.",
|
|
270
272
|
"Never put `CLUE_API_KEY` in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
271
273
|
"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.",
|
|
272
274
|
"Configure frontend `ClueInit` with `browserTokenProvider` that calls the local backend token endpoint and returns the token string.",
|
|
273
275
|
"The browser token request must include project key, environment, service key, and the current browser origin; the backend must attach `x-clue-api-key` server-side when calling Clue.",
|
|
276
|
+
"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.",
|
|
274
277
|
"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.",
|
|
275
|
-
"`CLUE_API_BASE_URL` is
|
|
278
|
+
"`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.",
|
|
279
|
+
"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.",
|
|
276
280
|
"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.",
|
|
277
|
-
"For frontend code, pass `serviceKey` from
|
|
281
|
+
"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`.",
|
|
278
282
|
"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.",
|
|
279
283
|
"For other backend frameworks, use the matching Clue backend SDK if one exists; if no backend SDK exists, report a blocker instead of silently frontend-only setup.",
|
|
280
284
|
"Do not send raw email, raw person names, tokens, workspace names, organization names, or tenant names as lifecycle traits unless the repository already has an explicit Clue privacy policy allowing them.",
|
|
@@ -305,6 +309,8 @@ const skillBody = (name) => {
|
|
|
305
309
|
"Reject ClueInit inside React component lifecycle hooks, page components, sidebars, login/register success callbacks, or any repeated user interaction path.",
|
|
306
310
|
"Reject broad ClueTrack instrumentation and DOM clue tags.",
|
|
307
311
|
"Reject ClueTrack instrumentation unless the user explicitly requested product event tracking.",
|
|
312
|
+
"Reject Next.js browser/client code that reads non-public `process.env.CLUE_*` variables.",
|
|
313
|
+
"Reject Clue SDK dependency entries that use `*` or `latest` instead of a concrete published version or package-manager-resolved semver range.",
|
|
308
314
|
"Confirm no project key, API key, secret, or env value appears in diff or report.",
|
|
309
315
|
"Confirm lifecycle insertions are minimal and reviewable.",
|
|
310
316
|
"Reject whitespace-only edits, import sorting, formatter churn, or comment/style cleanup outside the exact Clue SDK wiring lines.",
|
|
@@ -343,7 +349,7 @@ const skillBody = (name) => {
|
|
|
343
349
|
"List skills used for each workstream.",
|
|
344
350
|
"List execution agent and monitoring agents, or named review passes if subagents were unavailable.",
|
|
345
351
|
"List blockers with exact file or environment names when available.",
|
|
346
|
-
"List required env names without values and group them by scope:
|
|
352
|
+
"List required env names without values and group them by scope: frontend runtime, backend runtime, GitHub Secrets, and GitHub Variables. Do not report `CLUE_API_BASE_URL` as a backend SDK init env; it belongs to Clue CLI/semantic CI and to backend browser-token proxy config only when a frontend service exists.",
|
|
347
353
|
"List P0/P1 monitoring findings and fixes.",
|
|
348
354
|
"State that commit and push were not performed.",
|
|
349
355
|
],
|