@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 CHANGED
@@ -928,9 +928,24 @@ const renderSetupResult = ({ preparation }) => {
928
928
 
929
929
  const renderSetupAgentResult = (report) => {
930
930
  if (report?.status === "user_verification_pending") {
931
+ const setupDoctorPassed = report.setup_doctor?.status === "passed";
932
+ const setupDoctorMissingInputs = Array.isArray(
933
+ report.setup_doctor?.missing_inputs,
934
+ )
935
+ ? report.setup_doctor.missing_inputs
936
+ : [];
937
+ const setupDoctorLine =
938
+ setupDoctorPassed
939
+ ? "setup-doctor のAPI疎通確認は通過しました。"
940
+ : setupDoctorMissingInputs.length > 0
941
+ ? `setup-doctor のAPI疎通確認は未実行です。不足: ${setupDoctorMissingInputs.join(", ")}`
942
+ : "setup-doctor のAPI疎通確認は未実行です。local services 起動後に確認してください。";
931
943
  return [
932
- "Clue setup-agent の実行が完了しました。",
944
+ setupDoctorPassed
945
+ ? "Clue setup-agent の実行が完了しました。"
946
+ : "Clue setup-agent の静的実装が完了しました。",
933
947
  "",
948
+ setupDoctorLine,
934
949
  "コード差分を確認してください。",
935
950
  `次の動作確認: ${clueCliCommand("setup-watch --local")}`,
936
951
  ].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clue-ai/cli",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clue-ai": "bin/clue-cli.mjs"
@@ -79,6 +79,58 @@ const parseOpenAiStrictToolJson = async ({ response, toolName, emptyMessage }) =
79
79
  throw new Error(emptyMessage);
80
80
  };
81
81
 
82
+ const providerErrorDetailFromBody = (body) => {
83
+ if (!body) return "";
84
+ try {
85
+ const parsed = JSON.parse(body);
86
+ const message =
87
+ parsed?.error?.message ??
88
+ parsed?.message ??
89
+ parsed?.error ??
90
+ parsed?.detail ??
91
+ null;
92
+ if (typeof message === "string" && message.trim()) {
93
+ return message.trim();
94
+ }
95
+ } catch {
96
+ // The provider may return plain text, HTML, or an empty body.
97
+ }
98
+ return body.trim();
99
+ };
100
+
101
+ const redactedProviderErrorDetail = ({ detail, apiKey }) => {
102
+ const redacted = String(detail)
103
+ .replaceAll(apiKey, "[redacted]")
104
+ .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
105
+ .replace(/\bsk-ant-[A-Za-z0-9_-]{8,}\b/g, "[redacted]");
106
+ return redacted.length > 600 ? `${redacted.slice(0, 600)}...` : redacted;
107
+ };
108
+
109
+ const providerHttpError = async ({ response, failureMessage, apiKey }) => {
110
+ let detail = "";
111
+ try {
112
+ const body =
113
+ typeof response.text === "function"
114
+ ? await response.text()
115
+ : typeof response.json === "function"
116
+ ? JSON.stringify(await response.json())
117
+ : "";
118
+ detail = redactedProviderErrorDetail({
119
+ detail: providerErrorDetailFromBody(body),
120
+ apiKey,
121
+ });
122
+ } catch {
123
+ detail = "";
124
+ }
125
+ const statusText =
126
+ typeof response.statusText === "string" && response.statusText.trim()
127
+ ? ` ${response.statusText.trim()}`
128
+ : "";
129
+ return new Error(
130
+ `${failureMessage}: ${response.status}${statusText}${detail ? ` - ${detail}` : ""}`,
131
+ );
132
+ };
133
+
82
134
  const parseAnthropicJson = async ({ response, toolName, emptyMessage }) => {
83
135
  const body = await response.json();
84
136
  const toolUse = Array.isArray(body?.content)
@@ -162,12 +214,15 @@ export const callStrictToolAiProvider = async ({
162
214
  ],
163
215
  tool_choice: { type: "function", name: toolName },
164
216
  parallel_tool_calls: false,
165
- temperature: 0,
166
217
  }),
167
218
  });
168
219
 
169
220
  if (!response.ok) {
170
- throw new Error(`${failureMessage}: ${response.status}`);
221
+ throw await providerHttpError({
222
+ response,
223
+ failureMessage,
224
+ apiKey: config.apiKey,
225
+ });
171
226
  }
