@clue-ai/cli 0.0.22 → 0.0.23
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/package.json +1 -1
- package/src/lifecycle-init.mjs +81 -7
- package/src/setup-agent.mjs +1 -0
- package/src/setup-ai-contract.mjs +48 -1
- package/src/setup-documents.mjs +1 -1
- package/src/setup-tool.mjs +3 -1
package/package.json
CHANGED
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
|
|
|
@@ -57,6 +58,18 @@ const LIFECYCLE_PLAN_TOOL_SCHEMA = {
|
|
|
57
58
|
additionalProperties: false,
|
|
58
59
|
},
|
|
59
60
|
},
|
|
61
|
+
file_creations: {
|
|
62
|
+
type: "array",
|
|
63
|
+
items: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
file_path: { type: "string" },
|
|
67
|
+
content: { type: "string" },
|
|
68
|
+
},
|
|
69
|
+
required: ["file_path", "content"],
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
60
73
|
lifecycle_insertions: {
|
|
61
74
|
type: "array",
|
|
62
75
|
items: {
|
|
@@ -99,6 +112,7 @@ const LIFECYCLE_PLAN_TOOL_SCHEMA = {
|
|
|
99
112
|
required: [
|
|
100
113
|
"status",
|
|
101
114
|
"edits",
|
|
115
|
+
"file_creations",
|
|
102
116
|
"lifecycle_insertions",
|
|
103
117
|
"blockers",
|
|
104
118
|
"warnings",
|
|
@@ -197,6 +211,16 @@ const assertClueSetupOnlyEdit = (edit) => {
|
|
|
197
211
|
}
|
|
198
212
|
};
|
|
199
213
|
|
|
214
|
+
const assertClueSetupOnlyFileCreation = (fileCreation) => {
|
|
215
|
+
if (!CLUE_SETUP_ADDITION_PATTERN.test(fileCreation.content)) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Clue lifecycle file creations must contain only Clue setup related code in ${fileCreation.file_path}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
assertNoForbiddenInstrumentation(fileCreation.content);
|
|
221
|
+
assertLifecycleCallsAreNonBlocking(fileCreation.content);
|
|
222
|
+
};
|
|
223
|
+
|
|
200
224
|
const normalizeLifecycleInsertion = (input) => ({
|
|
201
225
|
api_name: assertApiName(nonEmpty(input.api_name, "api_name")),
|
|
202
226
|
file_path: nonEmpty(input.file_path, "file_path"),
|
|
@@ -240,6 +264,12 @@ const normalizePlan = (input) => {
|
|
|
240
264
|
find: nonEmpty(edit.find, "edit.find"),
|
|
241
265
|
replace: nonEmpty(edit.replace, "edit.replace"),
|
|
242
266
|
}));
|
|
267
|
+
const fileCreations = Array.isArray(input.file_creations)
|
|
268
|
+
? input.file_creations.map((fileCreation) => ({
|
|
269
|
+
file_path: nonEmpty(fileCreation.file_path, "file_creation.file_path"),
|
|
270
|
+
content: nonEmpty(fileCreation.content, "file_creation.content"),
|
|
271
|
+
}))
|
|
272
|
+
: [];
|
|
243
273
|
const lifecycleInsertions = Array.isArray(input.lifecycle_insertions)
|
|
244
274
|
? input.lifecycle_insertions.map(normalizeLifecycleInsertion)
|
|
245
275
|
: [];
|
|
@@ -247,7 +277,11 @@ const normalizePlan = (input) => {
|
|
|
247
277
|
? input.blockers.map(normalizeBlocker)
|
|
248
278
|
: [];
|
|
249
279
|
if (status === "blocked") {
|
|
250
|
-
if (
|
|
280
|
+
if (
|
|
281
|
+
edits.length > 0 ||
|
|
282
|
+
fileCreations.length > 0 ||
|
|
283
|
+
lifecycleInsertions.length > 0
|
|
284
|
+
) {
|
|
251
285
|
throw new Error("blocked lifecycle plan must not include edits");
|
|
252
286
|
}
|
|
253
287
|
if (blockers.length === 0) {
|
|
@@ -260,6 +294,7 @@ const normalizePlan = (input) => {
|
|
|
260
294
|
return {
|
|
261
295
|
status,
|
|
262
296
|
edits,
|
|
297
|
+
fileCreations,
|
|
263
298
|
lifecycleInsertions,
|
|
264
299
|
blockers,
|
|
265
300
|
warnings: Array.isArray(input.warnings)
|
|
@@ -532,10 +567,11 @@ const buildLifecycleEvidenceSourceMap = async ({
|
|
|
532
567
|
sourceByPath = new Map(),
|
|
533
568
|
}) => {
|
|
534
569
|
for (const filePath of [
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
570
|
+
...new Set([
|
|
571
|
+
...plan.edits.map((edit) => edit.file_path),
|
|
572
|
+
...plan.fileCreations.map((fileCreation) => fileCreation.file_path),
|
|
573
|
+
...plan.lifecycleInsertions.map((insertion) => insertion.file_path),
|
|
574
|
+
]),
|
|
539
575
|
]) {
|
|
540
576
|
await loadSourceIntoMap({ repoRoot, sourceByPath, filePath });
|
|
541
577
|
}
|
|
@@ -580,6 +616,28 @@ export const applyLifecyclePlan = async ({
|
|
|
580
616
|
}
|
|
581
617
|
const sourceByPath = new Map();
|
|
582
618
|
const pendingWrites = new Map();
|
|
619
|
+
for (const fileCreation of plan.fileCreations) {
|
|
620
|
+
const { absolutePath, relativePath } = safeRelativePath(
|
|
621
|
+
repoRoot,
|
|
622
|
+
fileCreation.file_path,
|
|
623
|
+
);
|
|
624
|
+
assertAllowedWritePath({ allowedWritePaths, filePath: relativePath });
|
|
625
|
+
assertClueSetupOnlyFileCreation(fileCreation);
|
|
626
|
+
try {
|
|
627
|
+
await readFile(absolutePath, "utf8");
|
|
628
|
+
throw new Error(`file_creation target already exists: ${relativePath}`);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
if (error?.code !== "ENOENT") throw error;
|
|
631
|
+
}
|
|
632
|
+
sourceByPath.set(relativePath, {
|
|
633
|
+
file_path: relativePath,
|
|
634
|
+
text: fileCreation.content,
|
|
635
|
+
});
|
|
636
|
+
pendingWrites.set(relativePath, {
|
|
637
|
+
absolutePath,
|
|
638
|
+
text: fileCreation.content,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
583
641
|
for (const edit of plan.edits) {
|
|
584
642
|
const { absolutePath, relativePath } = safeRelativePath(
|
|
585
643
|
repoRoot,
|
|
@@ -624,9 +682,13 @@ export const applyLifecyclePlan = async ({
|
|
|
624
682
|
});
|
|
625
683
|
}
|
|
626
684
|
for (const write of pendingWrites.values()) {
|
|
685
|
+
await mkdir(dirname(write.absolutePath), { recursive: true });
|
|
627
686
|
await writeFile(write.absolutePath, write.text, "utf8");
|
|
628
687
|
}
|
|
629
688
|
return {
|
|
689
|
+
fileCreations: plan.fileCreations.map((fileCreation) => ({
|
|
690
|
+
file_path: fileCreation.file_path,
|
|
691
|
+
})),
|
|
630
692
|
lifecycleInsertions: plan.lifecycleInsertions,
|
|
631
693
|
warnings: plan.warnings,
|
|
632
694
|
};
|
|
@@ -719,13 +781,17 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
719
781
|
task: "Add Clue SDK lifecycle API calls to this repository using exact text replacements.",
|
|
720
782
|
setup_doctrine: SETUP_DOCTRINE,
|
|
721
783
|
deterministic_control_model: DETERMINISTIC_CONTROL_MODEL,
|
|
784
|
+
official_sdk_contract: OFFICIAL_SDK_CONTRACT,
|
|
722
785
|
api_connectivity_contract: API_CONNECTIVITY_CONTRACT,
|
|
723
786
|
rules: [
|
|
724
787
|
"Return JSON only.",
|
|
725
788
|
"Understand the setup doctrine before choosing edits. This is an external Clue integration, not a host app refactor or redesign task.",
|
|
726
789
|
"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
|
-
"
|
|
790
|
+
"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.",
|
|
791
|
+
"If SDK signatures, environment names, browser token behavior, package installability, or verification ownership remain unclear after applying official_sdk_contract, return status blocked instead of guessing.",
|
|
728
792
|
"Use only exact replacements. Each find string must be copied exactly from source.",
|
|
793
|
+
"Use file_creations only for minimal Clue-owned setup wiring files when no existing Clue adapter or browser-token proxy module exists.",
|
|
794
|
+
"Do not use file_creations for host application refactors, business logic, unrelated helpers, formatting, or non-Clue abstractions.",
|
|
729
795
|
"Add ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout where repository code has clear lifecycle points.",
|
|
730
796
|
"Official Clue SDK public lifecycle APIs are no-throw and own SDK failure isolation.",
|
|
731
797
|
"Do not add per-call try/catch, try/except, .catch, or custom safe wrappers solely around official Clue SDK public lifecycle calls.",
|
|
@@ -752,6 +818,7 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
752
818
|
"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
819
|
"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
820
|
"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.",
|
|
821
|
+
"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.",
|
|
755
822
|
"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
823
|
"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
824
|
"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 +826,7 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
759
826
|
"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
827
|
"Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
|
|
761
828
|
"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.",
|
|
829
|
+
"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
830
|
"Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
|
|
763
831
|
"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
832
|
"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.",
|
|
@@ -797,6 +865,12 @@ const buildLifecyclePrompt = ({ request, files }) =>
|
|
|
797
865
|
output_shape: {
|
|
798
866
|
status: "ready",
|
|
799
867
|
blockers: [],
|
|
868
|
+
file_creations: [
|
|
869
|
+
{
|
|
870
|
+
file_path: "app/clue_adapter.py",
|
|
871
|
+
content: "new Clue-only setup file content",
|
|
872
|
+
},
|
|
873
|
+
],
|
|
800
874
|
edits: [
|
|
801
875
|
{
|
|
802
876
|
file_path: "app/main.py",
|
package/src/setup-agent.mjs
CHANGED
|
@@ -456,6 +456,7 @@ export const runSetupAgent = async ({
|
|
|
456
456
|
lastSetupCheck = setupCheck;
|
|
457
457
|
attempts.push({
|
|
458
458
|
attempt,
|
|
459
|
+
file_creations: lifecycleResult.fileCreations ?? [],
|
|
459
460
|
lifecycle_insertions: lifecycleResult.lifecycleInsertions,
|
|
460
461
|
warnings: lifecycleResult.warnings,
|
|
461
462
|
setup_check_passed: setupCheck.passed,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const AI_SETUP_CONTRACT_VERSION =
|
|
2
|
-
"2026-05-10.latest-sdk-contract.
|
|
2
|
+
"2026-05-10.latest-sdk-contract.v10";
|
|
3
3
|
|
|
4
4
|
export const SETUP_DOCTRINE = {
|
|
5
5
|
purpose:
|
|
@@ -102,6 +102,52 @@ export const FRONTEND_ADAPTER_CONTRACT = {
|
|
|
102
102
|
],
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
export const OFFICIAL_SDK_CONTRACT = {
|
|
106
|
+
purpose:
|
|
107
|
+
"This bundled contract is the authoritative Clue SDK contract for setup-agent. A customer repository is expected to start without Clue SDK imports or dependencies; absence of existing Clue code is not a blocker.",
|
|
108
|
+
frontend_browser_sdk: {
|
|
109
|
+
package_name: "@clue-ai/browser-sdk",
|
|
110
|
+
dependency_specifier: "@clue-ai/browser-sdk@latest",
|
|
111
|
+
import_path: "@clue-ai/browser-sdk",
|
|
112
|
+
public_lifecycle_apis: {
|
|
113
|
+
ClueInit:
|
|
114
|
+
"ClueInit(options: { endpoint: string; projectKey: string; environment: string; serviceKey?: string; producerId?: string; browserTokenProvider?: () => string | Promise<string>; ... }): void",
|
|
115
|
+
ClueIdentify:
|
|
116
|
+
"ClueIdentify(userId: string, traits?: Record<string, string | number | boolean | null>): void",
|
|
117
|
+
ClueSetAccount:
|
|
118
|
+
"ClueSetAccount(accountId: string, traits?: Record<string, string | number | boolean | null>): void",
|
|
119
|
+
ClueLogout: "ClueLogout(): void",
|
|
120
|
+
},
|
|
121
|
+
safety_contract:
|
|
122
|
+
"Public lifecycle APIs are no-throw wrappers. Do not add custom per-call try/catch or await them.",
|
|
123
|
+
},
|
|
124
|
+
backend_fastapi_sdk: {
|
|
125
|
+
package_name: "clue-fastapi-sdk",
|
|
126
|
+
dependency_specifier: "clue-fastapi-sdk",
|
|
127
|
+
python_import:
|
|
128
|
+
"from clue_fastapi_sdk import clue_init_fastapi, ClueIdentify, ClueSetAccount, ClueLogout",
|
|
129
|
+
public_lifecycle_apis: {
|
|
130
|
+
clue_init_fastapi:
|
|
131
|
+
"clue_init_fastapi(app, *, project_key: str, environment: str, api_key: str | None = None, service_name: str = 'python-service', producer_id: str | None = None, service_key: str | None = None, ... ) -> bool",
|
|
132
|
+
ClueIdentify:
|
|
133
|
+
"ClueIdentify(user_id: str, traits: Mapping[str, object] | None = None) -> bool",
|
|
134
|
+
ClueSetAccount:
|
|
135
|
+
"ClueSetAccount(account_id: str, traits: Mapping[str, object] | None = None) -> bool",
|
|
136
|
+
ClueLogout: "ClueLogout(reason: str | None = None) -> bool",
|
|
137
|
+
},
|
|
138
|
+
safety_contract:
|
|
139
|
+
"Public lifecycle APIs catch SDK errors internally and return bool where applicable. Do not wrap each call solely for Clue failure isolation.",
|
|
140
|
+
},
|
|
141
|
+
minimal_file_creation_contract: {
|
|
142
|
+
allowed_when:
|
|
143
|
+
"No existing Clue adapter, browser-token proxy module, or client bootstrap wrapper exists in the customer repo.",
|
|
144
|
+
allowed_files:
|
|
145
|
+
"Only Clue-owned minimal SDK wiring files under existing frontend/backend source roots, plus exact replacements in existing files to import/register those files.",
|
|
146
|
+
rule:
|
|
147
|
+
"Creating a minimal Clue adapter or browser-token proxy is allowed setup wiring, not a host application refactor.",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
105
151
|
export const setupDoctrineSkillLines = () => [
|
|
106
152
|
`- Purpose: ${SETUP_DOCTRINE.purpose}`,
|
|
107
153
|
`- Minimal diff reason: ${SETUP_DOCTRINE.minimal_diff_reason}`,
|
|
@@ -109,6 +155,7 @@ export const setupDoctrineSkillLines = () => [
|
|
|
109
155
|
`- Static control boundary: ${SETUP_DOCTRINE.deterministic_control_boundary}`,
|
|
110
156
|
`- Documentation reason: ${SETUP_DOCTRINE.documentation_reason}`,
|
|
111
157
|
`- Failure posture: ${SETUP_DOCTRINE.failure_posture}`,
|
|
158
|
+
`- Official SDK contract: ${OFFICIAL_SDK_CONTRACT.purpose}`,
|
|
112
159
|
`- API connectivity: ${API_CONNECTIVITY_CONTRACT.purpose}`,
|
|
113
160
|
`- Frontend adapter: ${FRONTEND_ADAPTER_CONTRACT.purpose}`,
|
|
114
161
|
`- API preflight: run ${API_CONNECTIVITY_CONTRACT.preflight_command} when local services and required env are available; do not substitute it for user-operated setup-watch.`,
|
package/src/setup-documents.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export const CORE_SETUP_DOCUMENT_IDS = [
|
|
|
8
8
|
"clue-boundary",
|
|
9
9
|
"environment-and-secrets",
|
|
10
10
|
"find-integration-points",
|
|
11
|
+
"official-sdk-contract",
|
|
11
12
|
"browser-token-endpoint",
|
|
12
13
|
"clue-init",
|
|
13
14
|
"clue-identify",
|
|
@@ -96,4 +97,3 @@ export const buildSetupDocumentationContract = ({
|
|
|
96
97
|
"Read the relevant Clue setup documents before editing and list consulted_document_ids in the final report.",
|
|
97
98
|
};
|
|
98
99
|
};
|
|
99
|
-
|
package/src/setup-tool.mjs
CHANGED
|
@@ -260,7 +260,7 @@ const skillBody = (name, { documentsUrl } = {}) => {
|
|
|
260
260
|
"Open the docs page when the AI tool has browser or HTTP access. If it cannot be opened, continue only from the generated skill text and manifest doc ids, then report `documentation_access_blocked`.",
|
|
261
261
|
"Read all ids in `documentation.required_doc_ids` that apply to the detected repository.",
|
|
262
262
|
"For framework-specific implementation, select ids from `documentation.selected_framework_doc_ids` first. If empty, use `documentation.framework_doc_ids_by_framework` for the detected framework.",
|
|
263
|
-
"Minimum lifecycle docs before implementation: ai-setup-order, environment-and-secrets, browser-token-endpoint, clue-init, clue-identify, clue-set-account, clue-logout, setup-verification, forbidden-patterns.",
|
|
263
|
+
"Minimum lifecycle docs before implementation: ai-setup-order, environment-and-secrets, official-sdk-contract, browser-token-endpoint, clue-init, clue-identify, clue-set-account, clue-logout, setup-verification, forbidden-patterns.",
|
|
264
264
|
"For Next.js, read framework-nextjs. For React, Vite, Vue, or Angular browser apps, read framework-react-spa. For FastAPI, read framework-fastapi. For Django, read framework-django.",
|
|
265
265
|
"Return `consulted_document_ids` to the orchestrator and final report.",
|
|
266
266
|
"If a document contradicts `.clue/setup-manifest.json` or generated skills, stop and report a blocker instead of choosing behavior silently.",
|
|
@@ -316,6 +316,8 @@ const skillBody = (name, { documentsUrl } = {}) => {
|
|
|
316
316
|
"Do not add per-call try/catch, try/except, `.catch`, or custom safe wrappers solely around official Clue SDK public lifecycle calls.",
|
|
317
317
|
"Never await a Clue lifecycle call in a way that can block login, logout, account selection, request handling, page rendering, or API responses.",
|
|
318
318
|
"Add or report the required SDK dependency instead of fabricating lifecycle APIs.",
|
|
319
|
+
"A clean customer repository is expected to have no existing Clue SDK imports, dependencies, browserTokenProvider wiring, or Clue adapter. Do not treat that absence as a blocker; add the minimal official Clue setup wiring.",
|
|
320
|
+
"When no existing Clue adapter or browser-token proxy module exists, creating a minimal Clue-owned setup file under the existing source root is allowed. Do not use new files for unrelated host app abstractions.",
|
|
319
321
|
"For frontend code, add the real `@clue-ai/browser-sdk` dependency when missing. Use the latest channel (`@clue-ai/browser-sdk@latest`) so setup receives current SDK fixes. Do not invent `clue-js-sdk`, `@clue/browser-sdk`, local placeholder modules, or dynamic imports that hide a missing SDK.",
|
|
320
322
|
`When lifecycle edits are clear, write an exact replacement plan to a temporary local JSON file and apply it with \`${clueCliCommand("lifecycle-apply --plan <plan-file> --repo .")}\`.`,
|
|
321
323
|
"If npm/npx cannot fetch the Clue CLI package, report a blocker with the exact command and error instead of manually applying replacement plans.",
|