@clue-ai/cli 0.0.16 → 0.0.18
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/README.md +9 -0
- package/bin/clue-cli.mjs +11 -0
- package/package.json +1 -1
- package/src/contracts.mjs +1 -1
- package/src/lifecycle-init.mjs +26 -5
- package/src/public-schema.cjs +81 -0
- package/src/semantic-ci.mjs +560 -45
- package/src/setup-ai-contract.mjs +114 -0
- package/src/setup-check.mjs +174 -2
- package/src/setup-doctor.mjs +442 -0
- package/src/setup-help.mjs +23 -2
- package/src/setup-prepare.mjs +27 -0
- package/src/setup-tool.mjs +31 -7
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { API_CONNECTIVITY_CONTRACT } from "./setup-ai-contract.mjs";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
6
|
+
const BROWSER_TOKEN_PROXY_PATH =
|
|
7
|
+
API_CONNECTIVITY_CONTRACT.hops.client_backend_browser_token_proxy.path;
|
|
8
|
+
const CLUE_BROWSER_TOKEN_PATH =
|
|
9
|
+
API_CONNECTIVITY_CONTRACT.hops.clue_backend_browser_token_issue.path;
|
|
10
|
+
const BROWSER_INGEST_PATH = API_CONNECTIVITY_CONTRACT.hops.browser_ingest.path;
|
|
11
|
+
const BACKEND_INGEST_PATH = API_CONNECTIVITY_CONTRACT.hops.backend_ingest.path;
|
|
12
|
+
|
|
13
|
+
const optionalString = (value) =>
|
|
14
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
15
|
+
|
|
16
|
+
const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
|
|
17
|
+
|
|
18
|
+
const joinUrl = (baseUrl, path) => `${trimTrailingSlash(baseUrl)}${path}`;
|
|
19
|
+
|
|
20
|
+
const readJsonIfPresent = async (path) => {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error?.code === "ENOENT") return null;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const manifestWatchTargets = (manifest, kind) => {
|
|
30
|
+
const targets = manifest?.lifecycle_verification?.watch_targets;
|
|
31
|
+
if (!Array.isArray(targets)) return [];
|
|
32
|
+
return targets.filter((target) => target?.kind === kind);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const firstTargetUrl = ({ env, manifest, kind }) => {
|
|
36
|
+
for (const target of manifestWatchTargets(manifest, kind)) {
|
|
37
|
+
const explicitUrl = optionalString(target.url);
|
|
38
|
+
if (explicitUrl) return explicitUrl;
|
|
39
|
+
const envName = optionalString(target.url_env_name);
|
|
40
|
+
if (envName && optionalString(env[envName])) return optionalString(env[envName]);
|
|
41
|
+
const candidates = Array.isArray(target.local_url_candidates)
|
|
42
|
+
? target.local_url_candidates
|
|
43
|
+
: [];
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
const value = optionalString(candidate);
|
|
46
|
+
if (value) return value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const firstTargetServiceKey = (manifest, kind) => {
|
|
53
|
+
for (const target of manifestWatchTargets(manifest, kind)) {
|
|
54
|
+
const serviceKey = optionalString(target.service_key ?? target.serviceKey);
|
|
55
|
+
if (serviceKey) return serviceKey;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const readTextResponse = async (response) => {
|
|
61
|
+
try {
|
|
62
|
+
return await response.text();
|
|
63
|
+
} catch {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const parseJsonText = (text) => {
|
|
69
|
+
try {
|
|
70
|
+
return text.trim() ? JSON.parse(text) : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const postJson = async ({ body, fetchImpl, headers = {}, url }) => {
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetchImpl(url, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
"content-type": "application/json",
|
|
82
|
+
...headers,
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(body),
|
|
85
|
+
});
|
|
86
|
+
const text = await readTextResponse(response);
|
|
87
|
+
return {
|
|
88
|
+
transportOk: true,
|
|
89
|
+
response,
|
|
90
|
+
text,
|
|
91
|
+
json: parseJsonText(text),
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return {
|
|
95
|
+
transportOk: false,
|
|
96
|
+
error: error instanceof Error ? error.message : String(error),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const compactFailure = (result) => {
|
|
102
|
+
if (!result.transportOk) return result.error ?? "request failed";
|
|
103
|
+
const jsonMessage =
|
|
104
|
+
typeof result.json?.message === "string"
|
|
105
|
+
? result.json.message
|
|
106
|
+
: typeof result.json?.error === "string"
|
|
107
|
+
? result.json.error
|
|
108
|
+
: null;
|
|
109
|
+
return jsonMessage ?? result.text?.slice(0, 240) ?? "request failed";
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const tokenFromResult = (result) =>
|
|
113
|
+
result.transportOk &&
|
|
114
|
+
result.response.ok &&
|
|
115
|
+
typeof result.json?.token === "string" &&
|
|
116
|
+
result.json.token.trim()
|
|
117
|
+
? result.json.token.trim()
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
const buildCheck = ({
|
|
121
|
+
error = null,
|
|
122
|
+
id,
|
|
123
|
+
method = "POST",
|
|
124
|
+
passed,
|
|
125
|
+
result = null,
|
|
126
|
+
url,
|
|
127
|
+
}) => ({
|
|
128
|
+
id,
|
|
129
|
+
method,
|
|
130
|
+
url,
|
|
131
|
+
passed: Boolean(passed),
|
|
132
|
+
status: result?.transportOk ? result.response.status : null,
|
|
133
|
+
error: passed ? null : (error ?? compactFailure(result)),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const buildBrowserEventPayload = ({ environment, projectKey, serviceKey }) => {
|
|
137
|
+
const timestamp = new Date().toISOString();
|
|
138
|
+
const id = `setup_doctor_${Date.now()}`;
|
|
139
|
+
return {
|
|
140
|
+
batch_id: `batch_${id}`,
|
|
141
|
+
idempotency_key: `idem_${id}`,
|
|
142
|
+
sent_at: timestamp,
|
|
143
|
+
source_type: "browser_sdk",
|
|
144
|
+
source_schema_version: "1",
|
|
145
|
+
producer_metadata: {
|
|
146
|
+
producer_id: serviceKey,
|
|
147
|
+
sdk_type: "browser",
|
|
148
|
+
sdk_version: "setup-doctor",
|
|
149
|
+
},
|
|
150
|
+
events: [
|
|
151
|
+
{
|
|
152
|
+
event_id: `event_${id}`,
|
|
153
|
+
event_category: "custom",
|
|
154
|
+
event_name: "setup_doctor_browser_connectivity",
|
|
155
|
+
occurred_at: timestamp,
|
|
156
|
+
source_event_id: `event_${id}`,
|
|
157
|
+
source_schema_version: "1",
|
|
158
|
+
source_event_type: "setup_doctor_browser_connectivity",
|
|
159
|
+
source_event_kind: "custom",
|
|
160
|
+
producer_id: serviceKey,
|
|
161
|
+
project_key: projectKey,
|
|
162
|
+
environment,
|
|
163
|
+
properties: {},
|
|
164
|
+
metrics: { count: 1 },
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const buildBackendEventPayload = ({
|
|
171
|
+
apiKey,
|
|
172
|
+
backendServiceKey,
|
|
173
|
+
environment,
|
|
174
|
+
projectKey,
|
|
175
|
+
}) => {
|
|
176
|
+
const timestamp = new Date().toISOString();
|
|
177
|
+
return {
|
|
178
|
+
projectKey,
|
|
179
|
+
apiKey,
|
|
180
|
+
environment,
|
|
181
|
+
sdkType: "backend",
|
|
182
|
+
sdkVersion: "setup-doctor",
|
|
183
|
+
schemaVersion: 1,
|
|
184
|
+
events: [
|
|
185
|
+
{
|
|
186
|
+
event_name: "setup_doctor_backend_connectivity",
|
|
187
|
+
occurred_at: timestamp,
|
|
188
|
+
service_key: backendServiceKey,
|
|
189
|
+
properties: {},
|
|
190
|
+
metrics: { count: 1 },
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const requiredInputCheck = ({ id, missing, url = null }) => ({
|
|
197
|
+
id,
|
|
198
|
+
method: "POST",
|
|
199
|
+
url,
|
|
200
|
+
passed: false,
|
|
201
|
+
status: null,
|
|
202
|
+
error: `missing required input: ${missing.join(", ")}`,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
export const runSetupDoctor = async ({
|
|
206
|
+
env = process.env,
|
|
207
|
+
fetchImpl = fetch,
|
|
208
|
+
flags,
|
|
209
|
+
repoRoot = ".",
|
|
210
|
+
}) => {
|
|
211
|
+
const manifestPath = String(
|
|
212
|
+
flags.get("manifest") || DEFAULT_SETUP_MANIFEST_PATH,
|
|
213
|
+
);
|
|
214
|
+
const manifest = await readJsonIfPresent(resolve(repoRoot, manifestPath));
|
|
215
|
+
const clueApiBaseUrl =
|
|
216
|
+
optionalString(flags.get("clue-api-base-url")) ??
|
|
217
|
+
optionalString(env.CLUE_API_BASE_URL) ??
|
|
218
|
+
optionalString(manifest?.clue_context?.clue_api_base_url);
|
|
219
|
+
const projectKey =
|
|
220
|
+
optionalString(flags.get("project-key")) ??
|
|
221
|
+
optionalString(env.CLUE_PROJECT_KEY) ??
|
|
222
|
+
optionalString(manifest?.clue_context?.project_key);
|
|
223
|
+
const environment =
|
|
224
|
+
optionalString(flags.get("environment")) ??
|
|
225
|
+
optionalString(env.CLUE_ENVIRONMENT) ??
|
|
226
|
+
optionalString(manifest?.clue_context?.environment) ??
|
|
227
|
+
"dev";
|
|
228
|
+
const apiKey =
|
|
229
|
+
optionalString(flags.get("clue-api-key")) ??
|
|
230
|
+
optionalString(env.CLUE_API_KEY);
|
|
231
|
+
const serviceKey =
|
|
232
|
+
optionalString(flags.get("service-key")) ??
|
|
233
|
+
optionalString(env.NEXT_PUBLIC_CLUE_SERVICE_KEY) ??
|
|
234
|
+
firstTargetServiceKey(manifest, "frontend");
|
|
235
|
+
const backendServiceKey =
|
|
236
|
+
optionalString(flags.get("backend-service-key")) ??
|
|
237
|
+
optionalString(env.CLUE_SERVICE_KEY) ??
|
|
238
|
+
firstTargetServiceKey(manifest, "backend") ??
|
|
239
|
+
serviceKey;
|
|
240
|
+
const clientBackendUrl =
|
|
241
|
+
optionalString(flags.get("client-backend-url")) ??
|
|
242
|
+
firstTargetUrl({ env, manifest, kind: "backend" });
|
|
243
|
+
const clientFrontendUrl =
|
|
244
|
+
optionalString(flags.get("client-frontend-url")) ??
|
|
245
|
+
firstTargetUrl({ env, manifest, kind: "frontend" });
|
|
246
|
+
const origin =
|
|
247
|
+
optionalString(flags.get("origin")) ??
|
|
248
|
+
clientFrontendUrl ??
|
|
249
|
+
"http://localhost";
|
|
250
|
+
const browserTokenProxyUrl =
|
|
251
|
+
optionalString(flags.get("browser-token-proxy-url")) ??
|
|
252
|
+
optionalString(env.NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT) ??
|
|
253
|
+
optionalString(env.CLUE_BROWSER_TOKEN_ENDPOINT) ??
|
|
254
|
+
(clientBackendUrl ? joinUrl(clientBackendUrl, BROWSER_TOKEN_PROXY_PATH) : null);
|
|
255
|
+
const clueBrowserTokenUrl = clueApiBaseUrl
|
|
256
|
+
? joinUrl(clueApiBaseUrl, CLUE_BROWSER_TOKEN_PATH)
|
|
257
|
+
: null;
|
|
258
|
+
const browserIngestUrl =
|
|
259
|
+
optionalString(flags.get("browser-ingest-url")) ??
|
|
260
|
+
optionalString(env.NEXT_PUBLIC_CLUE_INGEST_ENDPOINT) ??
|
|
261
|
+
optionalString(manifest?.clue_context?.ingest_endpoints?.browser) ??
|
|
262
|
+
(clueApiBaseUrl ? joinUrl(clueApiBaseUrl, BROWSER_INGEST_PATH) : null);
|
|
263
|
+
const backendIngestUrl =
|
|
264
|
+
optionalString(flags.get("backend-ingest-url")) ??
|
|
265
|
+
optionalString(env.CLUE_INGEST_ENDPOINT) ??
|
|
266
|
+
optionalString(manifest?.clue_context?.ingest_endpoints?.backend) ??
|
|
267
|
+
(clueApiBaseUrl ? joinUrl(clueApiBaseUrl, BACKEND_INGEST_PATH) : null);
|
|
268
|
+
|
|
269
|
+
const checks = [];
|
|
270
|
+
|
|
271
|
+
let proxyToken = null;
|
|
272
|
+
if (!browserTokenProxyUrl || !serviceKey) {
|
|
273
|
+
checks.push(
|
|
274
|
+
requiredInputCheck({
|
|
275
|
+
id: "client_backend_browser_token_proxy",
|
|
276
|
+
missing: [
|
|
277
|
+
...(!browserTokenProxyUrl
|
|
278
|
+
? [
|
|
279
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT or CLUE_BROWSER_TOKEN_ENDPOINT or client-backend-url",
|
|
280
|
+
]
|
|
281
|
+
: []),
|
|
282
|
+
...(!serviceKey ? ["service-key"] : []),
|
|
283
|
+
],
|
|
284
|
+
url: browserTokenProxyUrl,
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
} else {
|
|
288
|
+
const result = await postJson({
|
|
289
|
+
fetchImpl,
|
|
290
|
+
url: browserTokenProxyUrl,
|
|
291
|
+
headers: { origin },
|
|
292
|
+
body: { serviceKey },
|
|
293
|
+
});
|
|
294
|
+
proxyToken = tokenFromResult(result);
|
|
295
|
+
checks.push(
|
|
296
|
+
buildCheck({
|
|
297
|
+
id: "client_backend_browser_token_proxy",
|
|
298
|
+
passed: Boolean(proxyToken),
|
|
299
|
+
result,
|
|
300
|
+
url: browserTokenProxyUrl,
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let directToken = null;
|
|
306
|
+
if (!clueBrowserTokenUrl || !apiKey || !projectKey || !environment || !serviceKey) {
|
|
307
|
+
checks.push(
|
|
308
|
+
requiredInputCheck({
|
|
309
|
+
id: "clue_backend_browser_token_issue",
|
|
310
|
+
missing: [
|
|
311
|
+
...(!clueBrowserTokenUrl ? ["clue-api-base-url"] : []),
|
|
312
|
+
...(!apiKey ? ["CLUE_API_KEY"] : []),
|
|
313
|
+
...(!projectKey ? ["CLUE_PROJECT_KEY"] : []),
|
|
314
|
+
...(!environment ? ["CLUE_ENVIRONMENT"] : []),
|
|
315
|
+
...(!serviceKey ? ["service-key"] : []),
|
|
316
|
+
],
|
|
317
|
+
url: clueBrowserTokenUrl,
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
const result = await postJson({
|
|
322
|
+
fetchImpl,
|
|
323
|
+
url: clueBrowserTokenUrl,
|
|
324
|
+
headers: { "x-clue-api-key": apiKey },
|
|
325
|
+
body: {
|
|
326
|
+
projectKey,
|
|
327
|
+
environment,
|
|
328
|
+
serviceKey,
|
|
329
|
+
origin,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
directToken = tokenFromResult(result);
|
|
333
|
+
checks.push(
|
|
334
|
+
buildCheck({
|
|
335
|
+
id: "clue_backend_browser_token_issue",
|
|
336
|
+
passed: Boolean(directToken),
|
|
337
|
+
result,
|
|
338
|
+
url: clueBrowserTokenUrl,
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const browserToken = proxyToken ?? directToken;
|
|
344
|
+
if (!browserIngestUrl || !projectKey || !environment || !serviceKey || !browserToken) {
|
|
345
|
+
checks.push(
|
|
346
|
+
requiredInputCheck({
|
|
347
|
+
id: "browser_ingest",
|
|
348
|
+
missing: [
|
|
349
|
+
...(!browserIngestUrl ? ["browser-ingest-url"] : []),
|
|
350
|
+
...(!projectKey ? ["CLUE_PROJECT_KEY"] : []),
|
|
351
|
+
...(!environment ? ["CLUE_ENVIRONMENT"] : []),
|
|
352
|
+
...(!serviceKey ? ["service-key"] : []),
|
|
353
|
+
...(!browserToken ? ["browser token"] : []),
|
|
354
|
+
],
|
|
355
|
+
url: browserIngestUrl,
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
} else {
|
|
359
|
+
const result = await postJson({
|
|
360
|
+
fetchImpl,
|
|
361
|
+
url: browserIngestUrl,
|
|
362
|
+
headers: {
|
|
363
|
+
origin,
|
|
364
|
+
"x-clue-project-key": projectKey,
|
|
365
|
+
"x-clue-service-key": serviceKey,
|
|
366
|
+
"x-clue-browser-token": browserToken,
|
|
367
|
+
"x-clue-sdk-request": "browser",
|
|
368
|
+
"x-clue-environment": environment,
|
|
369
|
+
"x-clue-sdk-version": "setup-doctor",
|
|
370
|
+
"x-clue-source-schema-version": "1",
|
|
371
|
+
},
|
|
372
|
+
body: buildBrowserEventPayload({ environment, projectKey, serviceKey }),
|
|
373
|
+
});
|
|
374
|
+
checks.push(
|
|
375
|
+
buildCheck({
|
|
376
|
+
id: "browser_ingest",
|
|
377
|
+
passed: Boolean(result.transportOk && result.response.ok),
|
|
378
|
+
result,
|
|
379
|
+
url: browserIngestUrl,
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!backendIngestUrl || !apiKey || !projectKey || !environment || !backendServiceKey) {
|
|
385
|
+
checks.push(
|
|
386
|
+
requiredInputCheck({
|
|
387
|
+
id: "backend_ingest",
|
|
388
|
+
missing: [
|
|
389
|
+
...(!backendIngestUrl ? ["backend-ingest-url"] : []),
|
|
390
|
+
...(!apiKey ? ["CLUE_API_KEY"] : []),
|
|
391
|
+
...(!projectKey ? ["CLUE_PROJECT_KEY"] : []),
|
|
392
|
+
...(!environment ? ["CLUE_ENVIRONMENT"] : []),
|
|
393
|
+
...(!backendServiceKey ? ["backend-service-key"] : []),
|
|
394
|
+
],
|
|
395
|
+
url: backendIngestUrl,
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
} else {
|
|
399
|
+
const result = await postJson({
|
|
400
|
+
fetchImpl,
|
|
401
|
+
url: backendIngestUrl,
|
|
402
|
+
headers: {
|
|
403
|
+
"x-clue-project-key": projectKey,
|
|
404
|
+
"x-clue-api-key": apiKey,
|
|
405
|
+
},
|
|
406
|
+
body: buildBackendEventPayload({
|
|
407
|
+
apiKey,
|
|
408
|
+
backendServiceKey,
|
|
409
|
+
environment,
|
|
410
|
+
projectKey,
|
|
411
|
+
}),
|
|
412
|
+
});
|
|
413
|
+
checks.push(
|
|
414
|
+
buildCheck({
|
|
415
|
+
id: "backend_ingest",
|
|
416
|
+
passed: Boolean(result.transportOk && result.response.ok),
|
|
417
|
+
result,
|
|
418
|
+
url: backendIngestUrl,
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const passed = checks.every((check) => check.passed);
|
|
424
|
+
return {
|
|
425
|
+
status: passed ? "passed" : "failed",
|
|
426
|
+
passed,
|
|
427
|
+
contract: API_CONNECTIVITY_CONTRACT,
|
|
428
|
+
checks,
|
|
429
|
+
inputs: {
|
|
430
|
+
manifest_loaded: Boolean(manifest),
|
|
431
|
+
client_backend_url_configured: Boolean(clientBackendUrl),
|
|
432
|
+
client_frontend_url_configured: Boolean(clientFrontendUrl),
|
|
433
|
+
browser_token_proxy_url_configured: Boolean(browserTokenProxyUrl),
|
|
434
|
+
clue_api_base_url_configured: Boolean(clueApiBaseUrl),
|
|
435
|
+
project_key_configured: Boolean(projectKey),
|
|
436
|
+
environment_configured: Boolean(environment),
|
|
437
|
+
service_key_configured: Boolean(serviceKey),
|
|
438
|
+
backend_service_key_configured: Boolean(backendServiceKey),
|
|
439
|
+
clue_api_key_configured: Boolean(apiKey),
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
};
|
package/src/setup-help.mjs
CHANGED
|
@@ -2,9 +2,16 @@ import {
|
|
|
2
2
|
CLUE_CLI_INVOCATION_CONTRACT,
|
|
3
3
|
clueCliCommand,
|
|
4
4
|
} from "./cli-invocation.mjs";
|
|
5
|
+
import {
|
|
6
|
+
AI_SETUP_CONTRACT_VERSION,
|
|
7
|
+
API_CONNECTIVITY_CONTRACT,
|
|
8
|
+
DETERMINISTIC_CONTROL_MODEL,
|
|
9
|
+
FRONTEND_ADAPTER_CONTRACT,
|
|
10
|
+
SETUP_DOCTRINE,
|
|
11
|
+
} from "./setup-ai-contract.mjs";
|
|
5
12
|
import { buildSetupDocumentationContract } from "./setup-documents.mjs";
|
|
6
13
|
|
|
7
|
-
export const AI_SETUP_HELP_VERSION =
|
|
14
|
+
export const AI_SETUP_HELP_VERSION = AI_SETUP_CONTRACT_VERSION;
|
|
8
15
|
|
|
9
16
|
export const buildAiSetupHelp = () => ({
|
|
10
17
|
name: "@clue-ai/cli AI setup help",
|
|
@@ -13,6 +20,10 @@ export const buildAiSetupHelp = () => ({
|
|
|
13
20
|
"Machine-readable Clue setup contract for AI coding agents. Use this before editing a customer repository for Clue setup.",
|
|
14
21
|
cli_invocation: CLUE_CLI_INVOCATION_CONTRACT,
|
|
15
22
|
setup_execution_contract: {
|
|
23
|
+
setup_doctrine: SETUP_DOCTRINE,
|
|
24
|
+
deterministic_control_model: DETERMINISTIC_CONTROL_MODEL,
|
|
25
|
+
api_connectivity_contract: API_CONNECTIVITY_CONTRACT,
|
|
26
|
+
frontend_adapter_contract: FRONTEND_ADAPTER_CONTRACT,
|
|
16
27
|
agent_primary_task:
|
|
17
28
|
"Decide where to place ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout in existing repository lifecycle boundaries, then apply only those minimal Clue SDK wiring changes.",
|
|
18
29
|
implementation_workstreams: ["sdk_lifecycle_placement"],
|
|
@@ -51,12 +62,15 @@ export const buildAiSetupHelp = () => ({
|
|
|
51
62
|
"NEXT_PUBLIC_CLUE_ENVIRONMENT",
|
|
52
63
|
"NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
53
64
|
"NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
65
|
+
"NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
54
66
|
],
|
|
55
|
-
rule: "When the frontend framework is Next.js, browser/client code must read only these NEXT_PUBLIC_* Clue variables. Do not add process.env.CLUE_* fallbacks in client-bundled code.",
|
|
67
|
+
rule: "When the frontend framework is Next.js, browser/client code must read only these NEXT_PUBLIC_* Clue variables. Do not add process.env.CLUE_* fallbacks in client-bundled code. browserTokenProvider must call NEXT_PUBLIC_CLUE_BROWSER_TOKEN_ENDPOINT, not a relative frontend-origin path or generic NEXT_PUBLIC_API_URL.",
|
|
56
68
|
},
|
|
57
69
|
backend_browser_token_proxy_env: {
|
|
58
70
|
variables: ["CLUE_API_KEY", "CLUE_API_BASE_URL"],
|
|
59
71
|
rule: "CLUE_API_KEY stays server-side. CLUE_API_BASE_URL is used only by backend-owned browser token proxy code and is not part of backend SDK initialization. The proxy endpoint belongs to the customer backend, but it calls the Clue API server-side at /api/v1/ingest/browser-tokens. The frontend browserTokenProvider must send the frontend ClueInit serviceKey to that customer backend proxy, and the proxy must issue browser tokens for that frontend serviceKey, not the backend service key.",
|
|
72
|
+
origin_rule:
|
|
73
|
+
"The backend proxy must derive request origin from trusted request headers or server request metadata. Project key and environment must come from server configuration or be validated against it. Do not forward origin, projectKey, or environment from JSON/body payload fields under server CLUE_API_KEY.",
|
|
60
74
|
},
|
|
61
75
|
},
|
|
62
76
|
documentation_contract: buildSetupDocumentationContract(),
|
|
@@ -68,6 +82,13 @@ export const buildAiSetupHelp = () => ({
|
|
|
68
82
|
ai_agent_responsibility:
|
|
69
83
|
"Report the command and required user verification as pending when it was not run by the user.",
|
|
70
84
|
},
|
|
85
|
+
setup_doctor: {
|
|
86
|
+
owner: "ai_or_user",
|
|
87
|
+
ai_agent_may_run: true,
|
|
88
|
+
command: clueCliCommand("setup-doctor --local"),
|
|
89
|
+
rule: "Run setup-doctor after local frontend/backend services and required env are available. It checks API connectivity only and does not replace user-operated setup-watch.",
|
|
90
|
+
checked_hops: Object.keys(API_CONNECTIVITY_CONTRACT.hops),
|
|
91
|
+
},
|
|
71
92
|
completion_boundary: {
|
|
72
93
|
ai_may_claim: [
|
|
73
94
|
"Clue setup code changes were applied",
|
package/src/setup-prepare.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
CLUE_CLI_INVOCATION_CONTRACT,
|
|
9
9
|
clueCliCommand,
|
|
10
10
|
} from "./cli-invocation.mjs";
|
|
11
|
+
import { API_CONNECTIVITY_CONTRACT } from "./setup-ai-contract.mjs";
|
|
11
12
|
import { buildSetupDocumentationContract } from "./setup-documents.mjs";
|
|
12
13
|
import { runSetupDetect } from "./setup-detect.mjs";
|
|
13
14
|
|
|
@@ -15,11 +16,14 @@ const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
|
15
16
|
const DEFAULT_ENV_GUIDE_PATH = ".env.clue";
|
|
16
17
|
const BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
|
|
17
18
|
const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
|
|
19
|
+
const BROWSER_TOKEN_PROXY_PATH =
|
|
20
|
+
API_CONNECTIVITY_CONTRACT.hops.client_backend_browser_token_proxy.path;
|
|
18
21
|
const FRONTEND_PUBLIC_ENV_NAMES = [
|
|
19
22
|
"CLUE_INGEST_ENDPOINT",
|
|
20
23
|
"CLUE_PROJECT_KEY",
|
|
21
24
|
"CLUE_ENVIRONMENT",
|
|
22
25
|
"CLUE_SERVICE_KEY",
|
|
26
|
+
"CLUE_BROWSER_TOKEN_ENDPOINT",
|
|
23
27
|
];
|
|
24
28
|
const BACKEND_RUNTIME_ENV_NAMES = [
|
|
25
29
|
"CLUE_SERVICE_KEY",
|
|
@@ -186,6 +190,7 @@ const frontendEnvName = ({ target, name }) =>
|
|
|
186
190
|
: name;
|
|
187
191
|
|
|
188
192
|
const buildServiceEnvBlock = ({
|
|
193
|
+
browserTokenEndpoint,
|
|
189
194
|
target,
|
|
190
195
|
setupContext,
|
|
191
196
|
includeBrowserTokenProxyConfig = false,
|
|
@@ -210,6 +215,12 @@ const buildServiceEnvBlock = ({
|
|
|
210
215
|
value: target.service_key,
|
|
211
216
|
},
|
|
212
217
|
];
|
|
218
|
+
if (target.kind === "frontend") {
|
|
219
|
+
variables.push({
|
|
220
|
+
name: frontendEnvName({ target, name: "CLUE_BROWSER_TOKEN_ENDPOINT" }),
|
|
221
|
+
value: browserTokenEndpoint,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
213
224
|
if (target.kind === "backend") {
|
|
214
225
|
variables.push({ name: "CLUE_API_KEY", value: setupContext.clue_api_key });
|
|
215
226
|
if (includeBrowserTokenProxyConfig) {
|
|
@@ -265,6 +276,14 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
|
|
|
265
276
|
const includeBrowserTokenProxyConfig = watchTargets.some(
|
|
266
277
|
(target) => target.kind === "frontend",
|
|
267
278
|
);
|
|
279
|
+
const backendTarget = watchTargets.find((target) => target.kind === "backend");
|
|
280
|
+
const backendUrlCandidate =
|
|
281
|
+
optionalString(backendTarget?.local_url_candidates?.[0]) ??
|
|
282
|
+
"http://<client-backend-url>";
|
|
283
|
+
const browserTokenEndpoint = buildEndpoint(
|
|
284
|
+
backendUrlCandidate,
|
|
285
|
+
BROWSER_TOKEN_PROXY_PATH,
|
|
286
|
+
);
|
|
268
287
|
return {
|
|
269
288
|
status: "ready",
|
|
270
289
|
env_file_path: DEFAULT_ENV_GUIDE_PATH,
|
|
@@ -276,6 +295,7 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
|
|
|
276
295
|
service_key: target.service_key,
|
|
277
296
|
env_file_candidates: envFileCandidates(target),
|
|
278
297
|
env_block: buildServiceEnvBlock({
|
|
298
|
+
browserTokenEndpoint,
|
|
279
299
|
target,
|
|
280
300
|
setupContext,
|
|
281
301
|
includeBrowserTokenProxyConfig,
|
|
@@ -488,6 +508,7 @@ export const runSetupPrepare = async ({
|
|
|
488
508
|
project_key: setupContext.project_key,
|
|
489
509
|
environment: setupContext.environment,
|
|
490
510
|
clue_api_base_url: setupContext.clue_api_base_url,
|
|
511
|
+
api_connectivity_contract: API_CONNECTIVITY_CONTRACT,
|
|
491
512
|
ingest_endpoints: setupContext.clue_api_base_url
|
|
492
513
|
? {
|
|
493
514
|
browser: buildEndpoint(
|
|
@@ -557,6 +578,12 @@ export const runSetupPrepare = async ({
|
|
|
557
578
|
completion_meaning:
|
|
558
579
|
"required before setup-watch local completion can be trusted",
|
|
559
580
|
},
|
|
581
|
+
{
|
|
582
|
+
id: "local_api_connectivity_preflight",
|
|
583
|
+
command: clueCliCommand("setup-doctor --local"),
|
|
584
|
+
completion_meaning:
|
|
585
|
+
"required before user setup-watch; verifies customer proxy, Clue browser-token issuance, browser ingest, and backend ingest connectivity when local services and required env are available",
|
|
586
|
+
},
|
|
560
587
|
{
|
|
561
588
|
id: "local_event_delivery",
|
|
562
589
|
command: `user runs ${clueCliCommand("setup-watch --local")}`,
|