@clue-ai/cli 0.0.13 → 0.0.15
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 +12 -4
- package/bin/clue-cli.mjs +133 -36
- package/package.json +1 -1
- package/src/lifecycle-init.mjs +16 -5
- package/src/public-schema.cjs +15 -0
- package/src/semantic-ci.mjs +128 -17
- package/src/setup-check.mjs +227 -3
- package/src/setup-documents.mjs +99 -0
- package/src/setup-help.mjs +18 -1
- package/src/setup-prepare.mjs +117 -24
- package/src/setup-tool.mjs +75 -7
package/README.md
CHANGED
|
@@ -62,13 +62,21 @@ leaks, and SDK lifecycle presence when requested. With
|
|
|
62
62
|
installation, SDK imports in the target environments, app startup, and event
|
|
63
63
|
delivery remain required before setup can be called complete.
|
|
64
64
|
|
|
65
|
-
`npx -y @clue-ai/cli setup-watch` polls the Clue API setup-check endpoint
|
|
66
|
-
the
|
|
67
|
-
frontend URL. Use `--watch-targets` to list every
|
|
68
|
-
service key and the lifecycle checks expected for
|
|
65
|
+
`npx -y @clue-ai/cli setup-watch` polls the Clue API setup-check endpoint in
|
|
66
|
+
remote mode while you operate the service. `--clue-api-base-url` is the Clue API
|
|
67
|
+
URL, not the customer frontend URL. Use `--watch-targets` to list every
|
|
68
|
+
frontend/backend producer by service key and the lifecycle checks expected for
|
|
69
|
+
that service, for example
|
|
69
70
|
`frontend:web[init,identify,set-account,event-sent]=<frontend-url>,backend:api[init,identify,set-account,logout,event-sent]=<backend-url>`.
|
|
70
71
|
Do not assume localhost ports; use the actual URLs printed by the repository's
|
|
71
72
|
dev scripts or configured in its local env.
|
|
73
|
+
In `--local` mode, the watcher stays open until the expected lifecycle checks
|
|
74
|
+
pass or you stop it with Ctrl+C. The frontend/backend endpoint URLs printed by
|
|
75
|
+
the command are local Clue receiver endpoints; configure the customer's Clue SDK
|
|
76
|
+
ingest endpoint to those receiver URLs while testing. Local mode does not ask
|
|
77
|
+
for or health-check customer service URLs. It prints a per-service Clue
|
|
78
|
+
lifecycle checklist and only prints a new snapshot when the observed state
|
|
79
|
+
changes.
|
|
72
80
|
|
|
73
81
|
`npx -y @clue-ai/cli setup` reads Clue values from the setup screen flags, detects local
|
|
74
82
|
services, writes `.clue/setup-manifest.json`, and prints service-specific env
|
package/bin/clue-cli.mjs
CHANGED
|
@@ -135,6 +135,15 @@ const DEFAULT_WATCH_LIFECYCLE = [
|
|
|
135
135
|
"event-sent",
|
|
136
136
|
];
|
|
137
137
|
const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
138
|
+
const DEFAULT_REMOTE_SETUP_WATCH_TIMEOUT_MS = 120_000;
|
|
139
|
+
|
|
140
|
+
const WATCH_LIFECYCLE_LABELS = new Map([
|
|
141
|
+
["init", "ClueInit"],
|
|
142
|
+
["identify", "ClueIdentify"],
|
|
143
|
+
["set-account", "ClueSetAccount"],
|
|
144
|
+
["logout", "ClueLogout"],
|
|
145
|
+
["event-sent", "Event delivery"],
|
|
146
|
+
]);
|
|
138
147
|
|
|
139
148
|
const parseExpectedLifecycle = (value) => {
|
|
140
149
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -295,7 +304,18 @@ const maybeProtectEnvironmentGuide = async ({
|
|
|
295
304
|
};
|
|
296
305
|
};
|
|
297
306
|
|
|
298
|
-
const
|
|
307
|
+
const clearWatchTargetUrls = (watchTargets) =>
|
|
308
|
+
watchTargets.map((target) => ({
|
|
309
|
+
...target,
|
|
310
|
+
url: null,
|
|
311
|
+
urlEnvName: null,
|
|
312
|
+
localUrlCandidates: [],
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
const confirmTargetUrls = async ({ flags, localMode, watchTargets, env }) => {
|
|
316
|
+
if (localMode) {
|
|
317
|
+
return clearWatchTargetUrls(watchTargets);
|
|
318
|
+
}
|
|
299
319
|
const initialTargets = watchTargets.map((target) => ({
|
|
300
320
|
...target,
|
|
301
321
|
url: resolveTargetUrlFromEnv({ target, env }),
|
|
@@ -333,6 +353,20 @@ const buildWatchProducerIds = ({ explicitProducerIds, watchTargets }) =>
|
|
|
333
353
|
...watchTargets.map((target) => target.producerId),
|
|
334
354
|
]);
|
|
335
355
|
|
|
356
|
+
const parseSetupWatchTimeoutMs = ({ flags, localMode }) => {
|
|
357
|
+
if (!flags.has("timeout-ms")) {
|
|
358
|
+
return localMode ? null : DEFAULT_REMOTE_SETUP_WATCH_TIMEOUT_MS;
|
|
359
|
+
}
|
|
360
|
+
const timeoutMs = Number(flags.get("timeout-ms"));
|
|
361
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
362
|
+
throw new Error("--timeout-ms must be a non-negative number");
|
|
363
|
+
}
|
|
364
|
+
return timeoutMs;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const isSetupWatchTimedOut = ({ started, timeoutMs }) =>
|
|
368
|
+
timeoutMs !== null && Date.now() - started > timeoutMs;
|
|
369
|
+
|
|
336
370
|
const checkTargetUrl = async (url) => {
|
|
337
371
|
if (!url) return { checked: true, reachable: false, status: null };
|
|
338
372
|
try {
|
|
@@ -347,14 +381,20 @@ const checkTargetUrl = async (url) => {
|
|
|
347
381
|
}
|
|
348
382
|
};
|
|
349
383
|
|
|
350
|
-
const evaluateWatchTargets = async ({
|
|
384
|
+
const evaluateWatchTargets = async ({
|
|
385
|
+
latest,
|
|
386
|
+
requireTargetUrl = true,
|
|
387
|
+
watchTargets,
|
|
388
|
+
}) => {
|
|
351
389
|
const producers = Array.isArray(latest?.producers) ? latest.producers : [];
|
|
352
390
|
return Promise.all(
|
|
353
391
|
watchTargets.map(async (target) => {
|
|
354
392
|
const producer = producers.find(
|
|
355
393
|
(entry) => entry.id === target.producerId,
|
|
356
394
|
);
|
|
357
|
-
const urlHealth =
|
|
395
|
+
const urlHealth = requireTargetUrl
|
|
396
|
+
? await checkTargetUrl(target.url)
|
|
397
|
+
: { checked: false, reachable: true, status: null };
|
|
358
398
|
const lifecycleStatus = {
|
|
359
399
|
init: Boolean(producer?.clueInit ?? producer?.sdkInitialized),
|
|
360
400
|
identify: Boolean(producer?.clueIdentify),
|
|
@@ -388,7 +428,7 @@ const evaluateWatchTargets = async ({ latest, watchTargets }) => {
|
|
|
388
428
|
urlChecked: urlHealth.checked,
|
|
389
429
|
urlReachable: urlHealth.reachable,
|
|
390
430
|
urlStatus: urlHealth.status,
|
|
391
|
-
passed: producerPassed && urlHealth.reachable,
|
|
431
|
+
passed: producerPassed && (!requireTargetUrl || urlHealth.reachable),
|
|
392
432
|
};
|
|
393
433
|
}),
|
|
394
434
|
);
|
|
@@ -543,25 +583,60 @@ const startLocalSetupReceiver = async ({ host, port }) => {
|
|
|
543
583
|
};
|
|
544
584
|
};
|
|
545
585
|
|
|
586
|
+
const lifecycleLabel = (name) => WATCH_LIFECYCLE_LABELS.get(name) ?? name;
|
|
587
|
+
|
|
588
|
+
const statusMark = (passed) => (passed ? "x" : " ");
|
|
589
|
+
|
|
590
|
+
const lifecycleStatusText = (passed) => (passed ? "done!" : "pending...");
|
|
591
|
+
|
|
592
|
+
const targetUrlStatusText = (target) => {
|
|
593
|
+
if (!target.urlChecked) return null;
|
|
594
|
+
return String(
|
|
595
|
+
target.urlReachable
|
|
596
|
+
? target.urlStatus
|
|
597
|
+
: (target.urlStatus ?? "unreachable"),
|
|
598
|
+
);
|
|
599
|
+
};
|
|
600
|
+
|
|
546
601
|
const renderWatchTargets = (targetChecks) =>
|
|
547
602
|
targetChecks
|
|
548
603
|
.map((target) => {
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
604
|
+
const lifecycleLines = target.expectedLifecycle.map((name) => {
|
|
605
|
+
const passed = Boolean(target.lifecycleStatus[name]);
|
|
606
|
+
return ` [${statusMark(passed)}] ${lifecycleLabel(name)} ${lifecycleStatusText(passed)}`;
|
|
607
|
+
});
|
|
608
|
+
const urlStatus = targetUrlStatusText(target);
|
|
609
|
+
const detailLines = [` events: ${target.eventCount}`];
|
|
610
|
+
if (urlStatus !== null) {
|
|
611
|
+
detailLines.push(` url: ${urlStatus}`);
|
|
612
|
+
}
|
|
613
|
+
if (target.unexpectedLifecycle.length) {
|
|
614
|
+
detailLines.push(
|
|
615
|
+
` unexpected: ${target.unexpectedLifecycle.map(lifecycleLabel).join(", ")}`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return [
|
|
619
|
+
`[${statusMark(target.passed)}] ${target.kind}:${target.serviceKey}`,
|
|
620
|
+
...lifecycleLines,
|
|
621
|
+
...detailLines,
|
|
622
|
+
].join("\n");
|
|
562
623
|
})
|
|
624
|
+
.join("\n\n");
|
|
625
|
+
|
|
626
|
+
const renderSetupCheckEntries = (entries) =>
|
|
627
|
+
entries
|
|
628
|
+
.map(([id, status]) => `${status === "passed" ? "[x]" : "[ ]"} ${id}`)
|
|
563
629
|
.join("\n");
|
|
564
630
|
|
|
631
|
+
const joinRenderedSections = (...sections) =>
|
|
632
|
+
sections.filter((section) => section.trim()).join("\n\n");
|
|
633
|
+
|
|
634
|
+
const writeChangedSetupWatchSnapshot = ({ rendered, state }) => {
|
|
635
|
+
if (!rendered || rendered === state.lastRendered) return;
|
|
636
|
+
process.stdout.write(`${rendered}\n\n`);
|
|
637
|
+
state.lastRendered = rendered;
|
|
638
|
+
};
|
|
639
|
+
|
|
565
640
|
const setupCheckUrl = ({
|
|
566
641
|
clueApiBaseUrl,
|
|
567
642
|
environment,
|
|
@@ -621,13 +696,14 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
621
696
|
const startedAt = String(
|
|
622
697
|
flags.get("started-at") || new Date().toISOString(),
|
|
623
698
|
).trim();
|
|
624
|
-
const timeoutMs =
|
|
699
|
+
const timeoutMs = parseSetupWatchTimeoutMs({ flags, localMode });
|
|
625
700
|
const pollIntervalMs = Number(flags.get("poll-interval-ms") || 3000);
|
|
626
701
|
const limit = Number(flags.get("limit") || 200);
|
|
627
702
|
const projectId = flags.get("project-id");
|
|
628
703
|
const explicitWatchTargets = parseWatchTargets(flags.get("watch-targets"));
|
|
629
704
|
const watchTargets = await confirmTargetUrls({
|
|
630
705
|
flags,
|
|
706
|
+
localMode,
|
|
631
707
|
watchTargets:
|
|
632
708
|
explicitWatchTargets.length > 0
|
|
633
709
|
? explicitWatchTargets
|
|
@@ -639,6 +715,11 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
639
715
|
"setup-watch requires --watch-targets or .clue/setup-manifest.json lifecycle_verification.watch_targets before setup can be treated as verified",
|
|
640
716
|
);
|
|
641
717
|
}
|
|
718
|
+
if (localMode && watchTargets.length === 0) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
"setup-watch --local requires .clue/setup-manifest.json lifecycle_verification.watch_targets",
|
|
721
|
+
);
|
|
722
|
+
}
|
|
642
723
|
const producerIds = buildWatchProducerIds({
|
|
643
724
|
explicitProducerIds: flags.get("producer-ids"),
|
|
644
725
|
watchTargets,
|
|
@@ -664,26 +745,38 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
664
745
|
`Watching targets: ${watchTargets.map((target) => `${target.kind}:${target.serviceKey}`).join(", ")}\n`,
|
|
665
746
|
);
|
|
666
747
|
}
|
|
667
|
-
|
|
748
|
+
process.stdout.write(
|
|
749
|
+
timeoutMs === null
|
|
750
|
+
? "No automatic timeout in local mode. Operate your local app, then press Ctrl+C to stop if needed.\n"
|
|
751
|
+
: `Timeout: ${timeoutMs}ms\n`,
|
|
752
|
+
);
|
|
753
|
+
const renderState = { lastRendered: "" };
|
|
754
|
+
while (!isSetupWatchTimedOut({ started, timeoutMs })) {
|
|
668
755
|
latest = localSetupCheckSnapshot({
|
|
669
756
|
receivedBatches: receiver.receivedBatches,
|
|
670
757
|
});
|
|
671
758
|
const targetChecks = await evaluateWatchTargets({
|
|
672
759
|
latest,
|
|
760
|
+
requireTargetUrl: false,
|
|
673
761
|
watchTargets,
|
|
674
762
|
});
|
|
675
763
|
const targetChecksPassed =
|
|
676
764
|
targetChecks.length > 0 &&
|
|
677
765
|
targetChecks.every((target) => target.passed);
|
|
678
766
|
const renderedTargets = renderWatchTargets(targetChecks);
|
|
679
|
-
|
|
767
|
+
writeChangedSetupWatchSnapshot({
|
|
768
|
+
rendered: renderedTargets,
|
|
769
|
+
state: renderState,
|
|
770
|
+
});
|
|
680
771
|
if (targetChecksPassed) {
|
|
681
772
|
process.stdout.write("Clue local setup checks passed.\n");
|
|
682
773
|
return { ...latest, local: true, watchTargets: targetChecks };
|
|
683
774
|
}
|
|
684
775
|
await sleep(pollIntervalMs);
|
|
685
776
|
}
|
|
686
|
-
|
|
777
|
+
if (flags.has("json")) {
|
|
778
|
+
process.stdout.write(`${JSON.stringify(latest, null, 2)}\n`);
|
|
779
|
+
}
|
|
687
780
|
throw new Error(
|
|
688
781
|
"setup-watch --local timed out before all Clue setup checks passed",
|
|
689
782
|
);
|
|
@@ -701,7 +794,8 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
701
794
|
);
|
|
702
795
|
}
|
|
703
796
|
|
|
704
|
-
|
|
797
|
+
const renderState = { lastRendered: "" };
|
|
798
|
+
while (!isSetupWatchTimedOut({ started, timeoutMs })) {
|
|
705
799
|
const response = await fetch(
|
|
706
800
|
setupCheckUrl({
|
|
707
801
|
clueApiBaseUrl,
|
|
@@ -724,13 +818,12 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
724
818
|
const targetChecks = await evaluateWatchTargets({ latest, watchTargets });
|
|
725
819
|
const targetChecksPassed = targetChecks.every((target) => target.passed);
|
|
726
820
|
const passed = setupChecksPassed && targetChecksPassed;
|
|
727
|
-
const rendered = entries
|
|
728
|
-
.map(([id, status]) => `${status === "passed" ? "[x]" : "[ ]"} ${id}`)
|
|
729
|
-
.join("\n");
|
|
821
|
+
const rendered = renderSetupCheckEntries(entries);
|
|
730
822
|
const renderedTargets = renderWatchTargets(targetChecks);
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
823
|
+
writeChangedSetupWatchSnapshot({
|
|
824
|
+
rendered: joinRenderedSections(rendered, renderedTargets),
|
|
825
|
+
state: renderState,
|
|
826
|
+
});
|
|
734
827
|
if (passed) {
|
|
735
828
|
process.stdout.write("Clue setup checks passed.\n");
|
|
736
829
|
return { ...latest, watchTargets: targetChecks };
|
|
@@ -738,7 +831,9 @@ const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
|
738
831
|
await sleep(pollIntervalMs);
|
|
739
832
|
}
|
|
740
833
|
|
|
741
|
-
|
|
834
|
+
if (flags.has("json")) {
|
|
835
|
+
process.stdout.write(`${JSON.stringify(latest, null, 2)}\n`);
|
|
836
|
+
}
|
|
742
837
|
throw new Error("setup-watch timed out before all Clue setup checks passed");
|
|
743
838
|
};
|
|
744
839
|
|
|
@@ -753,7 +848,7 @@ const usage = () =>
|
|
|
753
848
|
"",
|
|
754
849
|
"Usage:",
|
|
755
850
|
" /clue-init",
|
|
756
|
-
` ${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment dev")}`,
|
|
851
|
+
` ${clueCliCommand("setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment dev --documents-url <url>")}`,
|
|
757
852
|
` ${clueCliCommand("setup-detect --repo .")}`,
|
|
758
853
|
` ${clueCliCommand("semantic-inventory --framework fastapi --backend-root-path backend --repo .")}`,
|
|
759
854
|
` ${clueCliCommand("semantic-agent-skills --output .clue/semantic-agent-skills.json")}`,
|
|
@@ -827,6 +922,7 @@ const main = async () => {
|
|
|
827
922
|
if (command === "setup") {
|
|
828
923
|
const report = await installSetupSkills({
|
|
829
924
|
repoRoot,
|
|
925
|
+
documentsUrl: flags.get("documents-url"),
|
|
830
926
|
target:
|
|
831
927
|
typeof flags.get("target") === "string"
|
|
832
928
|
? flags.get("target")
|
|
@@ -848,12 +944,13 @@ const main = async () => {
|
|
|
848
944
|
repoRoot,
|
|
849
945
|
target: report.target,
|
|
850
946
|
skillRoot: report.skill_root,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
947
|
+
setupContext: {
|
|
948
|
+
clueApiKey: flags.get("clue-api-key"),
|
|
949
|
+
clueApiBaseUrl: flags.get("clue-api-base-url"),
|
|
950
|
+
documentsUrl: flags.get("documents-url"),
|
|
951
|
+
projectKey: flags.get("project-key"),
|
|
952
|
+
environment: flags.get("environment"),
|
|
953
|
+
},
|
|
857
954
|
});
|
|
858
955
|
const environmentFileProtection =
|
|
859
956
|
preEnvironmentFileProtection?.env_file_path ===
|
package/package.json
CHANGED
package/src/lifecycle-init.mjs
CHANGED
|
@@ -568,12 +568,19 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
568
568
|
"Prefer stable ids and non-PII booleans/counts for ClueIdentify and ClueSetAccount traits.",
|
|
569
569
|
"Use environment variable names for Clue configuration values.",
|
|
570
570
|
"For Python/FastAPI code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_API_KEY, and CLUE_INGEST_ENDPOINT from the backend env block.",
|
|
571
|
-
"CLUE_API_BASE_URL is
|
|
572
|
-
"For browser code, read
|
|
571
|
+
"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.",
|
|
572
|
+
"For Next.js browser/client code, read NEXT_PUBLIC_CLUE_PROJECT_KEY, NEXT_PUBLIC_CLUE_ENVIRONMENT, NEXT_PUBLIC_CLUE_SERVICE_KEY, and NEXT_PUBLIC_CLUE_INGEST_ENDPOINT from the frontend .env.local block.",
|
|
573
|
+
"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.",
|
|
574
|
+
"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.",
|
|
573
575
|
"Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
574
576
|
"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.",
|
|
575
577
|
"Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
|
|
578
|
+
"The local backend token endpoint is part of the customer app, not the Clue API. It may use the customer's route convention, but it must call Clue server-side at /api/v1/ingest/browser-tokens.",
|
|
579
|
+
"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.",
|
|
576
580
|
"The browser token request must include project key, environment, service key, and the current browser origin; the backend must attach x-clue-api-key server-side when calling Clue.",
|
|
581
|
+
"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.",
|
|
582
|
+
"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.",
|
|
583
|
+
"Do not add @clue-ai/browser-sdk or backend SDK dependencies with * or latest; use a concrete published version or package-manager-resolved semver range and update the repository lockfile when one exists.",
|
|
577
584
|
"Prefer minimal edits that engineers can review in one PR.",
|
|
578
585
|
"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.",
|
|
579
586
|
"If a lifecycle point is unclear, skip that edit and include a warning.",
|
|
@@ -584,13 +591,17 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
584
591
|
target_tool: request.target_tool,
|
|
585
592
|
framework: request.framework,
|
|
586
593
|
project_key_env: "CLUE_PROJECT_KEY",
|
|
587
|
-
browser_project_key_env: "
|
|
594
|
+
browser_project_key_env: "framework_specific",
|
|
595
|
+
nextjs_browser_project_key_env: "NEXT_PUBLIC_CLUE_PROJECT_KEY",
|
|
588
596
|
environment_env: "CLUE_ENVIRONMENT",
|
|
589
|
-
browser_environment_env: "
|
|
597
|
+
browser_environment_env: "framework_specific",
|
|
598
|
+
nextjs_browser_environment_env: "NEXT_PUBLIC_CLUE_ENVIRONMENT",
|
|
590
599
|
clue_ingest_endpoint_env: "CLUE_INGEST_ENDPOINT",
|
|
591
600
|
tooling_api_base_url_env: "CLUE_API_BASE_URL",
|
|
592
|
-
browser_ingest_endpoint_env: "
|
|
601
|
+
browser_ingest_endpoint_env: "framework_specific",
|
|
602
|
+
nextjs_browser_ingest_endpoint_env: "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
|
|
593
603
|
browser_token_endpoint_path: "/api/v1/ingest/browser-tokens",
|
|
604
|
+
nextjs_browser_service_key_env: "NEXT_PUBLIC_CLUE_SERVICE_KEY",
|
|
594
605
|
service_key: request.service_key,
|
|
595
606
|
},
|
|
596
607
|
output_shape: {
|
package/src/public-schema.cjs
CHANGED
|
@@ -845,9 +845,23 @@ const semanticSnapshotRequestSchema = zod_1.z
|
|
|
845
845
|
], "semantic_meaning_candidates[].selection_ai_inference_evidence_ref");
|
|
846
846
|
});
|
|
847
847
|
});
|
|
848
|
+
const semanticSnapshotResponseSchema = zod_1.z.object({
|
|
849
|
+
accepted: zod_1.z.literal(true),
|
|
850
|
+
duplicate: zod_1.z.boolean(),
|
|
851
|
+
semantic_snapshot_id: nonEmptyStringSchema,
|
|
852
|
+
route_count: zod_1.z.number().int().nonnegative(),
|
|
853
|
+
diff_summary: zod_1.z.object({
|
|
854
|
+
added_routes: zod_1.z.number().int().nonnegative(),
|
|
855
|
+
removed_routes: zod_1.z.number().int().nonnegative(),
|
|
856
|
+
changed_routes: zod_1.z.number().int().nonnegative(),
|
|
857
|
+
unchanged_routes: zod_1.z.number().int().nonnegative(),
|
|
858
|
+
low_confidence_routes: zod_1.z.number().int().nonnegative(),
|
|
859
|
+
}),
|
|
860
|
+
});
|
|
848
861
|
|
|
849
862
|
return {
|
|
850
863
|
semanticSnapshotRequestSchema,
|
|
864
|
+
semanticSnapshotResponseSchema,
|
|
851
865
|
};
|
|
852
866
|
})();
|
|
853
867
|
|
|
@@ -855,6 +869,7 @@ const schemaPackage = {
|
|
|
855
869
|
clueInitToolRequestSchema: toolingSchemas.clueInitToolRequestSchema,
|
|
856
870
|
clueInitToolReportSchema: toolingSchemas.clueInitToolReportSchema,
|
|
857
871
|
semanticSnapshotRequestSchema: semanticSchemas.semanticSnapshotRequestSchema,
|
|
872
|
+
semanticSnapshotResponseSchema: semanticSchemas.semanticSnapshotResponseSchema,
|
|
858
873
|
};
|
|
859
874
|
|
|
860
875
|
module.exports = schemaPackage;
|
package/src/semantic-ci.mjs
CHANGED
|
@@ -706,7 +706,7 @@ const FORBIDDEN_TARGET_TEXT_PATTERNS = [
|
|
|
706
706
|
/\b[A-Za-z0-9_./-]+\.py\b/i,
|
|
707
707
|
/\b(from\s+[A-Za-z_][A-Za-z0-9_.]*\s+import|import\s+[A-Za-z_][A-Za-z0-9_.]*)\b/i,
|
|
708
708
|
/\b(class|def)\s+[A-Za-z_][A-Za-z0-9_]*/,
|
|
709
|
-
/\b(select|insert|update|delete
|
|
709
|
+
/\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b/i,
|
|
710
710
|
/\b(system|user|assistant)\s*:\s*.+\b(prompt|completion|transcript)\b/i,
|
|
711
711
|
/\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\b(?:\s*=\s*["']?[^"'\s,;}]+)?/,
|
|
712
712
|
/\b[A-Z][A-Z0-9_]{2,}\s*=\s*["']?[^"'\s,;}]+/,
|
|
@@ -1358,6 +1358,15 @@ const unresolvedEffect = ({
|
|
|
1358
1358
|
.map((entry) => sanitizeText(entry.trim(), route)),
|
|
1359
1359
|
});
|
|
1360
1360
|
|
|
1361
|
+
const findCatalogEntryByTargetObjectKey = (catalogByGroup, targetObjectKey) => {
|
|
1362
|
+
for (const [mapKey, entry] of catalogByGroup.entries()) {
|
|
1363
|
+
if (entry?.target_object_key === targetObjectKey) {
|
|
1364
|
+
return { mapKey, entry };
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return null;
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1361
1370
|
const buildOperationAssignmentCollections = ({
|
|
1362
1371
|
route,
|
|
1363
1372
|
aiRoute,
|
|
@@ -1535,7 +1544,15 @@ const buildOperationAssignmentCollections = ({
|
|
|
1535
1544
|
}
|
|
1536
1545
|
|
|
1537
1546
|
const groupKey = `${concept.toLowerCase()}::${businessRole.toLowerCase()}`;
|
|
1538
|
-
const
|
|
1547
|
+
const existingCatalogByTarget = findCatalogEntryByTargetObjectKey(
|
|
1548
|
+
catalogByGroup,
|
|
1549
|
+
targetKeyCandidate,
|
|
1550
|
+
);
|
|
1551
|
+
const existingCatalog =
|
|
1552
|
+
catalogByGroup.get(groupKey) ?? existingCatalogByTarget?.entry;
|
|
1553
|
+
const catalogMapKey = catalogByGroup.has(groupKey)
|
|
1554
|
+
? groupKey
|
|
1555
|
+
: (existingCatalogByTarget?.mapKey ?? groupKey);
|
|
1539
1556
|
const targetObjectKey =
|
|
1540
1557
|
existingCatalog?.target_object_key ?? targetKeyCandidate;
|
|
1541
1558
|
const operationEffectKey = `${targetObjectKey}.${effectAction}`;
|
|
@@ -1706,7 +1723,7 @@ const buildOperationAssignmentCollections = ({
|
|
|
1706
1723
|
catalogEntry.grouping_evidence_refs.push(ref);
|
|
1707
1724
|
}
|
|
1708
1725
|
}
|
|
1709
|
-
catalogByGroup.set(
|
|
1726
|
+
catalogByGroup.set(catalogMapKey, catalogEntry);
|
|
1710
1727
|
|
|
1711
1728
|
mappings.push({
|
|
1712
1729
|
id: mappingId,
|
|
@@ -1991,7 +2008,7 @@ const sanitizeText = (value, route) => {
|
|
|
1991
2008
|
}
|
|
1992
2009
|
result = result
|
|
1993
2010
|
.replace(
|
|
1994
|
-
/\b(select|insert|update|delete
|
|
2011
|
+
/\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b[^"',;}]+/gi,
|
|
1995
2012
|
"[sql]",
|
|
1996
2013
|
)
|
|
1997
2014
|
.replace(
|
|
@@ -2036,25 +2053,80 @@ const sanitizeSemantics = (value, route) => {
|
|
|
2036
2053
|
record && typeof record === "object" && !Array.isArray(record)
|
|
2037
2054
|
? record
|
|
2038
2055
|
: {};
|
|
2056
|
+
const routeConfidence = Number.isFinite(Number(safeRecord.route_confidence))
|
|
2057
|
+
? Math.max(0, Math.min(1, Number(safeRecord.route_confidence)))
|
|
2058
|
+
: 0;
|
|
2039
2059
|
return {
|
|
2040
2060
|
route_summary:
|
|
2041
2061
|
typeof safeRecord.route_summary === "string" &&
|
|
2042
2062
|
safeRecord.route_summary.trim()
|
|
2043
2063
|
? safeRecord.route_summary
|
|
2044
2064
|
: "unknown",
|
|
2045
|
-
action_candidates:
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2065
|
+
action_candidates: sanitizeSemanticCandidates(
|
|
2066
|
+
safeRecord.action_candidates,
|
|
2067
|
+
route,
|
|
2068
|
+
routeConfidence,
|
|
2069
|
+
),
|
|
2070
|
+
outcome_candidates: sanitizeSemanticCandidates(
|
|
2071
|
+
safeRecord.outcome_candidates,
|
|
2072
|
+
route,
|
|
2073
|
+
routeConfidence,
|
|
2074
|
+
),
|
|
2051
2075
|
value_event_candidates: [],
|
|
2052
|
-
route_confidence:
|
|
2053
|
-
? Math.max(0, Math.min(1, Number(safeRecord.route_confidence)))
|
|
2054
|
-
: 0,
|
|
2076
|
+
route_confidence: routeConfidence,
|
|
2055
2077
|
};
|
|
2056
2078
|
};
|
|
2057
2079
|
|
|
2080
|
+
const sanitizeSemanticCandidates = (value, route, routeConfidence) => {
|
|
2081
|
+
if (!Array.isArray(value)) {
|
|
2082
|
+
return [];
|
|
2083
|
+
}
|
|
2084
|
+
return value
|
|
2085
|
+
.map((entry) => {
|
|
2086
|
+
if (typeof entry === "string") {
|
|
2087
|
+
const label = sanitizeText(entry, route).trim();
|
|
2088
|
+
return label
|
|
2089
|
+
? {
|
|
2090
|
+
label,
|
|
2091
|
+
confidence: routeConfidence > 0 ? routeConfidence : 0.5,
|
|
2092
|
+
}
|
|
2093
|
+
: null;
|
|
2094
|
+
}
|
|
2095
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
const label =
|
|
2099
|
+
typeof entry.label === "string" && entry.label.trim()
|
|
2100
|
+
? sanitizeText(entry.label, route).trim()
|
|
2101
|
+
: "";
|
|
2102
|
+
if (!label) {
|
|
2103
|
+
return null;
|
|
2104
|
+
}
|
|
2105
|
+
const candidate = {
|
|
2106
|
+
label,
|
|
2107
|
+
confidence: Number.isFinite(Number(entry.confidence))
|
|
2108
|
+
? Math.max(0, Math.min(1, Number(entry.confidence)))
|
|
2109
|
+
: routeConfidence > 0
|
|
2110
|
+
? routeConfidence
|
|
2111
|
+
: 0.5,
|
|
2112
|
+
};
|
|
2113
|
+
for (const key of [
|
|
2114
|
+
"subject_type",
|
|
2115
|
+
"action_category",
|
|
2116
|
+
"outcome_kind",
|
|
2117
|
+
"value_kind",
|
|
2118
|
+
]) {
|
|
2119
|
+
if (typeof entry[key] === "string" && entry[key].trim()) {
|
|
2120
|
+
candidate[key] = sanitizeText(entry[key], route).trim();
|
|
2121
|
+
} else if (entry[key] === null) {
|
|
2122
|
+
candidate[key] = null;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return candidate;
|
|
2126
|
+
})
|
|
2127
|
+
.filter(Boolean);
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2058
2130
|
const sanitizeForClueStorage = (value, route) => {
|
|
2059
2131
|
if (typeof value === "string") {
|
|
2060
2132
|
return sanitizeText(value, route);
|
|
@@ -2152,6 +2224,10 @@ const previousRouteMap = (previousSnapshot) =>
|
|
|
2152
2224
|
]),
|
|
2153
2225
|
);
|
|
2154
2226
|
|
|
2227
|
+
const hasReusablePreviousRouteSemantics = (route) =>
|
|
2228
|
+
Number(route?.semantics?.route_confidence ?? 0) > 0 &&
|
|
2229
|
+
route?.semantics?.route_summary !== "unknown";
|
|
2230
|
+
|
|
2155
2231
|
const buildRouteEvidencePromptEntry = ({ route, hashScope }) => ({
|
|
2156
2232
|
operation_source_key: route.operation_source_key,
|
|
2157
2233
|
method: route.method,
|
|
@@ -2192,7 +2268,10 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2192
2268
|
continue;
|
|
2193
2269
|
}
|
|
2194
2270
|
|
|
2195
|
-
if (
|
|
2271
|
+
if (
|
|
2272
|
+
previousRoute.route_input_hash === currentHash &&
|
|
2273
|
+
hasReusablePreviousRouteSemantics(previousRoute)
|
|
2274
|
+
) {
|
|
2196
2275
|
plans.set(route.operation_source_key, {
|
|
2197
2276
|
origin: "unchanged_route_reused",
|
|
2198
2277
|
route_input_hash: currentHash,
|
|
@@ -2206,6 +2285,21 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2206
2285
|
continue;
|
|
2207
2286
|
}
|
|
2208
2287
|
|
|
2288
|
+
if (previousRoute.route_input_hash === currentHash) {
|
|
2289
|
+
plans.set(route.operation_source_key, {
|
|
2290
|
+
origin: "changed_route_semantic_regenerated",
|
|
2291
|
+
route_input_hash: currentHash,
|
|
2292
|
+
previous_route: previousRoute,
|
|
2293
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2294
|
+
previous_route_semantic_hash:
|
|
2295
|
+
previousRoute.route_semantic_hash ?? routeSemanticHash(previousRoute),
|
|
2296
|
+
semantic_change_reason:
|
|
2297
|
+
"Previous route semantics had no reusable confidence, so the route was regenerated.",
|
|
2298
|
+
});
|
|
2299
|
+
routesRequiringGeneration.push(route);
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2209
2303
|
const decision = await callAiReuseDecisionProvider({
|
|
2210
2304
|
request,
|
|
2211
2305
|
env,
|
|
@@ -2483,7 +2577,8 @@ const FORBIDDEN_CODE_STRUCTURE_PATTERNS = [
|
|
|
2483
2577
|
},
|
|
2484
2578
|
{
|
|
2485
2579
|
label: "raw sql",
|
|
2486
|
-
pattern:
|
|
2580
|
+
pattern:
|
|
2581
|
+
/\b(?:select\s+(?:\*|[a-z0-9_.,\s]+)\s+from|insert\s+into|update\s+[a-z0-9_."-]+\s+set|delete\s+from)\b/i,
|
|
2487
2582
|
},
|
|
2488
2583
|
{
|
|
2489
2584
|
label: "raw prompt",
|
|
@@ -2627,6 +2722,9 @@ const fetchLatestSnapshot = async ({ request, env }) => {
|
|
|
2627
2722
|
);
|
|
2628
2723
|
}
|
|
2629
2724
|
const body = await response.json();
|
|
2725
|
+
if (body?.found === false && body?.snapshot === undefined) {
|
|
2726
|
+
return null;
|
|
2727
|
+
}
|
|
2630
2728
|
const candidate = body?.snapshot ?? body;
|
|
2631
2729
|
if (!candidate || !Array.isArray(candidate.routes)) {
|
|
2632
2730
|
if (allowFullRegeneration) {
|
|
@@ -2664,7 +2762,16 @@ const sendSnapshot = async ({ request, env, snapshot }) => {
|
|
|
2664
2762
|
body: JSON.stringify(validatedSnapshot),
|
|
2665
2763
|
});
|
|
2666
2764
|
if (!response.ok) {
|
|
2667
|
-
|
|
2765
|
+
const responseBody =
|
|
2766
|
+
typeof response.text === "function"
|
|
2767
|
+
? await response.text().catch(() => "")
|
|
2768
|
+
: "";
|
|
2769
|
+
const responseDetail = responseBody.trim()
|
|
2770
|
+
? `: ${responseBody.trim().slice(0, 1000)}`
|
|
2771
|
+
: "";
|
|
2772
|
+
throw new Error(
|
|
2773
|
+
`Clue semantic snapshot upload failed: ${response.status}${responseDetail}`,
|
|
2774
|
+
);
|
|
2668
2775
|
}
|
|
2669
2776
|
const body = await response.json();
|
|
2670
2777
|
try {
|
|
@@ -2782,7 +2889,11 @@ export const runSemanticCi = async ({
|
|
|
2782
2889
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
2783
2890
|
const routeNeedsAi = routes.some((route) => {
|
|
2784
2891
|
const previousRoute = previousRoutes.get(route.operation_source_key);
|
|
2785
|
-
return
|
|
2892
|
+
return (
|
|
2893
|
+
!previousRoute ||
|
|
2894
|
+
previousRoute.route_input_hash !== routeInputHash(route) ||
|
|
2895
|
+
!hasReusablePreviousRouteSemantics(previousRoute)
|
|
2896
|
+
);
|
|
2786
2897
|
});
|
|
2787
2898
|
if (routeNeedsAi && !env.CLUE_AI_PROVIDER_API_KEY) {
|
|
2788
2899
|
throw new Error("CLUE_AI_PROVIDER_API_KEY is required for semantic generation");
|