@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clue-ai/cli",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clue-ai": "bin/clue-cli.mjs"
@@ -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 (edits.length > 0 || lifecycleInsertions.length > 0) {
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
- ...new Set([
536
- ...plan.edits.map((edit) => edit.file_path),
537
- ...plan.lifecycleInsertions.map((insertion) => insertion.file_path),
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
- "If SDK signatures, environment names, browser token behavior, package installability, or verification ownership are unclear, return status blocked instead of guessing.",
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",
@@ -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.v9";
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.`,
@@ -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
-
@@ -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.",