172
227
  return config.provider === "anthropic"
173
228
  ? parseAnthropicJson({ response, toolName, emptyMessage })
@@ -219,7 +274,6 @@ export const callJsonAiProvider = async ({
219
274
  },
220
275
  body: JSON.stringify({
221
276
  model: config.model,
222
- temperature: 0,
223
277
  messages: [
224
278
  { role: "system", content: system },
225
279
  { role: "user", content: user },
@@ -229,7 +283,11 @@ export const callJsonAiProvider = async ({
229
283
  });
230
284
 
231
285
  if (!response.ok) {
232
- throw new Error(`${failureMessage}: ${response.status}`);
286
+ throw await providerHttpError({
287
+ response,
288
+ failureMessage,
289
+ apiKey: config.apiKey,
290
+ });
233
291
  }
234
292
  return config.provider === "anthropic"
235
293
  ? parseAnthropicJson({ response, toolName, emptyMessage })
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import {
4
4
  callJsonAiProvider,
@@ -15,6 +15,7 @@ import { listAllowedSourceFiles } from "./path-policy.mjs";
15
15
  import {
16
16
  API_CONNECTIVITY_CONTRACT,
17
17
  DETERMINISTIC_CONTROL_MODEL,
18
+ OFFICIAL_SDK_CONTRACT,
18
19
  SETUP_DOCTRINE,
19
20
  } from "./setup-ai-contract.mjs";
20
21
 
@@ -39,7 +40,11 @@ const MAX_TOTAL_CHARS = 360_000;
39
40
  const FRONTEND_SDK_PACKAGE = "@clue-ai/browser-sdk";
40
41
  const WRONG_FRONTEND_SDK_PACKAGES = ["clue-js-sdk", "@clue/browser-sdk"];
41
42
  const CLUE_SETUP_ADDITION_PATTERN =
42
- /\b(?:ClueInit|ClueIdentify|ClueSetAccount|ClueLogout|clue_init_fastapi|clue_init_django|clue-fastapi-sdk|clue-django-sdk|CLUE_[A-Z0-9_]+|browserTokenProvider|clue[-_])|@clue-ai\/browser-sdk/i;
43
+ /\b(?:Clue[A-Za-z0-9_]*|clue_init_fastapi|clue_init_django|clue-fastapi-sdk|clue-django-sdk|CLUE_[A-Z0-9_]+|browserTokenProvider|clue[-_])|@clue-ai\/browser-sdk/i;
44
+ const CLUE_SETUP_SUPPORT_IMPORT_PATTERN =
45
+ /^\s*(?:import\s+os|from\s+os\s+import\s+(?:getenv|environ)|import\s+json|from\s+urllib(?:\.error)?\s+import\s+[A-Za-z_,\s]+|from\s+fastapi\s+import\s+.*\b(?:HTTPException|Request)\b.*|from\s+pydantic\s+import\s+BaseModel)\s*$/m;
46
+ const CLUE_SETUP_SUPPORT_ADDITION_PATTERN =
47
+ /\b(?:os|getenv|environ|json|urllib_request|HTTPError|URLError|HTTPException|Request|BaseModel)\b/;
43
48
  const LIFECYCLE_PLAN_TOOL_SCHEMA = {
44
49
  type: "object",
45
50
  properties: {
@@ -57,6 +62,32 @@ const LIFECYCLE_PLAN_TOOL_SCHEMA = {
57
62
  additionalProperties: false,
58
63
  },
59
64
  },
65
+ file_creations: {
66
+ type: "array",
67
+ items: {
68
+ type: "object",
69
+ properties: {
70
+ file_path: { type: "string" },
71
+ content: { type: "string" },
72
+ },
73
+ required: ["file_path", "content"],
74
+ additionalProperties: false,
75
+ },
76
+ },
77
+ insertions: {
78
+ type: "array",
79
+ items: {
80
+ type: "object",
81
+ properties: {
82
+ file_path: { type: "string" },
83
+ anchor: { type: "string" },
84
+ position: { type: "string", enum: ["before", "after"] },
85
+ content: { type: "string" },
86
+ },
87
+ required: ["file_path", "anchor", "position", "content"],
88
+ additionalProperties: false,
89
+ },
90
+ },
60
91
  lifecycle_insertions: {
61
92
  type: "array",
62
93
  items: {
@@ -85,7 +116,7 @@ const LIFECYCLE_PLAN_TOOL_SCHEMA = {
85
116
  type: "object",
86
117
  properties: {
87
118
  reason: { type: "string" },
88
- evidence: { type: ["string", "null"] },
119
+ evidence: { type: "string" },
89
120
  },
90
121
  required: ["reason", "evidence"],
91
122
  additionalProperties: false,
@@ -99,6 +130,8 @@ const LIFECYCLE_PLAN_TOOL_SCHEMA = {
99
130
  required: [
100
131
  "status",
101
132
  "edits",
133
+ "file_creations",
134
+ "insertions",
102
135
  "lifecycle_insertions",
103
136
  "blockers",
104
137
  "warnings",
@@ -113,6 +146,13 @@ const nonEmpty = (value, field) => {
113
146
  return value.trim();
114
147
  };
115
148
 
149
+ const nonEmptyRaw = (value, field) => {
150
+ if (typeof value !== "string" || value.trim() === "") {
151
+ throw new Error(`${field} is required`);
152
+ }
153
+ return value;
154
+ };
155
+
116
156
  const safeRelativePath = (repoRoot, filePath) => {
117
157
  const root = resolve(repoRoot);
118
158
  const absolutePath = resolve(root, filePath);
@@ -183,6 +223,18 @@ const assertLifecycleCallsAreNonBlocking = (replacement) => {
183
223
  }
184
224
  };
185
225
 
226
+ const assertNoRepeatedFrontendClueInit = (content) => {
227
+ if (
228
+ /useEffect\s*\([\s\S]{0,1200}ClueInit/.test(
229
+ stripSourceNoise(content, { stripStrings: true }),
230
+ )
231
+ ) {
232
+ throw new Error(
233
+ "ClueInit must not run inside React component lifecycle hooks; use a module-level client singleton or SDK adapter and import it from the app bootstrap",
234
+ );
235
+ }
236
+ };
237
+
186
238
  const assertClueSetupOnlyEdit = (edit) => {
187
239
  if (!edit.replace.includes(edit.find)) {
188
240
  throw new Error(
@@ -190,13 +242,41 @@ const assertClueSetupOnlyEdit = (edit) => {
190
242
  );
191
243
  }
192
244
  const addedText = edit.replace.split(edit.find).join("");
193
- if (!CLUE_SETUP_ADDITION_PATTERN.test(addedText)) {
245
+ if (
246
+ !CLUE_SETUP_ADDITION_PATTERN.test(addedText) &&
247
+ !CLUE_SETUP_SUPPORT_ADDITION_PATTERN.test(addedText)
248
+ ) {
194
249
  throw new Error(
195
250
  `Clue lifecycle edits must add only Clue setup related code in ${edit.file_path}`,
196
251
  );
197
252
  }
198
253
  };
199
254
 
255
+ const assertClueSetupOnlyFileCreation = (fileCreation) => {
256
+ if (!CLUE_SETUP_ADDITION_PATTERN.test(fileCreation.content)) {
257
+ throw new Error(
258
+ `Clue lifecycle file creations must contain only Clue setup related code in ${fileCreation.file_path}`,
259
+ );
260
+ }
261
+ assertNoForbiddenInstrumentation(fileCreation.content);
262
+ assertLifecycleCallsAreNonBlocking(fileCreation.content);
263
+ assertNoRepeatedFrontendClueInit(fileCreation.content);
264
+ };
265
+
266
+ const assertClueSetupOnlyInsertion = (insertion) => {
267
+ if (
268
+ !CLUE_SETUP_ADDITION_PATTERN.test(insertion.content) &&
269
+ !CLUE_SETUP_SUPPORT_IMPORT_PATTERN.test(insertion.content)
270
+ ) {
271
+ throw new Error(
272
+ `Clue lifecycle insertions must contain only Clue setup related code in ${insertion.file_path}`,
273
+ );
274
+ }
275
+ assertNoForbiddenInstrumentation(insertion.content);
276
+ assertLifecycleCallsAreNonBlocking(insertion.content);
277
+ assertNoRepeatedFrontendClueInit(insertion.content);
278
+ };
279
+
200
280
  const normalizeLifecycleInsertion = (input) => ({
201
281
  api_name: assertApiName(nonEmpty(input.api_name, "api_name")),
202
282
  file_path: nonEmpty(input.file_path, "file_path"),
@@ -237,9 +317,26 @@ const normalizePlan = (input) => {
237
317
  }
238
318
  const edits = input.edits.map((edit) => ({
239
319
  file_path: nonEmpty(edit.file_path, "edit.file_path"),
240
- find: nonEmpty(edit.find, "edit.find"),
241
- replace: nonEmpty(edit.replace, "edit.replace"),
320
+ find: nonEmptyRaw(edit.find, "edit.find"),
321
+ replace: nonEmptyRaw(edit.replace, "edit.replace"),
242
322
  }));
323
+ const fileCreations = Array.isArray(input.file_creations)
324
+ ? input.file_creations.map((fileCreation) => ({
325
+ file_path: nonEmpty(fileCreation.file_path, "file_creation.file_path"),
326
+ content: nonEmptyRaw(fileCreation.content, "file_creation.content"),
327
+ }))
328
+ : [];
329
+ const insertions = Array.isArray(input.insertions)
330
+ ? input.insertions.map((insertion) => ({
331
+ file_path: nonEmpty(insertion.file_path, "insertion.file_path"),
332
+ anchor: nonEmptyRaw(insertion.anchor, "insertion.anchor"),
333
+ position:
334
+ insertion.position === "before" || insertion.position === "after"
335
+ ? insertion.position
336
+ : "after",
337
+ content: nonEmptyRaw(insertion.content, "insertion.content"),
338
+ }))
339
+ : [];
243
340
  const lifecycleInsertions = Array.isArray(input.lifecycle_insertions)
244
341
  ? input.lifecycle_insertions.map(normalizeLifecycleInsertion)
245
342
  : [];
@@ -247,7 +344,12 @@ const normalizePlan = (input) => {
247
344
  ? input.blockers.map(normalizeBlocker)
248
345
  : [];
249
346
  if (status === "blocked") {
250
- if (edits.length > 0 || lifecycleInsertions.length > 0) {
347
+ if (
348
+ edits.length > 0 ||
349
+ fileCreations.length > 0 ||
350
+ insertions.length > 0 ||
351
+ lifecycleInsertions.length > 0
352
+ ) {
251
353
  throw new Error("blocked lifecycle plan must not include edits");
252
354
  }
253
355
  if (blockers.length === 0) {
@@ -260,6 +362,8 @@ const normalizePlan = (input) => {
260
362
  return {
261
363
  status,
262
364
  edits,
365
+ fileCreations,
366
+ insertions,
263
367
  lifecycleInsertions,
264
368
  blockers,
265
369
  warnings: Array.isArray(input.warnings)
@@ -532,10 +636,12 @@ const buildLifecycleEvidenceSourceMap = async ({
532
636
  sourceByPath = new Map(),
533
637
  }) => {
534
638
  for (const filePath of [
535
- ...new Set([
536
- ...plan.edits.map((edit) => edit.file_path),
537
- ...plan.lifecycleInsertions.map((insertion) => insertion.file_path),
538
- ]),
639
+ ...new Set([
640
+ ...plan.edits.map((edit) => edit.file_path),
641
+ ...plan.fileCreations.map((fileCreation) => fileCreation.file_path),
642
+ ...plan.insertions.map((insertion) => insertion.file_path),
643
+ ...plan.lifecycleInsertions.map((insertion) => insertion.file_path),
644
+ ]),
539
645
  ]) {
540
646
  await loadSourceIntoMap({ repoRoot, sourceByPath, filePath });
541
647
  }
@@ -580,6 +686,55 @@ export const applyLifecyclePlan = async ({
580
686
  }
581
687
  const sourceByPath = new Map();
582
688
  const pendingWrites = new Map();
689
+ for (const fileCreation of plan.fileCreations) {
690
+ const { absolutePath, relativePath } = safeRelativePath(
691
+ repoRoot,
692
+ fileCreation.file_path,
693
+ );
694
+ assertAllowedWritePath({ allowedWritePaths, filePath: relativePath });
695
+ assertClueSetupOnlyFileCreation(fileCreation);
696
+ try {
697
+ await readFile(absolutePath, "utf8");
698
+ throw new Error(`file_creation target already exists: ${relativePath}`);
699
+ } catch (error) {
700
+ if (error?.code !== "ENOENT") throw error;
701
+ }
702
+ sourceByPath.set(relativePath, {
703
+ file_path: relativePath,
704
+ text: fileCreation.content,
705
+ });
706
+ pendingWrites.set(relativePath, {
707
+ absolutePath,
708
+ text: fileCreation.content,
709
+ });
710
+ }
711
+ for (const insertion of plan.insertions) {
712
+ const { absolutePath, relativePath } = safeRelativePath(
713
+ repoRoot,
714
+ insertion.file_path,
715
+ );
716
+ assertAllowedWritePath({ allowedWritePaths, filePath: relativePath });
717
+ assertClueSetupOnlyInsertion(insertion);
718
+ const current =
719
+ sourceByPath.get(relativePath)?.text ??
720
+ (await readFile(absolutePath, "utf8"));
721
+ const occurrences = current.split(insertion.anchor).length - 1;
722
+ if (occurrences !== 1) {
723
+ throw new Error(
724
+ `insertion.anchor must match exactly once in ${insertion.file_path}; matched ${occurrences}`,
725
+ );
726
+ }
727
+ const replacement =
728
+ insertion.position === "before"
729
+ ? `${insertion.content}${insertion.anchor}`
730
+ : `${insertion.anchor}${insertion.content}`;
731
+ const next = current.replace(insertion.anchor, replacement);
732
+ sourceByPath.set(relativePath, {
733
+ file_path: relativePath,
734
+ text: next,
735
+ });
736
+ pendingWrites.set(relativePath, { absolutePath, text: next });
737
+ }
583
738
  for (const edit of plan.edits) {
584
739
  const { absolutePath, relativePath } = safeRelativePath(
585
740
  repoRoot,
@@ -598,6 +753,7 @@ export const applyLifecyclePlan = async ({
598
753
  }
599
754
  assertClueSetupOnlyEdit(edit);
600
755
  assertLifecycleCallsAreNonBlocking(edit.replace);
756
+ assertNoRepeatedFrontendClueInit(edit.replace);
601
757
  const next = current.replace(edit.find, edit.replace);
602
758
  sourceByPath.set(relativePath, {
603
759
  file_path: relativePath,
@@ -624,9 +780,17 @@ export const applyLifecyclePlan = async ({
624
780
  });
625
781
  }
626
782
  for (const write of pendingWrites.values()) {
783
+ await mkdir(dirname(write.absolutePath), { recursive: true });
627
784
  await writeFile(write.absolutePath, write.text, "utf8");
628
785
  }
629
786
  return {
787
+ fileCreations: plan.fileCreations.map((fileCreation) => ({
788
+ file_path: fileCreation.file_path,
789
+ })),
790
+ insertions: plan.insertions.map((insertion) => ({
791
+ file_path: insertion.file_path,
792
+ position: insertion.position,
793
+ })),
630
794
  lifecycleInsertions: plan.lifecycleInsertions,
631
795
  warnings: plan.warnings,
632
796
  };
@@ -719,13 +883,20 @@ const buildLifecyclePrompt = ({ request, files }) =>
719
883
  task: "Add Clue SDK lifecycle API calls to this repository using exact text replacements.",
720
884
  setup_doctrine: SETUP_DOCTRINE,
721
885
  deterministic_control_model: DETERMINISTIC_CONTROL_MODEL,
886
+ official_sdk_contract: OFFICIAL_SDK_CONTRACT,
722
887
  api_connectivity_contract: API_CONNECTIVITY_CONTRACT,
723
888
  rules: [
724
889
  "Return JSON only.",
725
890
  "Understand the setup doctrine before choosing edits. This is an external Clue integration, not a host app refactor or redesign task.",
726
891
  "Use AI judgment only for lifecycle boundary placement. Let the CLI, generated skills, documentation contract, lifecycle plan schema, and setup-check static guards control deterministic mechanics.",
727
- "If SDK signatures, environment names, browser token behavior, package installability, or verification ownership are unclear, return status blocked instead of guessing.",
892
+ "Treat official_sdk_contract as verified Clue setup input from the CLI. Do not block merely because the customer repo does not already contain Clue SDK imports, dependencies, browserTokenProvider wiring, or a Clue adapter.",
893
+ "For supported FastAPI and Next.js setup paths, do not block merely because the customer repo starts without Clue code or because one non-critical lifecycle point is unclear. Complete safe minimal Clue wiring, skip unclear lifecycle points with warnings, and let setup-check drive retries.",
894
+ "Return status blocked only when the official SDK contract does not cover the detected framework, required AI/provider setup is missing, no safe bootstrap or route insertion can be found, or applying the setup would require guessing outside Clue setup scope.",
728
895
  "Use only exact replacements. Each find string must be copied exactly from source.",
896
+ "Prefer insertions for adding imports, SDK initialization, lifecycle calls, route registration, and frontend provider mounts. For insertions, choose an anchor that appears exactly once and let the CLI preserve the existing source.",
897
+ "Use edits only when a true exact replacement is necessary. Every edit.replace must contain edit.find as an exact substring; otherwise the CLI rejects it as a host-app rewrite.",
898
+ "Use file_creations only for minimal Clue-owned setup wiring files when no existing Clue adapter or browser-token proxy module exists.",
899
+ "Do not use file_creations for host application refactors, business logic, unrelated helpers, formatting, or non-Clue abstractions.",
729
900
  "Add ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout where repository code has clear lifecycle points.",
730
901
  "Official Clue SDK public lifecycle APIs are no-throw and own SDK failure isolation.",
731
902
  "Do not add per-call try/catch, try/except, .catch, or custom safe wrappers solely around official Clue SDK public lifecycle calls.",
@@ -738,6 +909,7 @@ const buildLifecyclePrompt = ({ request, files }) =>
738
909
  "Inspect backend lifecycle points as carefully as frontend lifecycle points. Backend login/session/account code is especially important.",
739
910
  "For frontend code, add or use the real @clue-ai/browser-sdk dependency through the latest channel (@clue-ai/browser-sdk@latest). Do not invent clue-js-sdk, @clue/browser-sdk, local SDK modules, global window.Clue APIs, or dynamic imports that hide a missing SDK.",
740
911
  "For FastAPI backends, add the unpinned clue-fastapi-sdk dependency when missing so pip resolves the latest release, import clue_init_fastapi plus ClueIdentify/ClueSetAccount/ClueLogout where needed, and initialize the SDK at FastAPI app creation.",
912
+ "FastAPI clue_init_fastapi must pass service_key using the setup manifest service key or CLUE_SERVICE_KEY. Do not rely on service_name alone because setup-watch and event attribution use the service key.",
741
913
  "For Django backends, use clue-django-sdk only after dependency or registry verification confirms it is installable. If it cannot be verified, report a blocker instead of adding guessed imports or dependencies.",
742
914
  "For other backend frameworks, treat SDK existence as unverified unless present in dependency files or verified through package-manager/official documentation. If no backend SDK exists or verification is impossible, report a blocker instead of silently frontend-only setup.",
743
915
  "Do not add broad ClueTrack instrumentation.",
@@ -748,10 +920,15 @@ const buildLifecyclePrompt = ({ request, files }) =>
748
920
  "Prefer stable ids and non-PII booleans/counts for ClueIdentify and ClueSetAccount traits.",
749
921
  "Use environment variable names for Clue configuration values.",
750
922
  "For Python/FastAPI code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_API_KEY, and CLUE_INGEST_ENDPOINT from the backend env block.",
923
+ "Do not use os.environ[\"CLUE_*\"] required indexing for Clue env values. Use non-crashing reads such as os.environ.get/os.getenv and skip initialization if required Clue env is missing.",
751
924
  "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.",
752
925
  "For Next.js browser/client code, read NEXT_PUBLIC_CLUE_PROJECT_KEY, NEXT_PUBLIC_CLUE_ENVIRONMENT, NEXT_PUBLIC_CLUE_SERVICE_KEY, NEXT_PUBLIC_CLUE_INGEST_ENDPOINT, and NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT from the frontend .env.local block.",
753
926
  "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.",
754
927
  "Frontend SDK adapter code is contract-owned Clue setup wiring. The AI may choose the existing import/mount point, but must not invent token URL, env, or initialization semantics.",
928
+ "If no safe frontend/browser SDK adapter boundary exists, create a minimal Clue-owned adapter file under the existing frontend source root and add the smallest exact replacement needed to import or mount it from an existing stable bootstrap point.",
929
+ "For Next.js, prefer a module-level client singleton such as src/lib/clue.ts that calls ClueInit once after checking required NEXT_PUBLIC_CLUE_* values, then import that module from app/layout.tsx or the existing app bootstrap.",
930
+ "Next.js browser SDK adapter files must begin with the exact directive \"use client\" so browser SDK code is not imported as a server module.",
931
+ "Do not create a React component whose useEffect calls ClueInit. React component lifecycle hooks, page components, sidebars, and auth callbacks are repeated UI paths and are rejected by setup-check.",
755
932
  "For Next.js frontend adapters, read the full customer-backend browser-token proxy URL from NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT. Do not derive it from NEXT_PUBLIC_API_URL, generic app API env names, detected backend ports, or relative frontend-origin paths.",
756
933
  "Do not mix stale browser-token paths such as /api/clue/browser-token, /clue/browser-tokens, or /browser-tokens with the canonical /api/v1/clue/browser-tokens path.",
757
934
  "Do not call ClueInit with empty-string fallbacks for required NEXT_PUBLIC_CLUE_* values. If required Clue env is absent, skip initialization and report the missing env names.",
@@ -759,6 +936,7 @@ const buildLifecyclePrompt = ({ request, files }) =>
759
936
  "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.",
760
937
  "Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
761
938
  "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.",
939
+ "If no backend-owned browser token proxy module exists, create a minimal Clue-owned route/module under the existing backend source root and add the smallest exact replacement needed to register it in the existing backend router.",
762
940
  "Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
763
941
  "Keep the four setup API hops distinct: customer frontend -> customer backend /api/v1/clue/browser-tokens, customer backend -> Clue /api/v1/ingest/browser-tokens, customer frontend -> Clue /api/v1/ingest/browser, and customer backend -> Clue /api/v1/ingest/backend.",
764
942
  "The local backend token endpoint is part of the customer app, not the Clue API. Place it under a Clue-reserved local route such as /api/v1/clue/browser-tokens; do not use a generic path such as /browser-tokens that could be confused with product behavior. It must call Clue server-side at /api/v1/ingest/browser-tokens.",
@@ -766,13 +944,15 @@ const buildLifecyclePrompt = ({ request, files }) =>
766
944
  "The browser token request must include the frontend service key used by ClueInit. Project key and environment may be included only as public consistency hints; the backend must use server configuration or validate them against server configuration before calling Clue.",
767
945
  "The backend browser token proxy must derive request origin from trusted request headers or server request metadata. Do not trust origin, projectKey, or environment from JSON/body payload fields when calling Clue with server CLUE_API_KEY.",
768
946
  "For browser token proxy code, the service key sent to Clue must be the frontend ClueInit serviceKey from the browser request, not the backend service's CLUE_SERVICE_KEY.",
947
+ "When the backend browser token proxy calls Clue, send the server CLUE_API_KEY in the x-clue-api-key header. Do not use Authorization bearer, query parameters, or JSON body fields for the Clue API key.",
769
948
  "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.",
949
+ "Do not introduce non-Clue HTTP client dependencies for the browser-token proxy unless they already exist in dependency files. For Python, prefer an existing HTTP client or the standard library instead of importing undeclared httpx or requests.",
770
950
  "Install Clue SDK dependencies through the latest channel. Frontend package managers must use @clue-ai/browser-sdk@latest; Python backend dependency declarations must not pin clue-fastapi-sdk or clue-django-sdk to a fixed version.",
771
951
  "Prefer minimal edits that engineers can review in one PR.",
772
952
  "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.",
773
953
  "If a lifecycle point is unclear, skip that edit and include a warning.",
774
- "Return status ready only when edits are safe to apply. Return status blocked when required SDKs or lifecycle points cannot be verified.",
775
- "If status is blocked, return no edits and no lifecycle_insertions, and list blockers. Do not encode blockers as warnings.",
954
+ "Return status ready when the safe Clue setup edits can be applied, even if individual unclear lifecycle points are skipped and listed as warnings.",
955
+ "If status is blocked, return no edits and no lifecycle_insertions, and list blockers. Use an empty string for blocker evidence when there is no specific evidence. Do not encode blockers as warnings.",
776
956
  ],
777
957
  repository_context: {
778
958
  target_tool: request.target_tool,
@@ -797,6 +977,20 @@ const buildLifecyclePrompt = ({ request, files }) =>
797
977
  output_shape: {
798
978
  status: "ready",
799
979
  blockers: [],
980
+ file_creations: [
981
+ {
982
+ file_path: "app/clue_adapter.py",
983
+ content: "new Clue-only setup file content",
984
+ },
985
+ ],
986
+ insertions: [
987
+ {
988
+ file_path: "app/main.py",
989
+ anchor: "app = FastAPI()\n",
990
+ position: "after",
991
+ content: "clue_init_fastapi(app=app)\n",
992
+ },
993
+ ],
800
994
  edits: [
801
995
  {
802
996
  file_path: "app/main.py",