@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/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
|
-
|
|
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
package/src/ai-provider.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 })
|
package/src/lifecycle-init.mjs
CHANGED
|
@@ -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(?:
|
|
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:
|
|
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 (
|
|
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:
|
|
241
|
-
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 (
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
"
|
|
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
|
|
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",
|