@clue-ai/cli 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/clue-cli.mjs +15 -0
- package/package.json +1 -1
- package/src/ai-provider.mjs +92 -2
- package/src/fastapi-analyzer.mjs +36 -2
- package/src/lifecycle-init.mjs +234 -16
- package/src/public-schema.cjs +1 -0
- package/src/semantic-ci.mjs +235 -20
- package/src/setup-agent.mjs +448 -0
- package/src/setup-ai-contract.mjs +25 -1
- package/src/setup-check.mjs +199 -11
- package/src/setup-doctor.mjs +11 -4
- package/src/setup-help.mjs +32 -2
- package/src/setup-prepare.mjs +19 -0
- package/src/setup-tool.mjs +20 -8
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
applyLifecyclePlan,
|
|
5
|
+
planLifecycleInsertionsStrict,
|
|
6
|
+
} from "./lifecycle-init.mjs";
|
|
7
|
+
import { runSetupCheck } from "./setup-check.mjs";
|
|
8
|
+
import { runSetupDoctor } from "./setup-doctor.mjs";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
11
|
+
const DEFAULT_ENV_GUIDE_PATH = ".env.clue";
|
|
12
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
13
|
+
const DEPENDENCY_WRITE_FILE_CANDIDATES = [
|
|
14
|
+
"package.json",
|
|
15
|
+
"package-lock.json",
|
|
16
|
+
"pnpm-lock.yaml",
|
|
17
|
+
"yarn.lock",
|
|
18
|
+
"bun.lock",
|
|
19
|
+
"requirements.txt",
|
|
20
|
+
"requirements-dev.txt",
|
|
21
|
+
"pyproject.toml",
|
|
22
|
+
"Pipfile",
|
|
23
|
+
"Pipfile.lock",
|
|
24
|
+
"poetry.lock",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const optionalString = (value) =>
|
|
28
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
29
|
+
|
|
30
|
+
const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
31
|
+
|
|
32
|
+
const parseEnvGuide = (text) => {
|
|
33
|
+
const values = {};
|
|
34
|
+
for (const line of text.split(/\r?\n/)) {
|
|
35
|
+
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)\s*$/.exec(line);
|
|
36
|
+
if (!match) continue;
|
|
37
|
+
const value = match[2].trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
38
|
+
if (/^<[^>]+>$/.test(value)) continue;
|
|
39
|
+
values[match[1]] = value;
|
|
40
|
+
}
|
|
41
|
+
return values;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const readEnvGuideIfPresent = async ({ repoRoot }) => {
|
|
45
|
+
try {
|
|
46
|
+
return parseEnvGuide(
|
|
47
|
+
await readFile(resolve(repoRoot, DEFAULT_ENV_GUIDE_PATH), "utf8"),
|
|
48
|
+
);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error?.code === "ENOENT") return {};
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const exists = async (path) => {
|
|
56
|
+
try {
|
|
57
|
+
await access(path);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const normalizePositiveInteger = (value, fallback) => {
|
|
65
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
66
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const aiProviderEnvFromFlags = ({ env, flags }) => {
|
|
70
|
+
const aiProvider = optionalString(flags.get("ai-provider"));
|
|
71
|
+
const aiProviderApiKey = optionalString(flags.get("ai-provider-api-key"));
|
|
72
|
+
const aiModel = optionalString(flags.get("ai-model"));
|
|
73
|
+
return {
|
|
74
|
+
...env,
|
|
75
|
+
...(aiProvider ? { CLUE_AI_PROVIDER: aiProvider } : {}),
|
|
76
|
+
...(aiProviderApiKey
|
|
77
|
+
? { CLUE_AI_PROVIDER_API_KEY: aiProviderApiKey }
|
|
78
|
+
: {}),
|
|
79
|
+
...(aiModel ? { CLUE_AI_MODEL: aiModel } : {}),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const requireManifest = async ({ manifestPath, repoRoot }) => {
|
|
84
|
+
try {
|
|
85
|
+
return await readJson(resolve(repoRoot, manifestPath));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error?.code === "ENOENT") {
|
|
88
|
+
const blocker = new Error(
|
|
89
|
+
`setup-agent requires ${manifestPath}; run setup first`,
|
|
90
|
+
);
|
|
91
|
+
blocker.code = "CLUE_SETUP_MANIFEST_MISSING";
|
|
92
|
+
throw blocker;
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const sourcePathsFromManifest = (manifest) => {
|
|
99
|
+
const watchTargets = Array.isArray(
|
|
100
|
+
manifest.lifecycle_verification?.watch_targets,
|
|
101
|
+
)
|
|
102
|
+
? manifest.lifecycle_verification.watch_targets
|
|
103
|
+
: [];
|
|
104
|
+
return [
|
|
105
|
+
...new Set(
|
|
106
|
+
[
|
|
107
|
+
manifest.detected?.backend_root_path,
|
|
108
|
+
...watchTargets.map((target) => target?.root_path),
|
|
109
|
+
]
|
|
110
|
+
.map(optionalString)
|
|
111
|
+
.filter(Boolean),
|
|
112
|
+
),
|
|
113
|
+
];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const dependencyCandidateRoots = (sourcePaths) => [
|
|
117
|
+
".",
|
|
118
|
+
...sourcePaths.flatMap((root) => {
|
|
119
|
+
const normalized = root.replace(/\/+$/, "");
|
|
120
|
+
return normalized.endsWith("/src") || normalized.endsWith("/app")
|
|
121
|
+
? [normalized, dirname(normalized)]
|
|
122
|
+
: [normalized];
|
|
123
|
+
}),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const discoveredDependencyWritePaths = async ({ repoRoot, sourcePaths }) => {
|
|
127
|
+
const candidates = dependencyCandidateRoots(sourcePaths).flatMap((root) =>
|
|
128
|
+
DEPENDENCY_WRITE_FILE_CANDIDATES.map((file) =>
|
|
129
|
+
root === "." ? file : join(root, file),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
const discovered = [];
|
|
133
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
134
|
+
if (await exists(resolve(repoRoot, candidate))) {
|
|
135
|
+
discovered.push(candidate);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return discovered;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const allowedWritePathsFromRequest = async ({ repoRoot, request }) => [
|
|
142
|
+
...new Set([
|
|
143
|
+
...request.allowed_source_paths,
|
|
144
|
+
...(await discoveredDependencyWritePaths({
|
|
145
|
+
repoRoot,
|
|
146
|
+
sourcePaths: request.allowed_source_paths,
|
|
147
|
+
})),
|
|
148
|
+
]),
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const buildLifecycleRequestFromManifest = ({ manifest, target }) => {
|
|
152
|
+
if (manifest?.status !== "ready_for_ai") {
|
|
153
|
+
throw new Error("setup-agent requires a ready_for_ai setup manifest");
|
|
154
|
+
}
|
|
155
|
+
const framework = optionalString(manifest.detected?.framework);
|
|
156
|
+
const backendRootPath = optionalString(manifest.detected?.backend_root_path);
|
|
157
|
+
const serviceKey = optionalString(manifest.detected?.service_key);
|
|
158
|
+
const sourcePaths = sourcePathsFromManifest(manifest);
|
|
159
|
+
if (!framework || !backendRootPath || !serviceKey || sourcePaths.length === 0) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
"setup-agent manifest is missing framework, backend root, service key, or source paths",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
target_tool: optionalString(target) ?? optionalString(manifest.target) ?? "codex",
|
|
166
|
+
framework,
|
|
167
|
+
project_key: "CLUE_PROJECT_KEY",
|
|
168
|
+
environment: "CLUE_ENVIRONMENT",
|
|
169
|
+
service_key: serviceKey,
|
|
170
|
+
allowed_source_paths: sourcePaths,
|
|
171
|
+
excluded_source_paths: [],
|
|
172
|
+
backend_root_path: backendRootPath,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const failedChecks = (setupCheck) =>
|
|
177
|
+
Array.isArray(setupCheck?.checks)
|
|
178
|
+
? setupCheck.checks
|
|
179
|
+
.filter((check) => !check.passed)
|
|
180
|
+
.map((check) => ({
|
|
181
|
+
id: check.id,
|
|
182
|
+
summary: check.summary ?? null,
|
|
183
|
+
details: {
|
|
184
|
+
missing_apis: check.missing_apis ?? null,
|
|
185
|
+
backend_lifecycle: check.backend_lifecycle ?? null,
|
|
186
|
+
frontend_lifecycle: check.frontend_lifecycle ?? null,
|
|
187
|
+
findings: check.findings ?? null,
|
|
188
|
+
},
|
|
189
|
+
}))
|
|
190
|
+
: [];
|
|
191
|
+
|
|
192
|
+
const runStaticSetupCheck = async ({
|
|
193
|
+
manifest,
|
|
194
|
+
repoRoot,
|
|
195
|
+
request,
|
|
196
|
+
setupChecker,
|
|
197
|
+
target,
|
|
198
|
+
}) =>
|
|
199
|
+
setupChecker({
|
|
200
|
+
repoRoot,
|
|
201
|
+
request: {
|
|
202
|
+
framework: request.framework,
|
|
203
|
+
backend_root_path: request.backend_root_path,
|
|
204
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
205
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
206
|
+
service_key: request.service_key,
|
|
207
|
+
workflow_path: manifest.artifacts?.ci_workflow_path,
|
|
208
|
+
},
|
|
209
|
+
target,
|
|
210
|
+
requireSdkLifecycle: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const missingInputsFromDoctor = (report) => [
|
|
214
|
+
...new Set(
|
|
215
|
+
(report?.checks ?? [])
|
|
216
|
+
.flatMap((check) => {
|
|
217
|
+
const match = /^missing required input: (?<names>.+)$/.exec(
|
|
218
|
+
check.error ?? "",
|
|
219
|
+
);
|
|
220
|
+
return match?.groups?.names
|
|
221
|
+
? match.groups.names.split(",").map((entry) => entry.trim())
|
|
222
|
+
: [];
|
|
223
|
+
})
|
|
224
|
+
.filter(Boolean),
|
|
225
|
+
),
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const runOptionalSetupDoctor = async ({ env, flags, repoRoot, setupDoctor }) => {
|
|
229
|
+
if (flags.has("skip-setup-doctor")) {
|
|
230
|
+
return {
|
|
231
|
+
status: "skipped",
|
|
232
|
+
reason: "skip-setup-doctor flag was provided",
|
|
233
|
+
missing_inputs: [],
|
|
234
|
+
report: null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const report = await setupDoctor({ env, flags, repoRoot });
|
|
238
|
+
const missingInputs = missingInputsFromDoctor(report);
|
|
239
|
+
if (!report.passed) {
|
|
240
|
+
const failedChecks = Array.isArray(report?.checks)
|
|
241
|
+
? report.checks.filter((check) => !check.passed)
|
|
242
|
+
: [];
|
|
243
|
+
const allFailuresAreMissingInputs =
|
|
244
|
+
failedChecks.length > 0 &&
|
|
245
|
+
failedChecks.every((check) =>
|
|
246
|
+
/^missing required input: .+$/.test(check.error ?? ""),
|
|
247
|
+
);
|
|
248
|
+
if (allFailuresAreMissingInputs) {
|
|
249
|
+
return {
|
|
250
|
+
status: "blocked_missing_inputs",
|
|
251
|
+
reason: "setup-doctor required inputs are missing",
|
|
252
|
+
missing_inputs: missingInputs,
|
|
253
|
+
report,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
status: "failed",
|
|
258
|
+
reason: "setup-doctor connectivity preflight failed",
|
|
259
|
+
missing_inputs: [],
|
|
260
|
+
report,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
status: "passed",
|
|
265
|
+
reason: null,
|
|
266
|
+
missing_inputs: [],
|
|
267
|
+
report,
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const attemptFailureContext = ({ attempt, error, setupCheck, stage }) => ({
|
|
272
|
+
attempt,
|
|
273
|
+
stage,
|
|
274
|
+
error:
|
|
275
|
+
error === undefined
|
|
276
|
+
? null
|
|
277
|
+
: error instanceof Error
|
|
278
|
+
? error.message
|
|
279
|
+
: String(error),
|
|
280
|
+
failed_checks: failedChecks(setupCheck),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export const runSetupAgent = async ({
|
|
284
|
+
env = process.env,
|
|
285
|
+
flags = new Map(),
|
|
286
|
+
lifecycleApplier = applyLifecyclePlan,
|
|
287
|
+
lifecyclePlanner = planLifecycleInsertionsStrict,
|
|
288
|
+
repoRoot = ".",
|
|
289
|
+
setupChecker = runSetupCheck,
|
|
290
|
+
setupDoctor = runSetupDoctor,
|
|
291
|
+
}) => {
|
|
292
|
+
const resolvedRepoRoot = resolve(repoRoot ?? ".");
|
|
293
|
+
const manifestPath = String(
|
|
294
|
+
flags.get("manifest") || DEFAULT_SETUP_MANIFEST_PATH,
|
|
295
|
+
);
|
|
296
|
+
let manifest;
|
|
297
|
+
try {
|
|
298
|
+
manifest = await requireManifest({
|
|
299
|
+
manifestPath,
|
|
300
|
+
repoRoot: resolvedRepoRoot,
|
|
301
|
+
});
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error?.code === "CLUE_SETUP_MANIFEST_MISSING") {
|
|
304
|
+
return {
|
|
305
|
+
status: "blocked",
|
|
306
|
+
code: "CLUE_SETUP_MANIFEST_MISSING",
|
|
307
|
+
manifest_path: manifestPath,
|
|
308
|
+
attempts: [],
|
|
309
|
+
setup_check: null,
|
|
310
|
+
setup_doctor: {
|
|
311
|
+
status: "skipped",
|
|
312
|
+
reason: "setup manifest is missing",
|
|
313
|
+
missing_inputs: [],
|
|
314
|
+
report: null,
|
|
315
|
+
},
|
|
316
|
+
blockers: [
|
|
317
|
+
{
|
|
318
|
+
reason: error.message,
|
|
319
|
+
evidence: manifestPath,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
user_verification: {
|
|
323
|
+
setup_watch_owner: "user",
|
|
324
|
+
setup_watch_auto_run: false,
|
|
325
|
+
status: "not_reached",
|
|
326
|
+
required_command: "npx -y @clue-ai/cli setup-watch --local",
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
const target = optionalString(flags.get("target")) ?? manifest.target;
|
|
333
|
+
const mergedEnv = {
|
|
334
|
+
...(await readEnvGuideIfPresent({ repoRoot: resolvedRepoRoot })),
|
|
335
|
+
...env,
|
|
336
|
+
};
|
|
337
|
+
const request = buildLifecycleRequestFromManifest({ manifest, target });
|
|
338
|
+
const allowedWritePaths = await allowedWritePathsFromRequest({
|
|
339
|
+
repoRoot: resolvedRepoRoot,
|
|
340
|
+
request,
|
|
341
|
+
});
|
|
342
|
+
const maxAttempts = normalizePositiveInteger(
|
|
343
|
+
flags.get("max-attempts"),
|
|
344
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
345
|
+
);
|
|
346
|
+
const aiEnv = aiProviderEnvFromFlags({ env: mergedEnv, flags });
|
|
347
|
+
const attempts = [];
|
|
348
|
+
let failureContext = null;
|
|
349
|
+
let lastSetupCheck = null;
|
|
350
|
+
|
|
351
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
352
|
+
try {
|
|
353
|
+
const plan = await lifecyclePlanner({
|
|
354
|
+
repoRoot: resolvedRepoRoot,
|
|
355
|
+
request,
|
|
356
|
+
env: aiEnv,
|
|
357
|
+
failureContext,
|
|
358
|
+
});
|
|
359
|
+
const lifecycleResult = await lifecycleApplier({
|
|
360
|
+
repoRoot: resolvedRepoRoot,
|
|
361
|
+
plan,
|
|
362
|
+
allowedWritePaths,
|
|
363
|
+
});
|
|
364
|
+
const setupCheck = await runStaticSetupCheck({
|
|
365
|
+
manifest,
|
|
366
|
+
repoRoot: resolvedRepoRoot,
|
|
367
|
+
request,
|
|
368
|
+
setupChecker,
|
|
369
|
+
target,
|
|
370
|
+
});
|
|
371
|
+
lastSetupCheck = setupCheck;
|
|
372
|
+
attempts.push({
|
|
373
|
+
attempt,
|
|
374
|
+
lifecycle_insertions: lifecycleResult.lifecycleInsertions,
|
|
375
|
+
warnings: lifecycleResult.warnings,
|
|
376
|
+
setup_check_passed: setupCheck.passed,
|
|
377
|
+
failed_checks: failedChecks(setupCheck),
|
|
378
|
+
});
|
|
379
|
+
if (setupCheck.passed) {
|
|
380
|
+
const setupDoctorResult = await runOptionalSetupDoctor({
|
|
381
|
+
env: mergedEnv,
|
|
382
|
+
flags,
|
|
383
|
+
repoRoot: resolvedRepoRoot,
|
|
384
|
+
setupDoctor,
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
status:
|
|
388
|
+
setupDoctorResult.status === "passed" ||
|
|
389
|
+
setupDoctorResult.status === "skipped"
|
|
390
|
+
? "user_verification_pending"
|
|
391
|
+
: "blocked",
|
|
392
|
+
manifest_path: manifestPath,
|
|
393
|
+
attempts,
|
|
394
|
+
setup_check: setupCheck,
|
|
395
|
+
setup_doctor: setupDoctorResult,
|
|
396
|
+
user_verification: {
|
|
397
|
+
setup_watch_owner: "user",
|
|
398
|
+
setup_watch_auto_run: false,
|
|
399
|
+
status: "user_verification_pending",
|
|
400
|
+
required_command: "npx -y @clue-ai/cli setup-watch --local",
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
failureContext = attemptFailureContext({
|
|
405
|
+
attempt,
|
|
406
|
+
setupCheck,
|
|
407
|
+
stage: "setup_check",
|
|
408
|
+
});
|
|
409
|
+
} catch (error) {
|
|
410
|
+
failureContext = attemptFailureContext({
|
|
411
|
+
attempt,
|
|
412
|
+
error,
|
|
413
|
+
stage: "lifecycle_apply_or_plan",
|
|
414
|
+
});
|
|
415
|
+
attempts.push({
|
|
416
|
+
attempt,
|
|
417
|
+
setup_check_passed: false,
|
|
418
|
+
error: failureContext.error,
|
|
419
|
+
failed_checks: [],
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
status: "blocked",
|
|
426
|
+
manifest_path: manifestPath,
|
|
427
|
+
attempts,
|
|
428
|
+
setup_check: lastSetupCheck,
|
|
429
|
+
setup_doctor: {
|
|
430
|
+
status: "skipped",
|
|
431
|
+
reason: "setup-check did not pass",
|
|
432
|
+
missing_inputs: [],
|
|
433
|
+
report: null,
|
|
434
|
+
},
|
|
435
|
+
blockers: [
|
|
436
|
+
{
|
|
437
|
+
reason: "setup-agent retry budget exhausted",
|
|
438
|
+
evidence: failureContext,
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
user_verification: {
|
|
442
|
+
setup_watch_owner: "user",
|
|
443
|
+
setup_watch_auto_run: false,
|
|
444
|
+
status: "not_reached",
|
|
445
|
+
required_command: "npx -y @clue-ai/cli setup-watch --local",
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const AI_SETUP_CONTRACT_VERSION =
|
|
2
|
-
"2026-05-10.
|
|
2
|
+
"2026-05-10.latest-sdk-contract.v9";
|
|
3
3
|
|
|
4
4
|
export const SETUP_DOCTRINE = {
|
|
5
5
|
purpose:
|
|
@@ -26,12 +26,14 @@ export const DETERMINISTIC_CONTROL_MODEL = {
|
|
|
26
26
|
],
|
|
27
27
|
cli_should_control: [
|
|
28
28
|
"official SDK package names",
|
|
29
|
+
"latest-channel Clue SDK dependency declarations so setup agents cannot pin stale SDK versions",
|
|
29
30
|
"official public SDK function names and supported lifecycle API set",
|
|
30
31
|
"environment variable names produced by setup and consumed by setup code",
|
|
31
32
|
"machine-owned semantic workflow generation and verification",
|
|
32
33
|
"setup-watch ownership as user-operated verification",
|
|
33
34
|
"static rejection of known unsafe wiring such as leaked secrets, wrong SDKs, blocking lifecycle calls, broad ClueTrack setup, and unsafe browser token proxy patterns",
|
|
34
35
|
"local API connectivity preflight for the four required setup hops before user-operated setup-watch",
|
|
36
|
+
"canonical frontend SDK adapter env names, token proxy path, and initialization safety checks",
|
|
35
37
|
],
|
|
36
38
|
};
|
|
37
39
|
|
|
@@ -79,6 +81,27 @@ export const API_CONNECTIVITY_CONTRACT = {
|
|
|
79
81
|
"setup-doctor checks local API connectivity before user flows. setup-watch remains user-operated lifecycle and event-delivery verification.",
|
|
80
82
|
};
|
|
81
83
|
|
|
84
|
+
export const FRONTEND_ADAPTER_CONTRACT = {
|
|
85
|
+
purpose:
|
|
86
|
+
"Frontend SDK adapter code is part of the Clue setup contract. The AI may choose where the adapter is imported, but must not invent new token URL, env, or initialization semantics.",
|
|
87
|
+
nextjs_public_env: [
|
|
88
|
+
"NEXT_PUBLIC_CLUE_PROJECT_KEY",
|
|
89
|
+
"NEXT_PUBLIC_CLUE_ENVIRONMENT",
|
|
90
|
+
"NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
91
|
+
"NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
92
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
93
|
+
],
|
|
94
|
+
browser_token_proxy_path: "/api/v1/clue/browser-tokens",
|
|
95
|
+
rules: [
|
|
96
|
+
"Do not derive the Clue browser-token proxy from generic app API env names such as NEXT_PUBLIC_API_URL.",
|
|
97
|
+
"Do not mix stale browser token paths with the canonical /api/v1/clue/browser-tokens path in the same adapter.",
|
|
98
|
+
"Next.js browserTokenProvider must call NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT, whose value is the customer backend browser-token proxy URL.",
|
|
99
|
+
"Do not call ClueInit with empty-string fallbacks for required NEXT_PUBLIC_CLUE_* values.",
|
|
100
|
+
"If a singleton guard is used, do not mark initialized=true before ClueInit has been called with required config present.",
|
|
101
|
+
"The browser token provider must send the same frontend serviceKey used by ClueInit.",
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
82
105
|
export const setupDoctrineSkillLines = () => [
|
|
83
106
|
`- Purpose: ${SETUP_DOCTRINE.purpose}`,
|
|
84
107
|
`- Minimal diff reason: ${SETUP_DOCTRINE.minimal_diff_reason}`,
|
|
@@ -87,5 +110,6 @@ export const setupDoctrineSkillLines = () => [
|
|
|
87
110
|
`- Documentation reason: ${SETUP_DOCTRINE.documentation_reason}`,
|
|
88
111
|
`- Failure posture: ${SETUP_DOCTRINE.failure_posture}`,
|
|
89
112
|
`- API connectivity: ${API_CONNECTIVITY_CONTRACT.purpose}`,
|
|
113
|
+
`- Frontend adapter: ${FRONTEND_ADAPTER_CONTRACT.purpose}`,
|
|
90
114
|
`- 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.`,
|
|
91
115
|
];
|