@clue-ai/cli 0.0.22 → 0.0.24
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/bin/clue-cli.mjs +16 -1
- package/package.json +1 -1
- package/src/ai-provider.mjs +62 -4
- package/src/lifecycle-init.mjs +208 -14
- package/src/setup-agent.mjs +235 -4
- package/src/setup-ai-contract.mjs +89 -4
- package/src/setup-check.mjs +132 -9
- package/src/setup-detect.mjs +23 -1
- package/src/setup-documents.mjs +1 -1
- package/src/setup-help.mjs +2 -0
- package/src/setup-tool.mjs +17 -1
package/src/setup-agent.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, readFile } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
applyLifecyclePlan,
|
|
@@ -28,6 +28,8 @@ const DEPENDENCY_WRITE_FILE_CANDIDATES = [
|
|
|
28
28
|
"Pipfile.lock",
|
|
29
29
|
"poetry.lock",
|
|
30
30
|
];
|
|
31
|
+
const FRONTEND_SDK_PACKAGE = "@clue-ai/browser-sdk";
|
|
32
|
+
const BACKEND_FASTAPI_SDK_PACKAGE = "clue-fastapi-sdk";
|
|
31
33
|
|
|
32
34
|
const optionalString = (value) =>
|
|
33
35
|
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
@@ -100,6 +102,13 @@ const blockedMissingAiProviderConfig = ({ manifestPath, missingInputs }) => ({
|
|
|
100
102
|
missing_inputs: missingInputs,
|
|
101
103
|
report: null,
|
|
102
104
|
},
|
|
105
|
+
quality_gates: buildQualityGates({
|
|
106
|
+
aiConfigPresent: false,
|
|
107
|
+
setupDoctorResult: {
|
|
108
|
+
status: "skipped",
|
|
109
|
+
reason: "AI provider configuration is missing",
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
103
112
|
blockers: [
|
|
104
113
|
{
|
|
105
114
|
reason:
|
|
@@ -187,6 +196,143 @@ const discoveredDependencyWritePaths = async ({ repoRoot, sourcePaths }) => {
|
|
|
187
196
|
return discovered;
|
|
188
197
|
};
|
|
189
198
|
|
|
199
|
+
const writeJson = async ({ path, value }) => {
|
|
200
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const ensureFrontendSdkDependency = async ({ repoRoot, rootPath }) => {
|
|
204
|
+
const packageJsonPath = resolve(repoRoot, rootPath, "package.json");
|
|
205
|
+
if (!(await exists(packageJsonPath))) return null;
|
|
206
|
+
const packageJson = await readJson(packageJsonPath);
|
|
207
|
+
const dependencies =
|
|
208
|
+
packageJson.dependencies && typeof packageJson.dependencies === "object"
|
|
209
|
+
? packageJson.dependencies
|
|
210
|
+
: {};
|
|
211
|
+
if (dependencies[FRONTEND_SDK_PACKAGE] === "latest") return null;
|
|
212
|
+
packageJson.dependencies = {
|
|
213
|
+
...dependencies,
|
|
214
|
+
[FRONTEND_SDK_PACKAGE]: "latest",
|
|
215
|
+
};
|
|
216
|
+
await writeJson({ path: packageJsonPath, value: packageJson });
|
|
217
|
+
return {
|
|
218
|
+
file_path: join(rootPath, "package.json"),
|
|
219
|
+
dependency: FRONTEND_SDK_PACKAGE,
|
|
220
|
+
version: "latest",
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const ensureRequirementsDependency = async ({ repoRoot, path }) => {
|
|
225
|
+
const absolutePath = resolve(repoRoot, path);
|
|
226
|
+
if (!(await exists(absolutePath))) return null;
|
|
227
|
+
const text = await readFile(absolutePath, "utf8");
|
|
228
|
+
const rawLines = text.split(/\r?\n/);
|
|
229
|
+
while (rawLines.at(-1) === "") rawLines.pop();
|
|
230
|
+
const lines = rawLines.filter(
|
|
231
|
+
(line) => !new RegExp(`^\\s*${BACKEND_FASTAPI_SDK_PACKAGE}\\b`).test(line),
|
|
232
|
+
);
|
|
233
|
+
if (!lines.some((line) => line.trim() === BACKEND_FASTAPI_SDK_PACKAGE)) {
|
|
234
|
+
lines.push(BACKEND_FASTAPI_SDK_PACKAGE);
|
|
235
|
+
}
|
|
236
|
+
const next = `${lines.join("\n")}\n`;
|
|
237
|
+
if (next === text) return null;
|
|
238
|
+
await writeFile(absolutePath, next, "utf8");
|
|
239
|
+
return {
|
|
240
|
+
file_path: path,
|
|
241
|
+
dependency: BACKEND_FASTAPI_SDK_PACKAGE,
|
|
242
|
+
version: "latest",
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const ensureSdkDependencies = async ({ repoRoot, manifest, request }) => {
|
|
247
|
+
const writes = [];
|
|
248
|
+
const watchTargets = Array.isArray(
|
|
249
|
+
manifest.lifecycle_verification?.watch_targets,
|
|
250
|
+
)
|
|
251
|
+
? manifest.lifecycle_verification.watch_targets
|
|
252
|
+
: [];
|
|
253
|
+
for (const target of watchTargets) {
|
|
254
|
+
if (target?.kind !== "frontend" || !optionalString(target.root_path)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const write = await ensureFrontendSdkDependency({
|
|
258
|
+
repoRoot,
|
|
259
|
+
rootPath: target.root_path,
|
|
260
|
+
});
|
|
261
|
+
if (write) writes.push(write);
|
|
262
|
+
}
|
|
263
|
+
if (request.framework === "fastapi") {
|
|
264
|
+
const dependencyPaths = await discoveredDependencyWritePaths({
|
|
265
|
+
repoRoot,
|
|
266
|
+
sourcePaths: [request.backend_root_path],
|
|
267
|
+
});
|
|
268
|
+
const requirementsPath = dependencyPaths.find((path) =>
|
|
269
|
+
path.endsWith("requirements.txt"),
|
|
270
|
+
);
|
|
271
|
+
if (requirementsPath) {
|
|
272
|
+
const write = await ensureRequirementsDependency({
|
|
273
|
+
repoRoot,
|
|
274
|
+
path: requirementsPath,
|
|
275
|
+
});
|
|
276
|
+
if (write) writes.push(write);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return writes;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const planTouchedPaths = (plan) => [
|
|
283
|
+
...new Set(
|
|
284
|
+
[
|
|
285
|
+
...(Array.isArray(plan?.edits)
|
|
286
|
+
? plan.edits.map((edit) => edit?.file_path)
|
|
287
|
+
: []),
|
|
288
|
+
...(Array.isArray(plan?.file_creations)
|
|
289
|
+
? plan.file_creations.map((fileCreation) => fileCreation?.file_path)
|
|
290
|
+
: []),
|
|
291
|
+
...(Array.isArray(plan?.insertions)
|
|
292
|
+
? plan.insertions.map((insertion) => insertion?.file_path)
|
|
293
|
+
: []),
|
|
294
|
+
...(Array.isArray(plan?.lifecycle_insertions)
|
|
295
|
+
? plan.lifecycle_insertions.map((insertion) => insertion?.file_path)
|
|
296
|
+
: []),
|
|
297
|
+
]
|
|
298
|
+
.map(optionalString)
|
|
299
|
+
.filter(Boolean),
|
|
300
|
+
),
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
const snapshotPlanTouchedFiles = async ({ repoRoot, plan }) => {
|
|
304
|
+
const snapshot = new Map();
|
|
305
|
+
for (const filePath of planTouchedPaths(plan)) {
|
|
306
|
+
const absolutePath = resolve(repoRoot, filePath);
|
|
307
|
+
try {
|
|
308
|
+
snapshot.set(filePath, {
|
|
309
|
+
absolutePath,
|
|
310
|
+
existed: true,
|
|
311
|
+
text: await readFile(absolutePath, "utf8"),
|
|
312
|
+
});
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error?.code !== "ENOENT") throw error;
|
|
315
|
+
snapshot.set(filePath, {
|
|
316
|
+
absolutePath,
|
|
317
|
+
existed: false,
|
|
318
|
+
text: null,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return snapshot;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const restorePlanTouchedFiles = async (snapshot) => {
|
|
326
|
+
for (const entry of snapshot.values()) {
|
|
327
|
+
if (entry.existed) {
|
|
328
|
+
await mkdir(dirname(entry.absolutePath), { recursive: true });
|
|
329
|
+
await writeFile(entry.absolutePath, entry.text, "utf8");
|
|
330
|
+
} else {
|
|
331
|
+
await rm(entry.absolutePath, { force: true });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
190
336
|
const allowedWritePathsFromRequest = async ({ repoRoot, request }) => [
|
|
191
337
|
...new Set([
|
|
192
338
|
...request.allowed_source_paths,
|
|
@@ -274,6 +420,55 @@ const missingInputsFromDoctor = (report) => [
|
|
|
274
420
|
),
|
|
275
421
|
];
|
|
276
422
|
|
|
423
|
+
const buildQualityGates = ({
|
|
424
|
+
aiConfigPresent,
|
|
425
|
+
dependencyWrites = [],
|
|
426
|
+
lifecyclePlanApplied = false,
|
|
427
|
+
setupCheck = null,
|
|
428
|
+
setupDoctorResult = null,
|
|
429
|
+
}) => [
|
|
430
|
+
{
|
|
431
|
+
id: "ai_provider_config_present",
|
|
432
|
+
passed: Boolean(aiConfigPresent),
|
|
433
|
+
evidence: REQUIRED_SETUP_AGENT_AI_ENV_NAMES,
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: "sdk_dependencies_cli_owned",
|
|
437
|
+
passed: true,
|
|
438
|
+
evidence: {
|
|
439
|
+
dependency_writes: dependencyWrites,
|
|
440
|
+
note:
|
|
441
|
+
"setup-agent owns Clue SDK dependency declarations before AI planning",
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: "lifecycle_plan_cli_applied",
|
|
446
|
+
passed: Boolean(lifecyclePlanApplied),
|
|
447
|
+
evidence:
|
|
448
|
+
"AI returns a structured plan only; setup-agent validates and applies edits",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: "static_setup_check_passed",
|
|
452
|
+
passed: Boolean(setupCheck?.passed),
|
|
453
|
+
evidence: "setup-check --require-sdk-lifecycle",
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: "setup_doctor_preflight",
|
|
457
|
+
passed: setupDoctorResult?.status === "passed",
|
|
458
|
+
skipped: setupDoctorResult?.status === "skipped",
|
|
459
|
+
evidence:
|
|
460
|
+
setupDoctorResult?.status === "passed"
|
|
461
|
+
? "setup-doctor --local passed"
|
|
462
|
+
: setupDoctorResult?.reason ?? "setup-doctor --local did not pass",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: "setup_watch_user_owned",
|
|
466
|
+
passed: true,
|
|
467
|
+
evidence:
|
|
468
|
+
"setup-watch remains user-operated lifecycle and event-delivery verification",
|
|
469
|
+
},
|
|
470
|
+
];
|
|
471
|
+
|
|
277
472
|
const runOptionalSetupDoctor = async ({ env, flags, repoRoot, setupDoctor }) => {
|
|
278
473
|
if (flags.has("skip-setup-doctor")) {
|
|
279
474
|
return {
|
|
@@ -296,7 +491,7 @@ const runOptionalSetupDoctor = async ({ env, flags, repoRoot, setupDoctor }) =>
|
|
|
296
491
|
);
|
|
297
492
|
if (allFailuresAreMissingInputs) {
|
|
298
493
|
return {
|
|
299
|
-
status: "
|
|
494
|
+
status: "skipped",
|
|
300
495
|
reason: "setup-doctor required inputs are missing",
|
|
301
496
|
missing_inputs: missingInputs,
|
|
302
497
|
report,
|
|
@@ -430,10 +625,16 @@ export const runSetupAgent = async ({
|
|
|
430
625
|
});
|
|
431
626
|
}
|
|
432
627
|
const attempts = [];
|
|
628
|
+
const dependencyWrites = await ensureSdkDependencies({
|
|
629
|
+
repoRoot: resolvedRepoRoot,
|
|
630
|
+
manifest,
|
|
631
|
+
request,
|
|
632
|
+
});
|
|
433
633
|
let failureContext = null;
|
|
434
634
|
let lastSetupCheck = null;
|
|
435
635
|
|
|
436
636
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
637
|
+
let planSnapshot = null;
|
|
437
638
|
try {
|
|
438
639
|
const plan = await lifecyclePlanner({
|
|
439
640
|
repoRoot: resolvedRepoRoot,
|
|
@@ -441,6 +642,10 @@ export const runSetupAgent = async ({
|
|
|
441
642
|
env: aiEnv,
|
|
442
643
|
failureContext,
|
|
443
644
|
});
|
|
645
|
+
planSnapshot = await snapshotPlanTouchedFiles({
|
|
646
|
+
repoRoot: resolvedRepoRoot,
|
|
647
|
+
plan,
|
|
648
|
+
});
|
|
444
649
|
const lifecycleResult = await lifecycleApplier({
|
|
445
650
|
repoRoot: resolvedRepoRoot,
|
|
446
651
|
plan,
|
|
@@ -456,6 +661,9 @@ export const runSetupAgent = async ({
|
|
|
456
661
|
lastSetupCheck = setupCheck;
|
|
457
662
|
attempts.push({
|
|
458
663
|
attempt,
|
|
664
|
+
dependency_writes: dependencyWrites,
|
|
665
|
+
file_creations: lifecycleResult.fileCreations ?? [],
|
|
666
|
+
insertions: lifecycleResult.insertions ?? [],
|
|
459
667
|
lifecycle_insertions: lifecycleResult.lifecycleInsertions,
|
|
460
668
|
warnings: lifecycleResult.warnings,
|
|
461
669
|
setup_check_passed: setupCheck.passed,
|
|
@@ -470,14 +678,20 @@ export const runSetupAgent = async ({
|
|
|
470
678
|
});
|
|
471
679
|
return {
|
|
472
680
|
status:
|
|
473
|
-
setupDoctorResult.status
|
|
474
|
-
setupDoctorResult.status === "skipped"
|
|
681
|
+
setupDoctorResult.status !== "failed"
|
|
475
682
|
? "user_verification_pending"
|
|
476
683
|
: "blocked",
|
|
477
684
|
manifest_path: manifestPath,
|
|
478
685
|
attempts,
|
|
479
686
|
setup_check: setupCheck,
|
|
480
687
|
setup_doctor: setupDoctorResult,
|
|
688
|
+
quality_gates: buildQualityGates({
|
|
689
|
+
aiConfigPresent: true,
|
|
690
|
+
dependencyWrites,
|
|
691
|
+
lifecyclePlanApplied: true,
|
|
692
|
+
setupCheck,
|
|
693
|
+
setupDoctorResult,
|
|
694
|
+
}),
|
|
481
695
|
user_verification: {
|
|
482
696
|
setup_watch_owner: "user",
|
|
483
697
|
setup_watch_auto_run: false,
|
|
@@ -486,12 +700,16 @@ export const runSetupAgent = async ({
|
|
|
486
700
|
},
|
|
487
701
|
};
|
|
488
702
|
}
|
|
703
|
+
await restorePlanTouchedFiles(planSnapshot);
|
|
489
704
|
failureContext = attemptFailureContext({
|
|
490
705
|
attempt,
|
|
491
706
|
setupCheck,
|
|
492
707
|
stage: "setup_check",
|
|
493
708
|
});
|
|
494
709
|
} catch (error) {
|
|
710
|
+
if (planSnapshot) {
|
|
711
|
+
await restorePlanTouchedFiles(planSnapshot);
|
|
712
|
+
}
|
|
495
713
|
failureContext = attemptFailureContext({
|
|
496
714
|
attempt,
|
|
497
715
|
error,
|
|
@@ -499,6 +717,7 @@ export const runSetupAgent = async ({
|
|
|
499
717
|
});
|
|
500
718
|
attempts.push({
|
|
501
719
|
attempt,
|
|
720
|
+
dependency_writes: dependencyWrites,
|
|
502
721
|
setup_check_passed: false,
|
|
503
722
|
error: failureContext.error,
|
|
504
723
|
failed_checks: [],
|
|
@@ -517,6 +736,18 @@ export const runSetupAgent = async ({
|
|
|
517
736
|
missing_inputs: [],
|
|
518
737
|
report: null,
|
|
519
738
|
},
|
|
739
|
+
quality_gates: buildQualityGates({
|
|
740
|
+
aiConfigPresent: true,
|
|
741
|
+
dependencyWrites,
|
|
742
|
+
lifecyclePlanApplied: attempts.some(
|
|
743
|
+
(attempt) => Array.isArray(attempt.lifecycle_insertions),
|
|
744
|
+
),
|
|
745
|
+
setupCheck: lastSetupCheck,
|
|
746
|
+
setupDoctorResult: {
|
|
747
|
+
status: "skipped",
|
|
748
|
+
reason: "setup-check did not pass",
|
|
749
|
+
},
|
|
750
|
+
}),
|
|
520
751
|
blockers: [
|
|
521
752
|
{
|
|
522
753
|
reason: summarizeAttemptFailure(failureContext),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const AI_SETUP_CONTRACT_VERSION =
|
|
2
|
-
"2026-05-10.latest-sdk-contract.
|
|
2
|
+
"2026-05-10.latest-sdk-contract.v17";
|
|
3
3
|
|
|
4
4
|
export const SETUP_DOCTRINE = {
|
|
5
5
|
purpose:
|
|
@@ -13,7 +13,7 @@ export const SETUP_DOCTRINE = {
|
|
|
13
13
|
documentation_reason:
|
|
14
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
15
|
failure_posture:
|
|
16
|
-
"
|
|
16
|
+
"For supported setup paths, prefer completing safe minimal Clue wiring and reporting unclear lifecycle points as warnings. Reserve blockers for truly unsupported frameworks, unavailable SDK contracts, missing AI configuration, or edits that cannot be applied without guessing outside Clue setup scope.",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export const DETERMINISTIC_CONTROL_MODEL = {
|
|
@@ -22,7 +22,7 @@ export const DETERMINISTIC_CONTROL_MODEL = {
|
|
|
22
22
|
"which existing login/session success point owns ClueIdentify",
|
|
23
23
|
"which existing account/workspace/organization resolution point owns ClueSetAccount",
|
|
24
24
|
"which existing logout/session reset point owns ClueLogout",
|
|
25
|
-
"whether a lifecycle point is unclear and should be
|
|
25
|
+
"whether a lifecycle point is unclear and should be skipped with a warning instead of guessed",
|
|
26
26
|
],
|
|
27
27
|
cli_should_control: [
|
|
28
28
|
"official SDK package names",
|
|
@@ -37,6 +37,39 @@ export const DETERMINISTIC_CONTROL_MODEL = {
|
|
|
37
37
|
],
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
export const SETUP_AGENT_QUALITY_GATES = [
|
|
41
|
+
{
|
|
42
|
+
id: "ai_provider_config_present",
|
|
43
|
+
purpose:
|
|
44
|
+
"Prevent silent setup-agent startup failure when AI provider env is missing.",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "sdk_dependencies_cli_owned",
|
|
48
|
+
purpose:
|
|
49
|
+
"Ensure official SDK package names and latest-channel declarations are controlled by CLI logic, not model guesses.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "lifecycle_plan_cli_applied",
|
|
53
|
+
purpose:
|
|
54
|
+
"Ensure the AI only returns a structured plan and cannot directly write files or run shell commands.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "static_setup_check_passed",
|
|
58
|
+
purpose:
|
|
59
|
+
"Reject known bad local-prompt failure modes such as wrong SDKs, stale env names, unsafe browser token wiring, awaited lifecycle calls, and partial lifecycle setup.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "setup_doctor_preflight",
|
|
63
|
+
purpose:
|
|
64
|
+
"Separate API connectivity verification from static source verification and make skipped preflight visible.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "setup_watch_user_owned",
|
|
68
|
+
purpose:
|
|
69
|
+
"Keep real login/logout/account event delivery verification under the user's control.",
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
40
73
|
export const API_CONNECTIVITY_CONTRACT = {
|
|
41
74
|
purpose:
|
|
42
75
|
"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.",
|
|
@@ -55,7 +88,7 @@ export const API_CONNECTIVITY_CONTRACT = {
|
|
|
55
88
|
path: "/api/v1/ingest/browser-tokens",
|
|
56
89
|
caller: "customer_backend_browser_token_proxy",
|
|
57
90
|
purpose:
|
|
58
|
-
"Clue backend validates
|
|
91
|
+
"Clue backend validates x-clue-api-key, project, environment, service key, and origin, then returns a browser token.",
|
|
59
92
|
},
|
|
60
93
|
browser_ingest: {
|
|
61
94
|
owner: "clue_backend",
|
|
@@ -93,15 +126,66 @@ export const FRONTEND_ADAPTER_CONTRACT = {
|
|
|
93
126
|
],
|
|
94
127
|
browser_token_proxy_path: "/api/v1/clue/browser-tokens",
|
|
95
128
|
rules: [
|
|
129
|
+
"For Next.js, prefer a module-level client singleton such as src/lib/clue.ts that calls ClueInit once after required NEXT_PUBLIC_CLUE_* values are present, then import that module from app/layout.tsx or the existing app bootstrap.",
|
|
130
|
+
"Next.js browser SDK adapter files must start with \"use client\".",
|
|
131
|
+
"Do not create a React component whose useEffect calls ClueInit. Component lifecycle hooks, page components, sidebars, login/register success callbacks, and other repeated UI paths are rejected setup locations.",
|
|
96
132
|
"Do not derive the Clue browser-token proxy from generic app API env names such as NEXT_PUBLIC_API_URL.",
|
|
97
133
|
"Do not mix stale browser token paths with the canonical /api/v1/clue/browser-tokens path in the same adapter.",
|
|
98
134
|
"Next.js browserTokenProvider must call NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT, whose value is the customer backend browser-token proxy URL.",
|
|
99
135
|
"Do not call ClueInit with empty-string fallbacks for required NEXT_PUBLIC_CLUE_* values.",
|
|
100
136
|
"If a singleton guard is used, do not mark initialized=true before ClueInit has been called with required config present.",
|
|
101
137
|
"The browser token provider must send the same frontend serviceKey used by ClueInit.",
|
|
138
|
+
"Backend browser-token proxy calls to Clue must send CLUE_API_KEY as the x-clue-api-key header, never as Authorization bearer, query, or JSON body.",
|
|
139
|
+
"Do not introduce non-Clue HTTP client dependencies for browser-token proxy code unless they already exist in dependency files.",
|
|
102
140
|
],
|
|
103
141
|
};
|
|
104
142
|
|
|
143
|
+
export const OFFICIAL_SDK_CONTRACT = {
|
|
144
|
+
purpose:
|
|
145
|
+
"This bundled contract is the authoritative Clue SDK contract for setup-agent. A customer repository is expected to start without Clue SDK imports or dependencies; absence of existing Clue code is not a blocker.",
|
|
146
|
+
frontend_browser_sdk: {
|
|
147
|
+
package_name: "@clue-ai/browser-sdk",
|
|
148
|
+
dependency_specifier: "@clue-ai/browser-sdk@latest",
|
|
149
|
+
import_path: "@clue-ai/browser-sdk",
|
|
150
|
+
public_lifecycle_apis: {
|
|
151
|
+
ClueInit:
|
|
152
|
+
"ClueInit(options: { endpoint: string; projectKey: string; environment: string; serviceKey?: string; producerId?: string; browserTokenProvider?: () => string | Promise<string>; ... }): void",
|
|
153
|
+
ClueIdentify:
|
|
154
|
+
"ClueIdentify(userId: string, traits?: Record<string, string | number | boolean | null>): void",
|
|
155
|
+
ClueSetAccount:
|
|
156
|
+
"ClueSetAccount(accountId: string, traits?: Record<string, string | number | boolean | null>): void",
|
|
157
|
+
ClueLogout: "ClueLogout(): void",
|
|
158
|
+
},
|
|
159
|
+
safety_contract:
|
|
160
|
+
"Public lifecycle APIs are no-throw wrappers. Do not add custom per-call try/catch or await them.",
|
|
161
|
+
},
|
|
162
|
+
backend_fastapi_sdk: {
|
|
163
|
+
package_name: "clue-fastapi-sdk",
|
|
164
|
+
dependency_specifier: "clue-fastapi-sdk",
|
|
165
|
+
python_import:
|
|
166
|
+
"from clue_fastapi_sdk import clue_init_fastapi, ClueIdentify, ClueSetAccount, ClueLogout",
|
|
167
|
+
public_lifecycle_apis: {
|
|
168
|
+
clue_init_fastapi:
|
|
169
|
+
"clue_init_fastapi(app, *, project_key: str, environment: str, api_key: str | None = None, service_name: str = 'python-service', producer_id: str | None = None, service_key: str | None = None, ... ) -> bool",
|
|
170
|
+
ClueIdentify:
|
|
171
|
+
"ClueIdentify(user_id: str, traits: Mapping[str, object] | None = None) -> bool",
|
|
172
|
+
ClueSetAccount:
|
|
173
|
+
"ClueSetAccount(account_id: str, traits: Mapping[str, object] | None = None) -> bool",
|
|
174
|
+
ClueLogout: "ClueLogout(reason: str | None = None) -> bool",
|
|
175
|
+
},
|
|
176
|
+
safety_contract:
|
|
177
|
+
"Public lifecycle APIs catch SDK errors internally and return bool where applicable. Do not wrap each call solely for Clue failure isolation. FastAPI initialization must pass service_key so event attribution matches setup-watch service targets. Clue env reads must be non-crashing; do not use os.environ[\"CLUE_*\"] required indexing.",
|
|
178
|
+
},
|
|
179
|
+
minimal_file_creation_contract: {
|
|
180
|
+
allowed_when:
|
|
181
|
+
"No existing Clue adapter, browser-token proxy module, or client bootstrap wrapper exists in the customer repo.",
|
|
182
|
+
allowed_files:
|
|
183
|
+
"Only Clue-owned minimal SDK wiring files under existing frontend/backend source roots, plus exact replacements in existing files to import/register those files.",
|
|
184
|
+
rule:
|
|
185
|
+
"Creating a minimal Clue adapter or browser-token proxy is allowed setup wiring, not a host application refactor.",
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
105
189
|
export const setupDoctrineSkillLines = () => [
|
|
106
190
|
`- Purpose: ${SETUP_DOCTRINE.purpose}`,
|
|
107
191
|
`- Minimal diff reason: ${SETUP_DOCTRINE.minimal_diff_reason}`,
|
|
@@ -109,6 +193,7 @@ export const setupDoctrineSkillLines = () => [
|
|
|
109
193
|
`- Static control boundary: ${SETUP_DOCTRINE.deterministic_control_boundary}`,
|
|
110
194
|
`- Documentation reason: ${SETUP_DOCTRINE.documentation_reason}`,
|
|
111
195
|
`- Failure posture: ${SETUP_DOCTRINE.failure_posture}`,
|
|
196
|
+
`- Official SDK contract: ${OFFICIAL_SDK_CONTRACT.purpose}`,
|
|
112
197
|
`- API connectivity: ${API_CONNECTIVITY_CONTRACT.purpose}`,
|
|
113
198
|
`- Frontend adapter: ${FRONTEND_ADAPTER_CONTRACT.purpose}`,
|
|
114
199
|
`- 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.`,
|