@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
package/bin/clue-cli.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { runSemanticCi, runSemanticInventory } from "../src/semantic-ci.mjs";
|
|
|
20
20
|
import { runSetupCheck } from "../src/setup-check.mjs";
|
|
21
21
|
import { runSetupDetect } from "../src/setup-detect.mjs";
|
|
22
22
|
import { runSetupDoctor } from "../src/setup-doctor.mjs";
|
|
23
|
+
import { runSetupAgent } from "../src/setup-agent.mjs";
|
|
23
24
|
import { buildAiSetupHelp } from "../src/setup-help.mjs";
|
|
24
25
|
import { runSetupPrepare } from "../src/setup-prepare.mjs";
|
|
25
26
|
import { installSetupSkills } from "../src/setup-tool.mjs";
|
|
@@ -850,6 +851,7 @@ const usage = () =>
|
|
|
850
851
|
"Usage:",
|
|
851
852
|
" /clue-init",
|
|
852
853
|
` ${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment dev --documents-url <url>")}`,
|
|
854
|
+
` ${clueCliCommand("setup-agent --repo . [--ai-provider openai|anthropic --ai-provider-api-key <key> --ai-model <model>]")}`,
|
|
853
855
|
` ${clueCliCommand("setup-detect --repo .")}`,
|
|
854
856
|
` ${clueCliCommand("semantic-inventory --framework fastapi --backend-root-path backend --repo .")}`,
|
|
855
857
|
` ${clueCliCommand("semantic-agent-skills --output .clue/semantic-agent-skills.json")}`,
|
|
@@ -930,6 +932,19 @@ const main = async () => {
|
|
|
930
932
|
return;
|
|
931
933
|
}
|
|
932
934
|
|
|
935
|
+
if (command === "setup-agent") {
|
|
936
|
+
const report = await runSetupAgent({
|
|
937
|
+
env: process.env,
|
|
938
|
+
flags,
|
|
939
|
+
repoRoot,
|
|
940
|
+
});
|
|
941
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
942
|
+
if (report.status === "blocked") {
|
|
943
|
+
process.exitCode = 1;
|
|
944
|
+
}
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
933
948
|
if (command === "setup") {
|
|
934
949
|
const report = await installSetupSkills({
|
|
935
950
|
repoRoot,
|
package/package.json
CHANGED
package/src/ai-provider.mjs
CHANGED
|
@@ -25,8 +25,7 @@ const providerBaseUrl = ({ provider, env = {}, request = {} }) => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const providerModel = ({ env = {}, request = {} }) => {
|
|
28
|
-
const configuredModel =
|
|
29
|
-
env.CLUE_INIT_AI_MODEL || env.CLUE_AI_MODEL || request.ai_model;
|
|
28
|
+
const configuredModel = env.CLUE_AI_MODEL || request.ai_model;
|
|
30
29
|
const model = String(configuredModel || "").trim();
|
|
31
30
|
if (!model) {
|
|
32
31
|
throw new Error("CLUE_AI_MODEL is required");
|
|
@@ -59,6 +58,27 @@ const parseOpenAiJson = async ({ response, emptyMessage }) => {
|
|
|
59
58
|
return JSON.parse(content);
|
|
60
59
|
};
|
|
61
60
|
|
|
61
|
+
const parseOpenAiStrictToolJson = async ({ response, toolName, emptyMessage }) => {
|
|
62
|
+
const body = await response.json();
|
|
63
|
+
const output = Array.isArray(body?.output) ? body.output : [];
|
|
64
|
+
const functionCalls = output.flatMap((entry) => {
|
|
65
|
+
if (entry?.type === "function_call") return [entry];
|
|
66
|
+
if (Array.isArray(entry?.content)) {
|
|
67
|
+
return entry.content.filter((item) => item?.type === "function_call");
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
});
|
|
71
|
+
const toolCall = functionCalls.find((entry) => entry?.name === toolName);
|
|
72
|
+
const rawArguments = toolCall?.arguments ?? toolCall?.input;
|
|
73
|
+
if (typeof rawArguments === "string" && rawArguments.trim()) {
|
|
74
|
+
return JSON.parse(rawArguments);
|
|
75
|
+
}
|
|
76
|
+
if (rawArguments && typeof rawArguments === "object") {
|
|
77
|
+
return rawArguments;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(emptyMessage);
|
|
80
|
+
};
|
|
81
|
+
|
|
62
82
|
const parseAnthropicJson = async ({ response, toolName, emptyMessage }) => {
|
|
63
83
|
const body = await response.json();
|
|
64
84
|
const toolUse = Array.isArray(body?.content)
|
|
@@ -84,6 +104,76 @@ const parseAnthropicJson = async ({ response, toolName, emptyMessage }) => {
|
|
|
84
104
|
return JSON.parse(text);
|
|
85
105
|
};
|
|
86
106
|
|
|
107
|
+
export const callStrictToolAiProvider = async ({
|
|
108
|
+
config,
|
|
109
|
+
system,
|
|
110
|
+
user,
|
|
111
|
+
toolName,
|
|
112
|
+
toolDescription,
|
|
113
|
+
parameters,
|
|
114
|
+
failureMessage = "AI provider failed",
|
|
115
|
+
emptyMessage = "AI provider returned empty tool call",
|
|
116
|
+
}) => {
|
|
117
|
+
const response =
|
|
118
|
+
config.provider === "anthropic"
|
|
119
|
+
? await fetch(`${config.baseUrl}/messages`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
"content-type": "application/json",
|
|
123
|
+
"x-api-key": config.apiKey,
|
|
124
|
+
"anthropic-version": "2023-06-01",
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
model: config.model,
|
|
128
|
+
max_tokens: 4096,
|
|
129
|
+
temperature: 0,
|
|
130
|
+
system,
|
|
131
|
+
messages: [{ role: "user", content: user }],
|
|
132
|
+
tools: [
|
|
133
|
+
{
|
|
134
|
+
name: toolName,
|
|
135
|
+
description: toolDescription,
|
|
136
|
+
input_schema: parameters,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
tool_choice: { type: "tool", name: toolName },
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
142
|
+
: await fetch(`${config.baseUrl}/responses`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
"content-type": "application/json",
|
|
146
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
model: config.model,
|
|
150
|
+
input: [
|
|
151
|
+
{ role: "system", content: system },
|
|
152
|
+
{ role: "user", content: user },
|
|
153
|
+
],
|
|
154
|
+
tools: [
|
|
155
|
+
{
|
|
156
|
+
type: "function",
|
|
157
|
+
name: toolName,
|
|
158
|
+
description: toolDescription,
|
|
159
|
+
strict: true,
|
|
160
|
+
parameters,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
tool_choice: { type: "function", name: toolName },
|
|
164
|
+
parallel_tool_calls: false,
|
|
165
|
+
temperature: 0,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
throw new Error(`${failureMessage}: ${response.status}`);
|
|
171
|
+
}
|
|
172
|
+
return config.provider === "anthropic"
|
|
173
|
+
? parseAnthropicJson({ response, toolName, emptyMessage })
|
|
174
|
+
: parseOpenAiStrictToolJson({ response, toolName, emptyMessage });
|
|
175
|
+
};
|
|
176
|
+
|
|
87
177
|
export const callJsonAiProvider = async ({
|
|
88
178
|
config,
|
|
89
179
|
system,
|
package/src/fastapi-analyzer.mjs
CHANGED
|
@@ -11,6 +11,7 @@ const ROUTE_DECORATOR = new RegExp(
|
|
|
11
11
|
const API_ROUTE_DECORATOR = /@(?<router>[A-Za-z_][A-Za-z0-9_]*)\.api_route\(\s*["'](?<path>[^"']+)["'][\s\S]*?methods\s*=\s*\[(?<methods>[^\]]+)\]/gi;
|
|
12
12
|
const FUNCTION_PATTERN = /(?:async\s+def|def)\s+(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
|
|
13
13
|
const ROUTER_ASSIGNMENT = /\b(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*APIRouter\s*\(/g;
|
|
14
|
+
const ROUTER_ALIAS_ASSIGNMENT = /^\s*(?<alias>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<owner>[A-Za-z_][A-Za-z0-9_]*)\.(?<routerName>[A-Za-z_][A-Za-z0-9_]*)\s*$/gm;
|
|
14
15
|
const INCLUDE_ROUTER_CALL = /\b(?<owner>[A-Za-z_][A-Za-z0-9_]*)\.include_router\s*\(/g;
|
|
15
16
|
const FROM_IMPORT = /^\s*from\s+(?<module>[A-Za-z0-9_.]+|\.+[A-Za-z0-9_.]*)\s+import\s+(?<names>[A-Za-z0-9_,\s]+)$/gm;
|
|
16
17
|
const IMPORT_MODULE = /^\s*import\s+(?<modules>[A-Za-z0-9_.,\s]+)$/gm;
|
|
@@ -214,9 +215,35 @@ const parseImports = ({ source, currentRelativePath, filesByRelativePath }) => {
|
|
|
214
215
|
|
|
215
216
|
const routerKey = (file, routerName) => `${file}::${routerName}`;
|
|
216
217
|
|
|
217
|
-
const
|
|
218
|
+
const parseRouterAliases = ({ source, relativePath, imports }) => {
|
|
219
|
+
const aliases = new Map();
|
|
220
|
+
for (const match of source.matchAll(ROUTER_ALIAS_ASSIGNMENT)) {
|
|
221
|
+
const importedOwner = imports.get(match.groups.owner);
|
|
222
|
+
if (importedOwner?.file && !importedOwner.name) {
|
|
223
|
+
aliases.set(match.groups.alias, {
|
|
224
|
+
file: importedOwner.file,
|
|
225
|
+
name: match.groups.routerName,
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
aliases.set(match.groups.alias, {
|
|
230
|
+
file: relativePath,
|
|
231
|
+
name: match.groups.routerName,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return aliases;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const resolveRouterTarget = ({ target, relativePath, imports, routerAliases }) => {
|
|
218
238
|
const parts = target.split(".");
|
|
219
239
|
if (parts.length === 1) {
|
|
240
|
+
const aliased = routerAliases.get(target);
|
|
241
|
+
if (aliased) {
|
|
242
|
+
return {
|
|
243
|
+
file: aliased.file,
|
|
244
|
+
routerName: aliased.name,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
220
247
|
const imported = imports.get(target);
|
|
221
248
|
return {
|
|
222
249
|
file: imported?.file ?? relativePath,
|
|
@@ -236,7 +263,7 @@ const resolveRouterTarget = ({ target, relativePath, imports }) => {
|
|
|
236
263
|
};
|
|
237
264
|
};
|
|
238
265
|
|
|
239
|
-
const parseIncludeRouters = ({ source, relativePath, imports }) => {
|
|
266
|
+
const parseIncludeRouters = ({ source, relativePath, imports, routerAliases }) => {
|
|
240
267
|
const includes = [];
|
|
241
268
|
for (const match of source.matchAll(INCLUDE_ROUTER_CALL)) {
|
|
242
269
|
const openParenIndex = match.index + match[0].length - 1;
|
|
@@ -249,6 +276,7 @@ const parseIncludeRouters = ({ source, relativePath, imports }) => {
|
|
|
249
276
|
target: routerMatch.groups.router,
|
|
250
277
|
relativePath,
|
|
251
278
|
imports,
|
|
279
|
+
routerAliases,
|
|
252
280
|
});
|
|
253
281
|
includes.push({
|
|
254
282
|
ownerName: match.groups.owner,
|
|
@@ -359,6 +387,11 @@ export const analyzeFastApiRoutes = async ({ repoRoot, files }) => {
|
|
|
359
387
|
currentRelativePath: record.relativePath,
|
|
360
388
|
filesByRelativePath,
|
|
361
389
|
});
|
|
390
|
+
record.routerAliases = parseRouterAliases({
|
|
391
|
+
source: record.source,
|
|
392
|
+
relativePath: record.relativePath,
|
|
393
|
+
imports: record.imports,
|
|
394
|
+
});
|
|
362
395
|
}
|
|
363
396
|
|
|
364
397
|
const routerPrefixesByKey = new Map();
|
|
@@ -375,6 +408,7 @@ export const analyzeFastApiRoutes = async ({ repoRoot, files }) => {
|
|
|
375
408
|
source: record.source,
|
|
376
409
|
relativePath: record.relativePath,
|
|
377
410
|
imports: record.imports,
|
|
411
|
+
routerAliases: record.routerAliases,
|
|
378
412
|
})) {
|
|
379
413
|
const parentKey = record.routerPrefixes.has(include.ownerName)
|
|
380
414
|
? routerKey(record.relativePath, include.ownerName)
|
package/src/lifecycle-init.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
callJsonAiProvider,
|
|
5
|
+
callStrictToolAiProvider,
|
|
6
|
+
resolveAiProviderConfig,
|
|
7
|
+
} from "./ai-provider.mjs";
|
|
4
8
|
import {
|
|
5
9
|
extractExecutableModuleStatements,
|
|
6
10
|
findLifecycleCallApiNames,
|
|
@@ -22,6 +26,13 @@ const API_NAMES = new Set([
|
|
|
22
26
|
]);
|
|
23
27
|
|
|
24
28
|
const SOURCE_EXTENSIONS = [".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
29
|
+
const DEPENDENCY_CONTEXT_FILE_CANDIDATES = [
|
|
30
|
+
"package.json",
|
|
31
|
+
"requirements.txt",
|
|
32
|
+
"requirements-dev.txt",
|
|
33
|
+
"pyproject.toml",
|
|
34
|
+
"Pipfile",
|
|
35
|
+
];
|
|
25
36
|
const MAX_CONTEXT_FILES = 180;
|
|
26
37
|
const MAX_FILE_CHARS = 12_000;
|
|
27
38
|
const MAX_TOTAL_CHARS = 360_000;
|
|
@@ -29,6 +40,71 @@ const FRONTEND_SDK_PACKAGE = "@clue-ai/browser-sdk";
|
|
|
29
40
|
const WRONG_FRONTEND_SDK_PACKAGES = ["clue-js-sdk", "@clue/browser-sdk"];
|
|
30
41
|
const CLUE_SETUP_ADDITION_PATTERN =
|
|
31
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
|
+
const LIFECYCLE_PLAN_TOOL_SCHEMA = {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
status: { type: "string", enum: ["ready", "blocked"] },
|
|
47
|
+
edits: {
|
|
48
|
+
type: "array",
|
|
49
|
+
items: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
file_path: { type: "string" },
|
|
53
|
+
find: { type: "string" },
|
|
54
|
+
replace: { type: "string" },
|
|
55
|
+
},
|
|
56
|
+
required: ["file_path", "find", "replace"],
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
lifecycle_insertions: {
|
|
61
|
+
type: "array",
|
|
62
|
+
items: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
api_name: {
|
|
66
|
+
type: "string",
|
|
67
|
+
enum: [
|
|
68
|
+
"ClueInit",
|
|
69
|
+
"ClueIdentify",
|
|
70
|
+
"ClueSetAccount",
|
|
71
|
+
"ClueLogout",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
file_path: { type: "string" },
|
|
75
|
+
confidence: { type: "number" },
|
|
76
|
+
reason: { type: "string" },
|
|
77
|
+
},
|
|
78
|
+
required: ["api_name", "file_path", "confidence", "reason"],
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
blockers: {
|
|
83
|
+
type: "array",
|
|
84
|
+
items: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
reason: { type: "string" },
|
|
88
|
+
evidence: { type: ["string", "null"] },
|
|
89
|
+
},
|
|
90
|
+
required: ["reason", "evidence"],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
warnings: {
|
|
95
|
+
type: "array",
|
|
96
|
+
items: { type: "string" },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: [
|
|
100
|
+
"status",
|
|
101
|
+
"edits",
|
|
102
|
+
"lifecycle_insertions",
|
|
103
|
+
"blockers",
|
|
104
|
+
"warnings",
|
|
105
|
+
],
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
};
|
|
32
108
|
|
|
33
109
|
const nonEmpty = (value, field) => {
|
|
34
110
|
if (typeof value !== "string" || value.trim() === "") {
|
|
@@ -47,6 +123,33 @@ const safeRelativePath = (repoRoot, filePath) => {
|
|
|
47
123
|
return { absolutePath, relativePath };
|
|
48
124
|
};
|
|
49
125
|
|
|
126
|
+
const startsWithAllowedPath = (filePath, allowedPath) => {
|
|
127
|
+
const normalizedFilePath = filePath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
128
|
+
const normalizedAllowedPath = String(allowedPath ?? "")
|
|
129
|
+
.replace(/\\/g, "/")
|
|
130
|
+
.replace(/^\/+/, "")
|
|
131
|
+
.replace(/\/+$/, "");
|
|
132
|
+
return (
|
|
133
|
+
normalizedAllowedPath !== "" &&
|
|
134
|
+
(normalizedFilePath === normalizedAllowedPath ||
|
|
135
|
+
normalizedFilePath.startsWith(`${normalizedAllowedPath}/`))
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const assertAllowedWritePath = ({ allowedWritePaths, filePath }) => {
|
|
140
|
+
if (!Array.isArray(allowedWritePaths) || allowedWritePaths.length === 0) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
allowedWritePaths.some((allowedPath) =>
|
|
145
|
+
startsWithAllowedPath(filePath, allowedPath),
|
|
146
|
+
)
|
|
147
|
+
) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`setup-agent cannot edit outside allowed setup paths: ${filePath}`);
|
|
151
|
+
};
|
|
152
|
+
|
|
50
153
|
const assertApiName = (apiName) => {
|
|
51
154
|
if (!API_NAMES.has(apiName)) {
|
|
52
155
|
throw new Error(`unsupported lifecycle API: ${apiName}`);
|
|
@@ -458,7 +561,11 @@ const buildLifecycleEvidenceSourceMap = async ({
|
|
|
458
561
|
return sourceByPath;
|
|
459
562
|
};
|
|
460
563
|
|
|
461
|
-
export const applyLifecyclePlan = async ({
|
|
564
|
+
export const applyLifecyclePlan = async ({
|
|
565
|
+
repoRoot,
|
|
566
|
+
plan: rawPlan,
|
|
567
|
+
allowedWritePaths,
|
|
568
|
+
}) => {
|
|
462
569
|
const plan = normalizePlan(rawPlan);
|
|
463
570
|
if (plan.status === "blocked") {
|
|
464
571
|
throw new Error(
|
|
@@ -478,6 +585,7 @@ export const applyLifecyclePlan = async ({ repoRoot, plan: rawPlan }) => {
|
|
|
478
585
|
repoRoot,
|
|
479
586
|
edit.file_path,
|
|
480
587
|
);
|
|
588
|
+
assertAllowedWritePath({ allowedWritePaths, filePath: relativePath });
|
|
481
589
|
assertNoForbiddenInstrumentation(edit.replace);
|
|
482
590
|
const current =
|
|
483
591
|
sourceByPath.get(relativePath)?.text ??
|
|
@@ -499,6 +607,10 @@ export const applyLifecyclePlan = async ({ repoRoot, plan: rawPlan }) => {
|
|
|
499
607
|
}
|
|
500
608
|
await buildLifecycleEvidenceSourceMap({ repoRoot, plan, sourceByPath });
|
|
501
609
|
for (const insertion of plan.lifecycleInsertions) {
|
|
610
|
+
assertAllowedWritePath({
|
|
611
|
+
allowedWritePaths,
|
|
612
|
+
filePath: insertion.file_path,
|
|
613
|
+
});
|
|
502
614
|
const source = sourceByPath.get(insertion.file_path);
|
|
503
615
|
if (!source) {
|
|
504
616
|
throw new Error(
|
|
@@ -520,6 +632,42 @@ export const applyLifecyclePlan = async ({ repoRoot, plan: rawPlan }) => {
|
|
|
520
632
|
};
|
|
521
633
|
};
|
|
522
634
|
|
|
635
|
+
const dependencyContextCandidatePaths = (allowedSourcePaths) => [
|
|
636
|
+
...DEPENDENCY_CONTEXT_FILE_CANDIDATES,
|
|
637
|
+
...allowedSourcePaths.flatMap((root) => {
|
|
638
|
+
const normalized = root.replace(/\/+$/, "");
|
|
639
|
+
const roots =
|
|
640
|
+
normalized.endsWith("/src") || normalized.endsWith("/app")
|
|
641
|
+
? [normalized, dirname(normalized)]
|
|
642
|
+
: [normalized];
|
|
643
|
+
return roots.flatMap((candidateRoot) =>
|
|
644
|
+
DEPENDENCY_CONTEXT_FILE_CANDIDATES.map((file) =>
|
|
645
|
+
join(candidateRoot, file),
|
|
646
|
+
),
|
|
647
|
+
);
|
|
648
|
+
}),
|
|
649
|
+
];
|
|
650
|
+
|
|
651
|
+
const addContextFile = async ({
|
|
652
|
+
absolutePath,
|
|
653
|
+
context,
|
|
654
|
+
repoRoot,
|
|
655
|
+
totalChars,
|
|
656
|
+
}) => {
|
|
657
|
+
const text = await readFile(absolutePath, "utf8");
|
|
658
|
+
const snippet = text.slice(0, MAX_FILE_CHARS);
|
|
659
|
+
const nextTotalChars = totalChars + snippet.length;
|
|
660
|
+
if (nextTotalChars > MAX_TOTAL_CHARS) {
|
|
661
|
+
return { totalChars, added: false };
|
|
662
|
+
}
|
|
663
|
+
context.push({
|
|
664
|
+
file_path: relative(resolve(repoRoot), absolutePath),
|
|
665
|
+
source: snippet,
|
|
666
|
+
truncated: text.length > snippet.length,
|
|
667
|
+
});
|
|
668
|
+
return { totalChars: nextTotalChars, added: true };
|
|
669
|
+
};
|
|
670
|
+
|
|
523
671
|
const readContextFiles = async ({ repoRoot, request }) => {
|
|
524
672
|
const files = await listAllowedSourceFiles({
|
|
525
673
|
repoRoot,
|
|
@@ -530,17 +678,38 @@ const readContextFiles = async ({ repoRoot, request }) => {
|
|
|
530
678
|
const context = [];
|
|
531
679
|
let totalChars = 0;
|
|
532
680
|
for (const absolutePath of files.slice(0, MAX_CONTEXT_FILES)) {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
681
|
+
const result = await addContextFile({
|
|
682
|
+
absolutePath,
|
|
683
|
+
context,
|
|
684
|
+
repoRoot,
|
|
685
|
+
totalChars,
|
|
686
|
+
});
|
|
687
|
+
totalChars = result.totalChars;
|
|
688
|
+
if (!result.added) {
|
|
537
689
|
break;
|
|
538
690
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
691
|
+
}
|
|
692
|
+
const seen = new Set(context.map((entry) => entry.file_path));
|
|
693
|
+
for (const candidatePath of dependencyContextCandidatePaths(
|
|
694
|
+
request.allowed_source_paths,
|
|
695
|
+
)) {
|
|
696
|
+
const { absolutePath, relativePath } = safeRelativePath(
|
|
697
|
+
repoRoot,
|
|
698
|
+
candidatePath,
|
|
699
|
+
);
|
|
700
|
+
if (seen.has(relativePath)) continue;
|
|
701
|
+
try {
|
|
702
|
+
const result = await addContextFile({
|
|
703
|
+
absolutePath,
|
|
704
|
+
context,
|
|
705
|
+
repoRoot,
|
|
706
|
+
totalChars,
|
|
707
|
+
});
|
|
708
|
+
totalChars = result.totalChars;
|
|
709
|
+
if (result.added) seen.add(relativePath);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (error?.code !== "ENOENT") throw error;
|
|
712
|
+
}
|
|
544
713
|
}
|
|
545
714
|
return context;
|
|
546
715
|
};
|
|
@@ -567,8 +736,8 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
567
736
|
"Find all clear account, workspace, organization, or tenant resolution paths and add ClueSetAccount to every one of them.",
|
|
568
737
|
"Find all clear logout or session reset paths and add ClueLogout to every one of them.",
|
|
569
738
|
"Inspect backend lifecycle points as carefully as frontend lifecycle points. Backend login/session/account code is especially important.",
|
|
570
|
-
"For frontend code, add or use the real @clue-ai/browser-sdk dependency. Do not invent clue-js-sdk, @clue/browser-sdk, local SDK modules, global window.Clue APIs, or dynamic imports that hide a missing SDK.",
|
|
571
|
-
"For FastAPI backends, add the clue-fastapi-sdk dependency when missing, import clue_init_fastapi plus ClueIdentify/ClueSetAccount/ClueLogout where needed, and initialize the SDK at FastAPI app creation.",
|
|
739
|
+
"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
|
+
"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.",
|
|
572
741
|
"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.",
|
|
573
742
|
"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.",
|
|
574
743
|
"Do not add broad ClueTrack instrumentation.",
|
|
@@ -580,20 +749,25 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
580
749
|
"Use environment variable names for Clue configuration values.",
|
|
581
750
|
"For Python/FastAPI code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_API_KEY, and CLUE_INGEST_ENDPOINT from the backend env block.",
|
|
582
751
|
"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.",
|
|
583
|
-
"For Next.js browser/client code, read NEXT_PUBLIC_CLUE_PROJECT_KEY, NEXT_PUBLIC_CLUE_ENVIRONMENT, NEXT_PUBLIC_CLUE_SERVICE_KEY, and
|
|
752
|
+
"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.",
|
|
584
753
|
"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
|
+
"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.",
|
|
755
|
+
"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
|
+
"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
|
+
"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.",
|
|
758
|
+
"If a singleton guard is used, do not set initialized = true before ClueInit has actually been called with required config present.",
|
|
585
759
|
"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.",
|
|
586
760
|
"Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
587
761
|
"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.",
|
|
588
762
|
"Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
|
|
589
763
|
"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.",
|
|
590
764
|
"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.",
|
|
591
|
-
"The frontend browserTokenProvider must send the same service key used by ClueInit to the customer backend token endpoint. For Next.js this value comes from NEXT_PUBLIC_CLUE_SERVICE_KEY.",
|
|
765
|
+
"The frontend browserTokenProvider must call NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT and send the same service key used by ClueInit to the customer backend token endpoint. For Next.js this value comes from NEXT_PUBLIC_CLUE_SERVICE_KEY.",
|
|
592
766
|
"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.",
|
|
593
767
|
"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.",
|
|
594
768
|
"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.",
|
|
595
769
|
"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.",
|
|
596
|
-
"
|
|
770
|
+
"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.",
|
|
597
771
|
"Prefer minimal edits that engineers can review in one PR.",
|
|
598
772
|
"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.",
|
|
599
773
|
"If a lifecycle point is unclear, skip that edit and include a warning.",
|
|
@@ -614,6 +788,8 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
614
788
|
browser_ingest_endpoint_env: "framework_specific",
|
|
615
789
|
nextjs_browser_ingest_endpoint_env: "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
616
790
|
client_backend_browser_token_proxy_path: "/api/v1/clue/browser-tokens",
|
|
791
|
+
nextjs_browser_token_endpoint_env:
|
|
792
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
617
793
|
clue_backend_browser_token_issue_path: "/api/v1/ingest/browser-tokens",
|
|
618
794
|
nextjs_browser_service_key_env: "NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
619
795
|
service_key: request.service_key,
|
|
@@ -660,3 +836,45 @@ export const planLifecycleInsertions = async ({ repoRoot, request, env }) => {
|
|
|
660
836
|
emptyMessage: "AI provider returned empty lifecycle plan",
|
|
661
837
|
});
|
|
662
838
|
};
|
|
839
|
+
|
|
840
|
+
export const planLifecycleInsertionsStrict = async ({
|
|
841
|
+
repoRoot,
|
|
842
|
+
request,
|
|
843
|
+
env,
|
|
844
|
+
failureContext = null,
|
|
845
|
+
}) => {
|
|
846
|
+
const apiKey = env.CLUE_AI_PROVIDER_API_KEY;
|
|
847
|
+
if (!apiKey) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
"CLUE_AI_PROVIDER_API_KEY is required for setup-agent lifecycle planning",
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const setupAgentEnv = {
|
|
853
|
+
...env,
|
|
854
|
+
CLUE_AI_PROVIDER_BASE_URL: undefined,
|
|
855
|
+
};
|
|
856
|
+
const files = await readContextFiles({ repoRoot, request });
|
|
857
|
+
const prompt = JSON.parse(buildLifecyclePrompt({ request, files }));
|
|
858
|
+
const user = JSON.stringify({
|
|
859
|
+
...prompt,
|
|
860
|
+
setup_agent_mode: {
|
|
861
|
+
execution_owner: "clue_cli",
|
|
862
|
+
model_role: "lifecycle_plan_only",
|
|
863
|
+
direct_file_writes_allowed: false,
|
|
864
|
+
setup_watch_allowed: false,
|
|
865
|
+
retry_failure_context: failureContext,
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
return callStrictToolAiProvider({
|
|
869
|
+
config: resolveAiProviderConfig({ env: setupAgentEnv, apiKey }),
|
|
870
|
+
system:
|
|
871
|
+
"You are a safe code-edit planner for Clue SDK setup. Call the provided function with a schema-valid lifecycle plan only.",
|
|
872
|
+
user,
|
|
873
|
+
toolName: "propose_clue_lifecycle_plan",
|
|
874
|
+
toolDescription:
|
|
875
|
+
"Return the exact Clue SDK lifecycle insertion plan for the local CLI to validate and apply.",
|
|
876
|
+
parameters: LIFECYCLE_PLAN_TOOL_SCHEMA,
|
|
877
|
+
failureMessage: "AI provider failed during setup-agent lifecycle planning",
|
|
878
|
+
emptyMessage: "AI provider returned empty setup-agent lifecycle plan",
|
|
879
|
+
});
|
|
880
|
+
};
|
package/src/public-schema.cjs
CHANGED
|
@@ -185,6 +185,7 @@ const semanticSnapshotPurposeChangeStateValues = [
|
|
|
185
185
|
const semanticSnapshotActiveSemanticSourceValues = [
|
|
186
186
|
"previous_reused",
|
|
187
187
|
"new_confirmed",
|
|
188
|
+
"new_unconfirmed",
|
|
188
189
|
"previous_kept_pending_review",
|
|
189
190
|
];
|
|
190
191
|
const targetObjectKeySchema = nonEmptyStringSchema.regex(snakeSegmentPattern, {
|