@genn-inc/cluebase-cli 0.0.2 → 0.0.3

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 CHANGED
@@ -27,8 +27,9 @@ preparation:
27
27
  Node: Express/NestJS/Fastify/Koa) from dependency manifests and framework
28
28
  import signals — language, framework, and service root only
29
29
  - writes `.cluebase/setup-manifest.json`
30
- - defers service-specific runtime env guidance to the Cluebase setup screen Step 2;
31
- the CLI does not write `.env.cluebase`
30
+ - writes the single root `.env.cluebase` file with `[app]`, `[frontend]`, and
31
+ `[frontend_user]` sections when all three Cluebase values are supplied;
32
+ existing `.env*` files are never modified
32
33
 
33
34
  A frontend-only project (single-page app plus serverless/static hosting, with no
34
35
  detectable backend) is a supported setup path and continues as a frontend SDK
@@ -85,8 +86,8 @@ local customer frontend/backend.
85
86
 
86
87
  `npx -y @genn-inc/cluebase-cli setup` reads the Cluebase API base URL, project key, and API
87
88
  key from setup screen flags, detects local services, writes
88
- `.cluebase/setup-manifest.json`, and points runtime env setup to the Cluebase setup
89
- screen Step 2.
89
+ `.cluebase/setup-manifest.json`, and writes the same values to the root
90
+ `.env.cluebase`. The file is a setup artifact and is automatically added to `.gitignore`.
90
91
 
91
92
  ## Required Environment
92
93
 
@@ -94,6 +95,11 @@ screen Step 2.
94
95
  - `CLUEBASE_PROJECT_KEY`: Cluebase setup screen issues this value.
95
96
  - `CLUEBASE_API_BASE_URL`: Cluebase API base URL shown by the setup screen.
96
97
 
98
+ `.env.cluebase` stores `CLUEBASE_INGEST_ENDPOINT`, `CLUEBASE_PROJECT_KEY`, and
99
+ `CLUEBASE_API_KEY` under `[app]`; framework-prefixed public values under `[frontend]`;
100
+ and generic public values under `[frontend_user]`. The API key must never be copied into
101
+ either frontend section.
102
+
97
103
  ## Boundaries
98
104
 
99
105
  - The tool may read allowed source paths in the client repository.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genn-inc/cluebase-cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "cluebase-ai": "bin/cluebase-cli.mjs"
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { dirname, resolve } from "node:path";
3
+ import { resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
 
6
6
  const execFileAsync = promisify(execFile);
@@ -85,6 +85,7 @@ const CLUEBASE_ARTIFACT_GITIGNORE_ENTRIES = [
85
85
  ".cluebase/setup-diff.patch",
86
86
  ".cluebase/setup-review-findings.md",
87
87
  ".cluebase/secrets.json",
88
+ ".env.cluebase",
88
89
  ];
89
90
 
90
91
  const protectCluebaseArtifacts = async ({ repoRoot }) => {
@@ -102,26 +103,6 @@ const protectCluebaseArtifacts = async ({ repoRoot }) => {
102
103
  return results;
103
104
  };
104
105
 
105
- const SECRETS_FILE_PATH = ".cluebase/secrets.json";
106
-
107
- const writeSecretsFile = async ({ repoRoot, apiKey }) => {
108
- if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
109
- return { status: "skipped", reason: "no_api_key_provided" };
110
- }
111
- const absolutePath = resolve(repoRoot, SECRETS_FILE_PATH);
112
- const body = {
113
- cluebase_api_key: apiKey.trim(),
114
- _warning:
115
- "DO NOT COMMIT. This file contains the Cluebase API key. The Cluebase setup CLI auto-adds `.cluebase/secrets.json` to .gitignore — keep it that way.",
116
- };
117
- await mkdir(dirname(absolutePath), { recursive: true });
118
- await writeFile(absolutePath, `${JSON.stringify(body, null, 2)}\n`, {
119
- encoding: "utf8",
120
- mode: 0o600,
121
- });
122
- return { status: "written", path: SECRETS_FILE_PATH };
123
- };
124
-
125
106
  const usage = () =>
126
107
  [
127
108
  "Cluebase CLI:",
@@ -141,7 +122,7 @@ const usage = () =>
141
122
  ].join("\n");
142
123
 
143
124
  const renderEnvironmentInstructions = (instructions) => {
144
- if (!instructions || instructions.status !== "deferred_to_setup_wizard") {
125
+ if (!instructions || instructions.status !== "written_to_cluebase_env") {
145
126
  return "";
146
127
  }
147
128
  const services = Array.isArray(instructions.detected_services)
@@ -154,8 +135,9 @@ const renderEnvironmentInstructions = (instructions) => {
154
135
  });
155
136
  const lines = [
156
137
  "",
157
- "環境変数の設定は setup 画面の Step2 を見て行ってください。",
158
- "(CLI 側では .env.cluebase ファイルは生成しません。setup 画面に必要な値が表示されます)",
138
+ `環境変数を ${instructions.path} に保存しました。`,
139
+ "[app] はサーバー用、[frontend] はframework用、[frontend_user] は公開値の確認用です。",
140
+ "API key は [app] のみに保存しています。既存の .env / .env.local は変更していません。",
159
141
  "",
160
142
  "検出したサービス:",
161
143
  ...(serviceLines.length > 0 ? serviceLines : [" (なし)"]),
@@ -167,20 +149,35 @@ const renderEnvironmentInstructions = (instructions) => {
167
149
  const renderSetupResult = ({ preparation }) => {
168
150
  if (preparation?.status === "ready_for_ai") {
169
151
  const manifestPath =
170
- preparation.artifacts?.setup_manifest_path ??
171
- ".cluebase/setup-manifest.json";
172
- return [
173
- "Cluebase セットアップの準備が完了しました。",
174
- "",
175
- "作成したファイル:",
176
- `- ${manifestPath}`,
177
- "",
178
- "次にやること:",
179
- "1. Claude Code を使う場合: `claude` を起動し、`/cluebase-discover` -> `/cluebase-discover-review` -> `/cluebase-discover-context` -> `/cluebase-discover-check` -> `/cluebase-implement` -> `/cluebase-implement-check` -> `/cluebase-implement-review` -> `/cluebase-doctor` の順に実行してください。",
180
- "2. Codex を使う場合: `codex` を起動し、`$cluebase-discover` -> `$cluebase-discover-review` -> `$cluebase-discover-context` -> `$cluebase-discover-check` -> `$cluebase-implement` -> `$cluebase-implement-check` -> `$cluebase-implement-review` -> `$cluebase-doctor` の順に実行してください。",
181
- "",
182
- "環境変数は Step 2 のコマンド (/cluebase-implement) が env ファイルへ自動で書き込みます。手動設定は通常不要です(自動書込が動かなかった場合のみ、setup 画面の env セクションに表示された値をコピーしてください)。",
183
- ].join("\n");
152
+ preparation.artifacts?.setup_manifest_path ??
153
+ ".cluebase/setup-manifest.json";
154
+ const environmentFilePath =
155
+ preparation.environment_instructions?.status ===
156
+ "written_to_cluebase_env"
157
+ ? preparation.environment_instructions.path ??
158
+ preparation.artifacts?.environment_file_path ??
159
+ ".env.cluebase"
160
+ : null;
161
+ const missingEnvironmentFlags = Array.isArray(
162
+ preparation.environment_instructions?.required_flags,
163
+ )
164
+ ? preparation.environment_instructions.required_flags.join(", ")
165
+ : "--cluebase-api-key, --cluebase-api-base-url, --project-key";
166
+ return [
167
+ "Cluebase セットアップの準備が完了しました。",
168
+ "",
169
+ "作成したファイル:",
170
+ `- ${manifestPath}`,
171
+ ...(environmentFilePath ? [`- ${environmentFilePath}`] : []),
172
+ "",
173
+ "次にやること:",
174
+ "1. Claude Code を使う場合: `claude` を起動し、`/cluebase-discover` -> `/cluebase-discover-review` -> `/cluebase-discover-context` -> `/cluebase-discover-check` -> `/cluebase-implement` -> `/cluebase-implement-check` -> `/cluebase-implement-review` -> `/cluebase-doctor` の順に実行してください。",
175
+ "2. Codex を使う場合: `codex` を起動し、`$cluebase-discover` -> `$cluebase-discover-review` -> `$cluebase-discover-context` -> `$cluebase-discover-check` -> `$cluebase-implement` -> `$cluebase-implement-check` -> `$cluebase-implement-review` -> `$cluebase-doctor` の順に実行してください。",
176
+ "",
177
+ environmentFilePath
178
+ ? `環境変数はルートの ${environmentFilePath} に保存済みです。既存の .env / .env.local は変更していません。`
179
+ : `環境変数は未作成です。${missingEnvironmentFlags} を指定して再実行してください。`,
180
+ ].join("\n");
184
181
  }
185
182
 
186
183
  const blockers = Array.isArray(preparation?.blockers)
@@ -312,6 +309,9 @@ export const runCli = async (argv, io = defaultIo) => {
312
309
  repoRoot,
313
310
  documentsUrl: flags.get("documents-url"),
314
311
  });
312
+ const cluebaseArtifactProtection = await protectCluebaseArtifacts({
313
+ repoRoot,
314
+ });
315
315
  const preparation = flags.has("skills-only")
316
316
  ? {
317
317
  status: "skipped",
@@ -326,15 +326,7 @@ export const runCli = async (argv, io = defaultIo) => {
326
326
  projectKey: flags.get("project-key"),
327
327
  },
328
328
  });
329
- const cluebaseArtifactProtection = await protectCluebaseArtifacts({
330
- repoRoot,
331
- });
332
329
  preparation.cluebase_artifact_protection = cluebaseArtifactProtection;
333
- const secretsProtection = await writeSecretsFile({
334
- repoRoot,
335
- apiKey: flags.get("cluebase-api-key"),
336
- });
337
- preparation.secrets_protection = secretsProtection;
338
330
  const environmentInstructions = renderEnvironmentInstructions(
339
331
  preparation.environment_instructions,
340
332
  );
@@ -0,0 +1,177 @@
1
+ import { chmod, writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+
4
+ export const CLUEBASE_ENV_FILE_PATH = ".env.cluebase";
5
+ export const CLUEBASE_ENV_SECTIONS = Object.freeze([
6
+ "app",
7
+ "frontend",
8
+ "frontend_user",
9
+ ]);
10
+
11
+ export const FRONTEND_PUBLIC_ENV_NAMES = Object.freeze([
12
+ "CLUEBASE_API_BASE_URL",
13
+ "CLUEBASE_PROJECT_KEY",
14
+ ]);
15
+
16
+ export const FRONTEND_PUBLIC_ENV_PREFIX_BY_FRAMEWORK = new Map([
17
+ ["nextjs", "NEXT_PUBLIC_"],
18
+ ["vite", "VITE_"],
19
+ ["vue", "VITE_"],
20
+ ["react", "REACT_APP_"],
21
+ ["sveltekit", "PUBLIC_"],
22
+ ["nuxt", "NUXT_PUBLIC_"],
23
+ ]);
24
+
25
+ const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
26
+ const SECTION_PATTERN = /^\[([a-z_]+)\]$/;
27
+
28
+ const optionalString = (value) =>
29
+ typeof value === "string" && value.trim() ? value.trim() : null;
30
+
31
+ const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
32
+
33
+ const formatEnvValue = (value) => {
34
+ const normalized = optionalString(value);
35
+ if (normalized === null) return null;
36
+ if (/[\r\n]/.test(normalized)) {
37
+ throw new Error(".env.cluebase values must not contain newlines");
38
+ }
39
+ return JSON.stringify(normalized);
40
+ };
41
+
42
+ const envLine = (name, value) => {
43
+ if (!ENV_NAME_PATTERN.test(name)) {
44
+ throw new Error(`invalid .env.cluebase key: ${name}`);
45
+ }
46
+ const formatted = formatEnvValue(value);
47
+ return formatted === null ? null : `${name}=${formatted}`;
48
+ };
49
+
50
+ const section = (name, entries) => {
51
+ const lines = [`[${name}]`];
52
+ for (const [key, value] of entries) {
53
+ const line = envLine(key, value);
54
+ if (line) lines.push(line);
55
+ }
56
+ if (lines.length === 1) lines.push("# no values");
57
+ return lines.join("\n");
58
+ };
59
+
60
+ const frontendEnvName = ({ framework, name }) => {
61
+ const prefix = FRONTEND_PUBLIC_ENV_PREFIX_BY_FRAMEWORK.get(
62
+ String(framework ?? "").toLowerCase(),
63
+ );
64
+ return prefix === undefined ? null : `${prefix}${name}`;
65
+ };
66
+
67
+ export const buildCluebaseEnvFile = ({
68
+ cluebaseApiBaseUrl,
69
+ cluebaseApiKey,
70
+ detectedServices = [],
71
+ projectKey,
72
+ }) => {
73
+ const apiBaseUrlValue = optionalString(cluebaseApiBaseUrl);
74
+ const apiBaseUrl = apiBaseUrlValue
75
+ ? trimTrailingSlash(apiBaseUrlValue)
76
+ : null;
77
+ const normalizedProjectKey = optionalString(projectKey);
78
+ const ingestEndpoint = apiBaseUrl
79
+ ? `${trimTrailingSlash(apiBaseUrl)}/api/v1/ingest/backend`
80
+ : null;
81
+ const frontendServices = Array.isArray(detectedServices)
82
+ ? detectedServices.filter((service) => service?.kind === "frontend")
83
+ : [];
84
+ const frontendEntries = [];
85
+ for (const service of frontendServices) {
86
+ for (const name of FRONTEND_PUBLIC_ENV_NAMES) {
87
+ const prefixedName = frontendEnvName({
88
+ framework: service.framework,
89
+ name,
90
+ });
91
+ if (prefixedName) {
92
+ frontendEntries.push([
93
+ prefixedName,
94
+ name === "CLUEBASE_API_BASE_URL" ? apiBaseUrl : normalizedProjectKey,
95
+ ]);
96
+ }
97
+ }
98
+ }
99
+ const uniqueFrontendEntries = [
100
+ ...new Map(frontendEntries.map(([key, value]) => [key, value])).entries(),
101
+ ];
102
+
103
+ return [
104
+ section("app", [
105
+ ["CLUEBASE_INGEST_ENDPOINT", ingestEndpoint],
106
+ ["CLUEBASE_PROJECT_KEY", normalizedProjectKey],
107
+ ["CLUEBASE_API_KEY", cluebaseApiKey],
108
+ ]),
109
+ section("frontend", uniqueFrontendEntries),
110
+ section("frontend_user", [
111
+ ["CLUEBASE_API_BASE_URL", apiBaseUrl],
112
+ ["CLUEBASE_PROJECT_KEY", normalizedProjectKey],
113
+ ]),
114
+ ].join("\n\n") + "\n";
115
+ };
116
+
117
+ const unquoteValue = (value) => {
118
+ const trimmed = value.trim();
119
+ if (trimmed.length < 2) return trimmed;
120
+ const quote = trimmed[0];
121
+ if ((quote !== "'" && quote !== '"') || trimmed.at(-1) !== quote) {
122
+ return trimmed;
123
+ }
124
+ const inner = trimmed.slice(1, -1);
125
+ if (quote === "'") return inner;
126
+ return inner
127
+ .replace(/\\n/g, "\n")
128
+ .replace(/\\r/g, "\r")
129
+ .replace(/\\t/g, "\t")
130
+ .replace(/\\"/g, '"')
131
+ .replace(/\\\\/g, "\\");
132
+ };
133
+
134
+ export const parseCluebaseEnvFile = (content) => {
135
+ const sections = Object.fromEntries(
136
+ CLUEBASE_ENV_SECTIONS.map((name) => [name, {}]),
137
+ );
138
+ let currentSection = null;
139
+ for (const rawLine of String(content).split(/\r?\n/)) {
140
+ const line = rawLine.trim();
141
+ if (!line || line.startsWith("#")) continue;
142
+ const sectionMatch = SECTION_PATTERN.exec(line);
143
+ if (sectionMatch) {
144
+ currentSection = CLUEBASE_ENV_SECTIONS.includes(sectionMatch[1])
145
+ ? sectionMatch[1]
146
+ : null;
147
+ continue;
148
+ }
149
+ if (!currentSection) continue;
150
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
151
+ if (match) sections[currentSection][match[1]] = unquoteValue(match[2]);
152
+ }
153
+ return sections;
154
+ };
155
+
156
+ export const writeCluebaseEnvFile = async ({
157
+ repoRoot,
158
+ cluebaseApiBaseUrl,
159
+ cluebaseApiKey,
160
+ detectedServices,
161
+ projectKey,
162
+ }) => {
163
+ const absolutePath = resolve(repoRoot, CLUEBASE_ENV_FILE_PATH);
164
+ const content = buildCluebaseEnvFile({
165
+ cluebaseApiBaseUrl,
166
+ cluebaseApiKey,
167
+ detectedServices,
168
+ projectKey,
169
+ });
170
+ await writeFile(absolutePath, content, { encoding: "utf8", mode: 0o600 });
171
+ await chmod(absolutePath, 0o600);
172
+ return {
173
+ status: "written",
174
+ path: CLUEBASE_ENV_FILE_PATH,
175
+ sections: [...CLUEBASE_ENV_SECTIONS],
176
+ };
177
+ };
@@ -6,6 +6,10 @@ export const SETUP_DOCTRINE = {
6
6
  "Cluebase setup installs an external SDK integration so observed product facts can reach Cluebase's Customer Value Understanding Engine. It is not an opportunity to improve, refactor, redesign, or reinterpret the host application.",
7
7
  minimal_diff_reason:
8
8
  "The customer must be able to review and merge the setup diff with confidence. Extra formatting, refactors, auth rewrites, UI changes, or unrelated cleanup make the integration harder to trust.",
9
+ allowed_change_scope:
10
+ "Only files and lines required for Cluebase SDK dependency wiring, lifecycle calls, Cluebase environment loading, and setup verification may change.",
11
+ forbidden_change_scope:
12
+ "Do not fix pre-existing defects, move unrelated code, reformat files, rewrite host authentication or API behavior, or add files that are not required for the Cluebase setup path.",
9
13
  ai_decision_boundary:
10
14
  "The AI should use repository understanding only to choose existing lifecycle boundaries for cluebase.init, cluebase.identify, cluebase.group, and cluebase.reset.",
11
15
  deterministic_control_boundary:
@@ -207,6 +211,8 @@ export const OFFICIAL_SDK_CONTRACT = {
207
211
  export const setupDoctrineSkillLines = () => [
208
212
  `- Purpose: ${SETUP_DOCTRINE.purpose}`,
209
213
  `- Minimal diff reason: ${SETUP_DOCTRINE.minimal_diff_reason}`,
214
+ `- Allowed change scope: ${SETUP_DOCTRINE.allowed_change_scope}`,
215
+ `- Forbidden change scope: ${SETUP_DOCTRINE.forbidden_change_scope}`,
210
216
  `- AI decision boundary: ${SETUP_DOCTRINE.ai_decision_boundary}`,
211
217
  `- Static control boundary: ${SETUP_DOCTRINE.deterministic_control_boundary}`,
212
218
  `- Documentation reason: ${SETUP_DOCTRINE.documentation_reason}`,
@@ -553,7 +553,7 @@ const validateSecretsGitignore = async ({ repoRoot }) => {
553
553
  return errors;
554
554
  };
555
555
 
556
- const buildWarnings = (discoveries, { repoRoot } = {}) => {
556
+ const buildWarnings = (discoveries) => {
557
557
  if (!isPlainObject(discoveries)) return [];
558
558
  const warnings = [];
559
559
  if (
@@ -619,19 +619,7 @@ const buildWarnings = (discoveries, { repoRoot } = {}) => {
619
619
  message: `STEP 3 context outputs are missing: ${missingFields.join(", ")}. /cluebase-implement will hard stop without them — re-run /cluebase-discover so it can resume from step3_context.`,
620
620
  });
621
621
  }
622
- // SECRETS_FILE_MISSING: STEP 5 が CLUEBASE_API_KEY を env file に auto-write
623
- // するための値の source (= `.cluebase/secrets.json`) が無い場合。
624
- if (repoRoot) {
625
- const secretsPath = resolve(repoRoot, ".cluebase/secrets.json");
626
- if (!existsSync(secretsPath)) {
627
- warnings.push({
628
- code: "SECRETS_FILE_MISSING",
629
- message:
630
- ".cluebase/secrets.json was not generated. STEP 5 will not auto-write CLUEBASE_API_KEY into the backend env file. Re-run `npx -y @genn-inc/cluebase-cli setup ... --cluebase-api-key <key>` with the value from the setup screen, or set CLUEBASE_API_KEY manually in the backend env file.",
631
- });
632
- }
633
- }
634
- return warnings;
622
+ return warnings;
635
623
  };
636
624
 
637
625
  const pickExitCode = (errors) => {
@@ -667,7 +655,7 @@ export const validateDiscoveries = async ({
667
655
  errors.push(...validateNoDuplicates(discoveries));
668
656
  errors.push(...(await validateSecretsGitignore({ repoRoot })));
669
657
  }
670
- const warnings = buildWarnings(discoveries, { repoRoot });
658
+ const warnings = buildWarnings(discoveries);
671
659
  const passed = errors.length === 0;
672
660
  return {
673
661
  passed,
@@ -1,12 +1,16 @@
1
1
  // setup-doctor env / URL / dotenv helpers.
2
- // Extracted from setup-doctor for file-size limits; content is unchanged.
2
+ // setup-doctor の正規化された .env.cluebase 読み込みをここで一元管理する。
3
3
 
4
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
5
5
  import { API_CONNECTIVITY_CONTRACT } from "./setup-ai-contract.mjs";
6
6
  import {
7
7
  readSetupDoctorFile,
8
8
  readSetupDoctorJson,
9
9
  } from "./setup-doctor-file-boundary.mjs";
10
+ import {
11
+ CLUEBASE_ENV_FILE_PATH,
12
+ parseCluebaseEnvFile,
13
+ } from "./cluebase-env-file.mjs";
10
14
 
11
15
  export const DEFAULT_SETUP_MANIFEST_PATH = ".cluebase/setup-manifest.json";
12
16
  export const CLUEBASE_BROWSER_TOKEN_PATH =
@@ -18,12 +22,7 @@ export const CLUEBASE_TEST_SETUP_SDK_VERSION = "cluebase_test_setup";
18
22
  export const FRONTEND_SOURCE_IDENTIFIER = "frontend";
19
23
  export const DEFAULT_BATCH_VISIBILITY_TIMEOUT_MS = 60_000;
20
24
  export const BATCH_VISIBILITY_POLL_INTERVAL_MS = 500;
21
- export const DOCTOR_ENV_FILE_NAMES = [
22
- ".env",
23
- ".env.development",
24
- ".env.local",
25
- ".env.development.local",
26
- ];
25
+ export const DOCTOR_ENV_FILE_NAMES = [CLUEBASE_ENV_FILE_PATH];
27
26
 
28
27
  // Frontend SDK calls Cluebase backend directly. Customer backends must not expose
29
28
  // Cluebase-specific route handlers.
@@ -96,14 +95,16 @@ export const publicCluebaseApiBaseUrl = (env) =>
96
95
  optionalString(env.VITE_CLUEBASE_API_BASE_URL) ??
97
96
  optionalString(env.REACT_APP_CLUEBASE_API_BASE_URL) ??
98
97
  optionalString(env.PUBLIC_CLUEBASE_API_BASE_URL) ??
99
- optionalString(env.NUXT_PUBLIC_CLUEBASE_API_BASE_URL);
98
+ optionalString(env.NUXT_PUBLIC_CLUEBASE_API_BASE_URL) ??
99
+ optionalString(env.CLUEBASE_API_BASE_URL);
100
100
 
101
101
  export const publicProjectKeyFromEnv = (env) =>
102
102
  optionalString(env.NEXT_PUBLIC_CLUEBASE_PROJECT_KEY) ??
103
103
  optionalString(env.VITE_CLUEBASE_PROJECT_KEY) ??
104
104
  optionalString(env.REACT_APP_CLUEBASE_PROJECT_KEY) ??
105
105
  optionalString(env.PUBLIC_CLUEBASE_PROJECT_KEY) ??
106
- optionalString(env.NUXT_PUBLIC_CLUEBASE_PROJECT_KEY);
106
+ optionalString(env.NUXT_PUBLIC_CLUEBASE_PROJECT_KEY) ??
107
+ optionalString(env.CLUEBASE_PROJECT_KEY);
107
108
 
108
109
  export const cluebaseApiBaseUrlFromIngestEndpoint = (endpoint) => {
109
110
  const raw = optionalString(endpoint);
@@ -252,22 +253,7 @@ export const envRootsFromManifest = (manifest, kind) => {
252
253
  };
253
254
 
254
255
  export const envFileCandidates = ({ discoveries, kind, manifest, repoRoot }) => {
255
- const candidates = new Set();
256
- if (isPlainObject(discoveries?.env_files)) {
257
- const entry = discoveries.env_files[kind];
258
- if (isPlainObject(entry)) {
259
- addEnvCandidate({ candidates, path: entry.path, repoRoot });
260
- }
261
- }
262
- for (const name of DOCTOR_ENV_FILE_NAMES) {
263
- addEnvCandidate({ candidates, path: name, repoRoot });
264
- }
265
- for (const root of envRootsFromManifest(manifest, kind)) {
266
- for (const name of DOCTOR_ENV_FILE_NAMES) {
267
- addEnvCandidate({ candidates, path: join(root, name), repoRoot });
268
- }
269
- }
270
- return [...candidates];
256
+ return [CLUEBASE_ENV_FILE_PATH];
271
257
  };
272
258
 
273
259
  export const loadDoctorEnvFiles = async ({
@@ -292,7 +278,13 @@ export const loadDoctorEnvFiles = async ({
292
278
  optional: true,
293
279
  });
294
280
  if (content === null) continue;
295
- Object.assign(fileEnv, parseDotenv(content));
281
+ const sections = parseCluebaseEnvFile(content);
282
+ Object.assign(
283
+ fileEnv,
284
+ kind === "backend"
285
+ ? sections.app
286
+ : { ...sections.frontend_user, ...sections.frontend },
287
+ );
296
288
  loadedFiles.push(relPath);
297
289
  }
298
290
  return { env: fileEnv, loadedFiles };
@@ -596,6 +596,10 @@ export const buildBackendEventPayload = ({
596
596
  };
597
597
  const resource = {
598
598
  "service.name": backendServiceKey,
599
+ "cluebase.project_key": projectKey,
600
+ "cluebase.runtime_language": "nodejs",
601
+ "cluebase.service_type": "backend",
602
+ "cluebase.producer_id": backendServiceKey,
599
603
  "telemetry.sdk.language": "nodejs",
600
604
  "service.version": CLUEBASE_TEST_SETUP_SDK_VERSION,
601
605
  };
@@ -40,7 +40,7 @@ import {
40
40
  checkC7,
41
41
  checkC8,
42
42
  } from "./setup-doctor-quality-checks-b.mjs";
43
- import { isPlainObject, isString } from "./setup-doctor-quality-shared.mjs";
43
+ import { CLUEBASE_ENV_FILE_PATH } from "./cluebase-env-file.mjs";
44
44
  import {
45
45
  readSetupDoctorFile,
46
46
  readSetupDoctorJson,
@@ -163,7 +163,7 @@ export const loadQualityInputs = async ({ repoRoot = ".", signal } = {}) => {
163
163
  "package.json",
164
164
  signal,
165
165
  );
166
- const envFiles = await readEnvFiles({ repoRoot, discoveries, signal });
166
+ const envFiles = await readEnvFiles({ repoRoot, signal });
167
167
  return { discoveries, packageJson, envFiles };
168
168
  };
169
169
 
@@ -171,39 +171,16 @@ const readJsonIfExists = async (repoRoot, path, signal) => {
171
171
  return readSetupDoctorJson({ repoRoot, path, signal, optional: true });
172
172
  };
173
173
 
174
- const COMMON_ENV_FILE_NAMES = [
175
- ".env",
176
- ".env.local",
177
- ".env.development",
178
- ".env.production",
179
- ];
180
-
181
- const readEnvFiles = async ({ repoRoot, discoveries, signal }) => {
174
+ const readEnvFiles = async ({ repoRoot, signal }) => {
182
175
  const result = {};
183
- const candidates = new Set();
184
- // discoveries.env_files から
185
- if (isPlainObject(discoveries) && isPlainObject(discoveries.env_files)) {
186
- for (const kind of ["frontend", "backend"]) {
187
- const entry = discoveries.env_files[kind];
188
- if (isPlainObject(entry) && isString(entry.path)) {
189
- candidates.add(entry.path);
190
- }
191
- }
192
- }
193
- // よくある場所
194
- for (const name of COMMON_ENV_FILE_NAMES) {
195
- candidates.add(name);
196
- }
197
- for (const candidate of candidates) {
198
- const content = await readSetupDoctorFile({
199
- repoRoot,
200
- path: candidate,
201
- signal,
202
- optional: true,
203
- });
204
- if (content !== null) {
205
- result[candidate] = content;
206
- }
176
+ const content = await readSetupDoctorFile({
177
+ repoRoot,
178
+ path: CLUEBASE_ENV_FILE_PATH,
179
+ signal,
180
+ optional: true,
181
+ });
182
+ if (content !== null) {
183
+ result[CLUEBASE_ENV_FILE_PATH] = content;
207
184
  }
208
185
  return result;
209
186
  };
@@ -11,26 +11,17 @@ import {
11
11
  } from "./setup-ai-contract.mjs";
12
12
  import { buildSetupDocumentationContract } from "./setup-documents.mjs";
13
13
  import { discoverSetupRepository } from "./setup-repository-discovery.mjs";
14
+ import {
15
+ CLUEBASE_ENV_FILE_PATH,
16
+ FRONTEND_PUBLIC_ENV_NAMES,
17
+ FRONTEND_PUBLIC_ENV_PREFIX_BY_FRAMEWORK,
18
+ writeCluebaseEnvFile,
19
+ } from "./cluebase-env-file.mjs";
14
20
 
15
21
  const DEFAULT_SETUP_MANIFEST_PATH = ".cluebase/setup-manifest.json";
16
22
  const BROWSER_INGEST_PATH =
17
23
  API_CONNECTIVITY_CONTRACT.hops.browser_ingest.path;
18
24
  const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
19
- // setup runtime で各 service が読む env 名一覧。
20
- // 実際の値の提示は setup 画面 (web app の setup wizard) が担当する。
21
- // CLI はサービス検出と manifest 出力のみを担う。
22
- const FRONTEND_PUBLIC_ENV_NAMES = [
23
- "CLUEBASE_API_BASE_URL",
24
- "CLUEBASE_PROJECT_KEY",
25
- ];
26
- const FRONTEND_PUBLIC_ENV_PREFIX_BY_FRAMEWORK = new Map([
27
- ["nextjs", "NEXT_PUBLIC_"],
28
- ["vite", "VITE_"],
29
- ["vue", "VITE_"],
30
- ["react", "REACT_APP_"],
31
- ["sveltekit", "PUBLIC_"],
32
- ["nuxt", "NUXT_PUBLIC_"],
33
- ]);
34
25
  const BACKEND_RUNTIME_ENV_NAMES = [
35
26
  "CLUEBASE_PROJECT_KEY",
36
27
  "CLUEBASE_INGEST_ENDPOINT",
@@ -108,11 +99,11 @@ const requiredFrontendEnvNames = (detectedServices) => [
108
99
 
109
100
  const requiredBackendEnvNames = () => [...BACKEND_RUNTIME_ENV_NAMES];
110
101
 
111
- // 環境変数は CLI 側で .env.cluebase ファイルを生成せず、setup 画面 (web app の setup wizard)
112
- // に表示・誘導を任せる方針。 CLI はサービス検出結果と manifest 出力だけを担う。
113
- // この関数は CLI が detect した service 一覧と「Step 2 は setup 画面で行ってください」
114
- // という deferred 状態を返すのみ。
115
- const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
102
+ const buildEnvironmentInstructions = ({
103
+ environmentFile,
104
+ manifest,
105
+ setupContext,
106
+ }) => {
116
107
  const missingFlags = [
117
108
  ["cluebase_api_key", "--cluebase-api-key"],
118
109
  ["cluebase_api_base_url", "--cluebase-api-base-url"],
@@ -134,9 +125,11 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
134
125
  ? manifest.detected_services
135
126
  : [];
136
127
  return {
137
- status: "deferred_to_setup_wizard",
128
+ status: "written_to_cluebase_env",
138
129
  message:
139
- "環境変数は setup 画面の Step 2 を見て、各サービスの env ファイルに反映してください。",
130
+ `Cluebase の環境変数をルートの ${CLUEBASE_ENV_FILE_PATH} に保存しました。既存の .env / .env.local は変更していません。`,
131
+ path: environmentFile?.path ?? CLUEBASE_ENV_FILE_PATH,
132
+ sections: environmentFile?.sections ?? ["app", "frontend", "frontend_user"],
140
133
  detected_services: detectedServices.map((target) => ({
141
134
  kind: target.kind,
142
135
  framework: target.framework,
@@ -146,14 +139,7 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
146
139
  };
147
140
 
148
141
  const summarizeEnvironmentInstructions = (instructions) => {
149
- if (!instructions || instructions.status !== "deferred_to_setup_wizard") {
150
- return instructions;
151
- }
152
- return {
153
- status: "deferred_to_setup_wizard",
154
- message: instructions.message,
155
- detected_services: instructions.detected_services,
156
- };
142
+ return instructions;
157
143
  };
158
144
 
159
145
  export const runSetupPrepare = async ({
@@ -272,9 +258,10 @@ export const runSetupPrepare = async ({
272
258
  },
273
259
  artifacts: {
274
260
  setup_manifest_path: setupManifestPath,
261
+ environment_file_path: CLUEBASE_ENV_FILE_PATH,
275
262
  },
276
- machine_owned_artifacts: [setupManifestPath],
277
- ai_must_not_edit: [],
263
+ machine_owned_artifacts: [setupManifestPath, CLUEBASE_ENV_FILE_PATH],
264
+ ai_must_not_edit: [CLUEBASE_ENV_FILE_PATH],
278
265
  ai_owned_workstreams: ["sdk_lifecycle_placement"],
279
266
  ai_implementation_scope: {
280
267
  rule: "AI implementation is limited to placing cluebase.init, cluebase.identify, cluebase.group, and cluebase.reset in existing lifecycle boundaries plus the minimal SDK wiring required for those calls.",
@@ -342,7 +329,20 @@ export const runSetupPrepare = async ({
342
329
  backend_runtime: backendRuntimeEnvNames,
343
330
  },
344
331
  };
332
+ const environmentFile =
333
+ setupContext.cluebase_api_key &&
334
+ setupContext.cluebase_api_base_url &&
335
+ setupContext.project_key
336
+ ? await writeCluebaseEnvFile({
337
+ repoRoot: resolvedRepoRoot,
338
+ cluebaseApiBaseUrl: setupContext.cluebase_api_base_url,
339
+ cluebaseApiKey: setupContext.cluebase_api_key,
340
+ detectedServices,
341
+ projectKey: setupContext.project_key,
342
+ })
343
+ : null;
345
344
  const environmentInstructions = buildEnvironmentInstructions({
345
+ environmentFile,
346
346
  manifest,
347
347
  setupContext,
348
348
  });
@@ -105,27 +105,15 @@ Steps:
105
105
  - Discriminator skip: \`status !== "success"\`, \`result.kind === "error"\`, \`type !== "real"\`.
106
106
  The lifecycle call must NEVER fire with the filtered value. Pick the line of the next concrete side-effect after the guard (typical examples: \`router.push\`, \`router.refresh\`, \`navigate(\`, \`await api.(post|put|delete|patch)\`, \`db.commit\`, \`return response\`, \`dispatch(success)\`, \`emit("success", ...)\`).
107
107
 
108
- 4.5. ENV FILE PATH DISCOVER. STEP 5 (= /cluebase-implement) needs to auto-write CLUEBASE_* env vars into the customer's frontend and backend env files. Discover those file paths NOW so STEP 5 has the addresses to write into.
108
+ 4.5. ENV CONFIGURATION BOUNDARY. The CLI has already written the single Cluebase
109
+ environment source at the repository root: \`.env.cluebase\`. Do not search for,
110
+ create, edit, or record any customer \`.env\`, \`.env.local\`, or other service-local
111
+ environment file. Do not copy values into another file. The sections are fixed:
112
+ \`[app]\` for server-only values, \`[frontend]\` for framework-prefixed public values,
113
+ and \`[frontend_user]\` for the generic public values a user can inspect.
109
114
 
110
- For each \`detected_services\` entry in \`.cluebase/setup-manifest.json\` (recall: each entry has \`kind\` = "frontend" or "backend", plus \`root_path\` = that service's directory relative to the repo root):
111
-
112
- (a) Frontend env file: probe these candidates in order via Read / Glob and pick the FIRST one that exists:
113
- 1. \`<root_path>/.env.local\` (= Next.js / Vite convention for local dev env)
114
- 2. \`<root_path>/.env.development\`
115
- 3. \`<root_path>/.env\`
116
- If none exists, default to \`<root_path>/.env.local\` and treat it as a new file that STEP 5 will create. Record this as \`env_files.frontend = { "path": "<rel path>", "format": "dotenv" }\`. If \`detected_services\` has no \`kind="frontend"\` entry (= no frontend service), set \`env_files.frontend = null\`.
117
-
118
- (b) Backend env file: probe candidates in order:
119
- 1. \`<root_path>/.env\`
120
- 2. \`<root_path>/.env.development\`
121
- If neither exists, default to \`<root_path>/.env\` as a new file. Record \`env_files.backend = { "path": "<rel path>", "format": "dotenv" }\`. If no backend service is detected, set \`env_files.backend = null\`.
122
-
123
- (c) Validate paths:
124
- * Path MUST be relative to the repo root (no absolute paths).
125
- * Path MUST live inside the repo (no \`..\` escape).
126
- * If a probed path is outside the repo or unreachable, add an entry to \`unclear_points\` describing the issue and set \`env_files.<kind> = null\`.
127
-
128
- Output the result on the new top-level \`env_files\` field (see "Output JSON schema" below).
115
+ If \`.env.cluebase\` is absent, record one \`unclear_points\` entry asking the user
116
+ to re-run \`npx -y @genn-inc/cluebase-cli setup\` with the three Cluebase values.
129
117
 
130
118
  5. COMPLETENESS SELF-CHECK (mandatory — answer each item internally; any "no" / "未確認" means you MUST loop back to Phase A or B before writing the artifact):
131
119
 
@@ -274,10 +262,6 @@ Output JSON schema (output exactly this shape; arrays may be empty []):
274
262
  "mastra_sites": [ { "file": "<rel path>", "line": <int>, "mastra_variable_name": "<identifier>", "evidence": "<1-line snippet>" }, ... ],
275
263
  "existing_cluebase_calls": [ { "api": "cluebase.init|cluebase.identify|cluebase.group|cluebase.reset", "file": "<rel path>", "line": <int> }, ... ],
276
264
  "unclear_points": [ { "concern": "<short>", "files_or_areas": ["<path or pattern>"] }, ... ],
277
- "env_files": {
278
- "frontend": { "path": "<rel path to frontend env file, e.g. 'frontend/.env.local'>", "format": "dotenv" } | null,
279
- "backend": { "path": "<rel path to backend env file, e.g. 'backend/.env'>", "format": "dotenv" } | null
280
- },
281
265
  "db_schema": {
282
266
  "detection_notes": "<1-3 sentence summary of which schema sources were inspected (e.g. 'Prisma schema.prisma at root + raw SQL migrations in db/migrations; user/account/workspace entities identified.')>",
283
267
  "entities": [
@@ -530,44 +514,7 @@ Steps:
530
514
 
531
515
  f. Be CONSERVATIVE. When a candidate path is uncertain (e.g. the variable might be undefined at the insertion line, or comes from a guard branch that may not be entered), record it as \`null\` with a note explaining the uncertainty — do NOT invent a fake path. STEP 5 will pass null / \`None\` and degrade gracefully.
532
516
 
533
- 3.5. ENV LINES CALCULATION. STEP 5 (= /cluebase-implement) writes env files. STEP 3 pre-computes EXACTLY what NAME=value lines belong in each env file so STEP 5 is a pure write operation with no judgment.
534
-
535
- (a) Read \`.cluebase/setup-manifest.json\` and extract:
536
- * \`cluebase_context.project_key\` (e.g. \`pk_dev_...\`)
537
- * \`cluebase_context.cluebase_api_base_url\`
538
- * \`cluebase_context.ingest_endpoints.browser\`
539
- * \`cluebase_context.ingest_endpoints.backend\`
540
- * \`detected_services[]\` root paths. The frontend SDK calls the Cluebase backend directly.
541
-
542
- (b) Read \`.cluebase/secrets.json\` and extract \`cluebase_api_key\`. If the file does not exist, skip CLUEBASE_API_KEY in env_lines.backend and add an \`unclear_points\` entry "CLUEBASE_API_KEY is unavailable (.cluebase/secrets.json missing) — STEP 5 will not auto-write it. Re-run setup with --cluebase-api-key.".
543
-
544
- (c) Determine frontend env-var prefix from \`discoveries.framework_frontend\`:
545
- * \`nextjs\` → \`NEXT_PUBLIC_\`
546
- * \`vite\` / Vite-based React → \`VITE_\`
547
- * \`react\` → \`REACT_APP_\`
548
- * \`sveltekit\` → \`PUBLIC_\`
549
- * \`nuxt\` → \`NUXT_PUBLIC_\`
550
- * \`angular\` → leave \`env_lines.frontend\` empty and add an \`unclear_points\` entry instructing the user to copy the setup screen's Angular runtime config values.
551
- * \`solid\` / \`qwik\` / \`astro\` / unknown → leave \`env_lines.frontend\` empty and add an \`unclear_points\` entry naming the framework; STEP 5 will then ask the user once for the correct public config shape.
552
-
553
- (d) Build the line set:
554
- \`env_lines.frontend\` (2 entries when prefix is known; the frontend SDK calls Cluebase directly):
555
- \`<PREFIX>CLUEBASE_API_BASE_URL=<cluebase_api_base_url>\`
556
- \`<PREFIX>CLUEBASE_PROJECT_KEY=<project_key>\`
557
-
558
- \`env_lines.backend\` (3 entries; omit CLUEBASE_API_KEY when secrets.json is absent):
559
- \`CLUEBASE_INGEST_ENDPOINT=<ingest_endpoints.backend>\`
560
- \`CLUEBASE_PROJECT_KEY=<project_key>\`
561
- \`CLUEBASE_API_KEY=<cluebase_api_key from .cluebase/secrets.json>\`
562
-
563
- (e) Hard rules:
564
- * NEVER include CLUEBASE_API_KEY (or any other server-only secret) in env_lines.frontend. env_lines.frontend is only for browser-public SDK configuration.
565
- * Every line MUST match the pattern \`^[A-Z_][A-Z0-9_]*=...$\`. No comments, no trailing whitespace, no quotes around values.
566
- * env_lines.frontend / env_lines.backend may be empty arrays when the corresponding side has no env file or when the prefix is unknown (see (c)).
567
-
568
- Record the result on the new top-level \`env_lines\` field (alongside \`env_files\` recorded by STEP 1).
569
-
570
- 4. Use the Write tool to overwrite \`.cluebase/discoveries.json\` with the enriched JSON. Preserve all existing top-level keys (\`framework_frontend\`, \`framework_backend\`, \`service_key\`, \`cluebase_init_frontend\`, \`cluebase_init_backend\`, \`identify_sites\`, \`group_sites\`, \`reset_sites\`, \`existing_cluebase_calls\`, \`unclear_points\`, \`env_files\`) and add the new top-level \`organization_context\` + \`env_lines\`, the new per-site \`available_fields\` + \`field_acquisition_notes\` keys, and \`group_owner_kind\` on every \`group_sites\` entry. Output only the \`{\` ... \`}\` object content; nothing else.
517
+ 4. Use the Write tool to overwrite \`.cluebase/discoveries.json\` with the enriched JSON. Preserve all existing discovery keys and add the new per-site \`available_fields\` + \`field_acquisition_notes\` keys, \`organization_context\`, and \`group_owner_kind\` on every \`group_sites\` entry. Do not add environment file paths or copied environment values to discoveries.json. Output only the \`{\` ... \`}\` object content; nothing else.
571
518
 
572
519
  5. Respond with exactly ONE Japanese line and stop. Substitute N (identify_sites length), M (group_sites length), and the chosen organization label:
573
520
  \`.cluebase/discoveries.json の context 解析を完了しました(識別 N 件、organization M 件、organization_label = <primary_organization_label>)。次は Claude Code で /cluebase-discover-check と打って STEP 4 を実行してください。\`
@@ -675,11 +622,6 @@ Respond with exactly this Japanese line:
675
622
  SUBSTEP_TO_RESTART = step3_context
676
623
  REASON = cluebase.identify / cluebase.group に渡す引数情報 (available_fields / organization_context) がまだ記録できていません
677
624
 
678
- (e) Else if ANY error.message mentions one of: "env_files", "env_lines":
679
- NEXT_ACTION = RESUME_FROM_SUBSTEP
680
- SUBSTEP_TO_RESTART = step1_discover
681
- REASON = env file の path / line 情報が discoveries.json に正しく記録できていません
682
-
683
625
  (f) Otherwise (site shape / file path / duplicate / unclear points など STEP 2 で直すべき問題):
684
626
  NEXT_ACTION = RESUME_FROM_SUBSTEP
685
627
  SUBSTEP_TO_RESTART = step2_review
@@ -58,52 +58,20 @@ Steps:
58
58
  - Express / NestJS / Fastify / Hono (Node backend): add \`${RECOMMENDED_BACKEND_SDK_PACKAGE_SPEC}\` to package.json. The same package serves all Node backend integrations (Express middleware / LangChain / Mastra / AI observer / MCP observer); do NOT install a separate package per framework.
59
59
  4. Run the matching install command via the Bash tool: \`bun.lock\` -> \`bun install\`, \`pnpm-lock.yaml\` -> \`pnpm install\`, \`yarn.lock\` -> \`yarn install\`, \`package-lock.json\` -> \`npm install\`. For Python: \`pip install -r requirements.txt\` (or the uv equivalent if uv is present). A package manifest / lockfile mismatch causes STEP 6's setup-check to fail. If install fails or no manager is detected, STOP and report a blocker.
60
60
 
61
- 4.5. ENV FILE AUTO-WRITE (STEP 5 owns env file setup so STEP 9 can pass without manual user copy/paste from the setup screen).
62
-
63
- Goal: write the required CLUEBASE_* env vars into the customer's frontend and backend env files based on values in \`.cluebase/setup-manifest.json\` + \`.cluebase/secrets.json\`.
64
-
65
- 4.5.a Read inputs (skip this whole step and continue to step 5 if any required input is missing — tell the user which one):
66
- - Use Read tool on \`.cluebase/setup-manifest.json\`. Extract \`cluebase_context.project_key\`, \`cluebase_context.cluebase_api_base_url\`, \`cluebase_context.ingest_endpoints.browser\`, \`cluebase_context.ingest_endpoints.backend\`, plus \`detected_services[]\` root paths.
67
- - Use Read tool on \`.cluebase/secrets.json\`. Extract \`cluebase_api_key\`. If the file does not exist, skip the CLUEBASE_API_KEY auto-write and tell the user: "CLUEBASE_API_KEY は .cluebase/secrets.json 不在のため自動書込できません。 setup CLI を --cluebase-api-key 付きで再実行するか、 backend env file に手動で追加してください。"
68
-
69
- 4.5.b Determine env file paths:
70
- - frontend service root: probe in order \`<root_path>/.env.local\`, \`<root_path>/.env.development\`, \`<root_path>/.env\`. Pick the first that exists. If none exists, default to \`<root_path>/.env.local\` and create it (treat as new file).
71
- - backend service root: probe \`<root_path>/.env\`, \`<root_path>/.env.development\`. Default to \`<root_path>/.env\` for new files.
72
- - All paths must resolve inside the repo root (no absolute paths leading outside).
73
-
74
- 4.5.c Determine the frontend env-var prefix from \`discoveries.framework_frontend\`:
75
- - \`nextjs\` → \`NEXT_PUBLIC_\`
76
- - \`vite\` or React/Vite stack → \`VITE_\`
77
- - \`react\` → \`REACT_APP_\`
78
- - \`sveltekit\` → \`PUBLIC_\`
79
- - \`nuxt\` → \`NUXT_PUBLIC_\` (verify against \`nuxt.config\` if uncertain)
80
- - \`angular\` → skip frontend env auto-write and instruct the user to copy the setup screen's Angular runtime config values.
81
- - \`solid\` / \`qwik\` / \`astro\` / unknown → ask the user once in a single Japanese line which public config shape the framework uses, or skip auto-write and instruct manual setup.
82
-
83
- 4.5.d Build the env line set:
84
- Frontend (2 lines, every line uses the prefix from 4.5.c — collectively \`<PREFIX>\`; the frontend SDK calls Cluebase directly):
85
- \`<PREFIX>CLUEBASE_API_BASE_URL=<cluebase_api_base_url>\`
86
- \`<PREFIX>CLUEBASE_PROJECT_KEY=<project_key>\`
87
-
88
- Backend (3 lines, NO prefix; these are server-only env names):
89
- \`CLUEBASE_INGEST_ENDPOINT=<ingest_endpoints.backend>\`
90
- \`CLUEBASE_PROJECT_KEY=<project_key>\`
91
- \`CLUEBASE_API_KEY=<from .cluebase/secrets.json>\` ← skip this line if secrets.json was absent
92
-
93
- Hard rule: CLUEBASE_API_KEY must NEVER be wired into browser-public frontend SDK configuration. Both are server-side values.
94
-
95
- 4.5.e Upsert the lines (use Edit tool with surgical \`old_string\` / \`new_string\` on existing files; use Write tool only when creating a new env file):
96
- For each NAME in the line set, scan the existing file for a line matching \`^<NAME>\\s*=\`:
97
- * found AND value differs → replace the matched line with the new \`NAME=value\`.
98
- * found AND value matches → leave it; do not duplicate.
99
- * not found → append \`NAME=value\` to the end (add a leading newline if the file does not already end with one).
100
- When creating a new env file: write only the joined env lines + trailing newline. Do not add narrative comments.
101
-
102
- 4.5.f Verify post-write state via Read on each env file:
103
- - Every required NAME appears exactly once with the expected value.
104
- - The browser-public frontend SDK env lines MUST NOT contain \`CLUEBASE_API_KEY=\` or any unprefixed \`CLUEBASE_\` line. If such a line would be used by browser code, remove it from the frontend SDK wiring and re-verify.
105
-
106
- 4.5.g Record a short internal summary of what was written (counts only — do NOT echo the secret values themselves) so STEP 7 can review the env state in the next pass.
61
+ 4.5. ENV CONFIGURATION CHECK. The CLI owns one root file: \`.env.cluebase\`.
62
+
63
+ - Read \`.env.cluebase\` and use its fixed sections: \`[app]\` for server-only
64
+ values, \`[frontend]\` for framework-prefixed public values, and
65
+ \`[frontend_user]\` for generic public values.
66
+ - Do not create, edit, or copy Cluebase values into any \`.env\`,
67
+ \`.env.local\`, \`.env.development\`, or other service-local environment file.
68
+ - Do not edit \`.env.cluebase\` in this AI step. The CLI created it with mode
69
+ 0600, and it is a machine-owned setup artifact.
70
+ - Verify that \`[app]\` contains \`CLUEBASE_INGEST_ENDPOINT\`,
71
+ \`CLUEBASE_PROJECT_KEY\`, and \`CLUEBASE_API_KEY\`. Verify that
72
+ \`[frontend]\` contains only the framework-prefixed public base URL and
73
+ project key. Verify that \`CLUEBASE_API_KEY\` is absent from both frontend
74
+ sections.
107
75
 
108
76
  5. SELF-REVIEW PASS (mandatory; up to 3 iterations). Inspect your own changes via \`git diff HEAD\` (Bash tool) and apply the rubric below to your diff. If any P0/P1 issue is found, fix it surgically (Edit tool, or \`git checkout HEAD -- <file>\` + re-apply for reformat noise), then re-inspect. Stop when clean or after 3 iterations. Track the cumulative number of self-corrections.
109
77
 
@@ -433,10 +401,10 @@ STEP 5(実装 + 自己レビュー)完了
433
401
  cluebase.init / cluebase.identify / cluebase.group / cluebase.reset 挿入数: <counts>
434
402
  依存追加: <dependency files>
435
403
  install 実行: <command>
436
- env file 書込: frontend=<frontend-env-path> (lines=<N>), backend=<backend-env-path> (lines=<N>)
404
+ 環境設定: ルートの .env.cluebase を確認済み(既存の .env / .env.local は変更していません)
437
405
  自己修正: <N> 件
438
406
 
439
- 重要: frontend / backend の dev server を一度停止して再起動してください。 env file を書き換えただけでは dev server は新しい env を読み込みません。 再起動後に次の STEP に進んでください。
407
+ 重要: 顧客アプリが参照する既存の環境変数ローダーで .env.cluebase の該当sectionを読み込めることを確認してください。新しい環境変数ファイルを追加してはいけません。
440
408
 
441
409
  次は Claude Code で /cluebase-implement-check と打って STEP 6(静的検証)を実行してください。
442
410
  ==========================================
@@ -519,9 +487,8 @@ Process (loop up to 3 iterations; track iteration index N starting at 1 and cumu
519
487
 
520
488
  P0 rubric (must block release):
521
489
  - CLUEBASE_API_KEY wired into browser-public frontend SDK config. If detected, REMOVE that line from the frontend SDK wiring and verify the same key/value remains available to the server runtime.
522
- - Backend env file is missing any of: \`CLUEBASE_API_KEY\`, \`CLUEBASE_PROJECT_KEY\`, \`CLUEBASE_INGEST_ENDPOINT\`. Auto-fix by reading values from \`.cluebase/setup-manifest.json\` (\`cluebase_context.project_key\` / \`cluebase_context.ingest_endpoints.backend\`) + \`.cluebase/secrets.json\` (\`cluebase_api_key\`) and appending / upserting via Edit. Skip CLUEBASE_API_KEY only if \`.cluebase/secrets.json\` is absent, in which case escalate to residual findings asking the user to re-run setup with --cluebase-api-key.
523
- - Frontend env file is missing any of the prefixed equivalents of \`CLUEBASE_API_BASE_URL\`, \`CLUEBASE_PROJECT_KEY\` (prefix determined by framework_frontend: \`NEXT_PUBLIC_\` for Next.js, \`VITE_\` for Vite, \`PUBLIC_\` for SvelteKit, etc.). Auto-fix the same way as backend, using \`cluebase_context.cluebase_api_base_url\` for API_BASE_URL. The frontend SDK calls the Cluebase backend directly.
524
- - \`.cluebase/secrets.json\` exists but is NOT registered in the repo's \`.gitignore\`. Auto-fix by appending \`.cluebase/secrets.json\` to \`.gitignore\` via Edit.
490
+ - Root \`.env.cluebase\` is missing any required value or section. Do not create a second env file and do not edit an existing \`.env*\`; ask the user to re-run the CLI setup so the machine-owned file is regenerated.
491
+ - \`.env.cluebase\` contains \`CLUEBASE_API_KEY\` in \`[frontend]\` or \`[frontend_user]\`. Remove the leaked value from those public sections and re-run the CLI setup; the key may exist only in \`[app]\`.
525
492
  - CLUEBASE_API_KEY referenced from any browser/client-bundled file.
526
493
  - await on cluebase.init / cluebase.identify / cluebase.group / cluebase.reset in a blocking path.
527
494
  - try/catch / try/except / .catch wrapping lifecycle calls solely for Cluebase.
@@ -613,7 +580,7 @@ export function buildStep9SetupDoctor() {
613
580
 
614
581
  Before running the command, briefly explain to the user (1-2 lines) what this step does:
615
582
 
616
- STEP 9 は Cluebase の疎通確認です。 Cluebase token 発行 / browser ingest / backend ingest を customer env files から作った SDK-equivalent payload で実際に POST し、downstream batch publish evidence まで検証します。 加えて顧客 backend の \`/api/v1/cluebase/*\` route を blocking error として確認します。実際の顧客 frontend/backend lifecycle 発火は、顧客のローカル画面操作後に Cluebase setup logs または published batch evidence で確認します。
583
+ STEP 9 は Cluebase の疎通確認です。ルートの \`.env.cluebase\` の \`[frontend]\` / \`[app]\` を読み、Cluebase token 発行 / browser ingest / backend ingest を SDK-equivalent payload で実際に POST し、downstream batch publish evidence まで検証します。加えて顧客 backend の \`/api/v1/cluebase/*\` route を blocking error として確認します。実際の顧客 frontend/backend lifecycle 発火は、顧客のローカル画面操作後に Cluebase setup logs または published batch evidence で確認します。
617
584
 
618
585
  Then run the following command via the Bash tool:
619
586
 
@@ -638,17 +605,17 @@ Respond with exactly:
638
605
  • If the local frontend Origin is not available automatically, re-run \`${CLUEBASE_CLI_INVOCATION} setup-doctor --local --client-frontend-url http://localhost:<frontend-port>\`.
639
606
 
640
607
  Bucket FRONTEND_ENV_MISSING — when check.error mentions "CLUEBASE_API_BASE_URL" or "CLUEBASE_PROJECT_KEY" on the \`browser_ingest\` check.
641
- • Required env (set in frontend env file, e.g. \`.env.local\` for Next.js / \`.env\` for Vite, prefix the variable with the framework's public-env prefix):
608
+ • Required env (set in the \`[frontend]\` section of root \`.env.cluebase\`, using the framework's public-env prefix):
642
609
  <frontend public prefix>CLUEBASE_API_BASE_URL=<from setup screen Step 2>
643
610
  <frontend public prefix>CLUEBASE_PROJECT_KEY=<from setup screen Step 2>
644
- • After writing, RESTART the frontend dev server.
611
+ • After the existing runtime loader reads \`.env.cluebase\`, RESTART the frontend dev server.
645
612
 
646
613
  Bucket BACKEND_ENV_MISSING — when check.error mentions "CLUEBASE_API_KEY", "CLUEBASE_INGEST_ENDPOINT", or "CLUEBASE_PROJECT_KEY" on the \`backend_ingest\` check.
647
- • Required env (set in backend env file, e.g. \`.env\` for FastAPI / Django / Express):
614
+ • Required env (set in the \`[app]\` section of root \`.env.cluebase\`):
648
615
  CLUEBASE_API_KEY=<from setup screen Step 2>
649
616
  CLUEBASE_PROJECT_KEY=<from setup screen Step 2>
650
617
  CLUEBASE_INGEST_ENDPOINT=<from setup screen Step 2>
651
- • After writing, RESTART the backend dev server.
618
+ • After the existing runtime loader reads \`.env.cluebase\`, RESTART the backend dev server.
652
619
 
653
620
  Bucket UPSTREAM_FAILURE — when check.error mentions "browser token" on the \`browser_ingest\` check (= caused by FRONTEND_ENV_MISSING; do not bucket separately).
654
621
 
@@ -662,23 +629,21 @@ Respond with exactly:
662
629
 
663
630
  3. AUTO-FIX (offer once before falling back to manual instructions):
664
631
 
665
- For each missing env name in FRONTEND_ENV_MISSING / BACKEND_ENV_MISSING buckets, resolve the value:
666
- • project key / API base URL / ingest endpoint → use the setup screen Step 2 value already written to the target env file, or the setup artifact value produced from the same screen when the env file is missing the line.
667
- • CLUEBASE_API_KEY → use the setup screen Step 2 value already written to the backend env file, or \`.cluebase/secrets.json\` when the backend env file is missing the line. If neither source exists, instruct the user to copy CLUEBASE_API_KEY from the Cluebase setup screen into the backend env file; do NOT proceed to auto-fix CLUEBASE_API_KEY without a source.
632
+ For each missing env name in FRONTEND_ENV_MISSING / BACKEND_ENV_MISSING buckets, resolve the value from the matching section of root \`.env.cluebase\` or from the original setup arguments. If the value is absent, ask the user to re-run the CLI setup; do not create another env file and do not print the API key.
668
633
 
669
634
  For each candidate env value that resolves to a concrete string, propose the auto-fix as ONE message to the user in the form:
670
635
 
671
636
  STEP 9 で env 不足を検出しました。 以下を自動で書き込みますか?
672
- frontend env file (<path>): <list of NAME=value lines>
673
- backend env file (<path>): <list of NAME=value lines>
674
- 書き込み後、 frontend / backend dev server を再起動して /cluebase-doctor を再実行してください。
637
+ .env.cluebase [frontend]: <list of NAME=value keys>
638
+ .env.cluebase [app]: <list of NAME=value keys>
639
+ 設定後、 frontend / backend dev server を再起動して /cluebase-doctor を再実行してください。
675
640
  OK の場合: 「OK」 と返信してください。 NG の場合: 「NG」 と返信してください。
676
641
 
677
642
  When the user replies "OK":
678
- - Use Edit tool to append the lines to the frontend / backend env file (upsert semantics: if a NAME already exists with a different value, replace its value; if NAME does not exist, append a new line).
679
- - Use Edit / Bash tool to do nothing else. Do NOT restart the dev server (the user controls dev servers).
643
+ - Do not use Edit to write any \`.env*\` file. Ask the user to re-run the CLI setup so it owns the update of \`.env.cluebase\`.
644
+ - Do not edit \`.env.cluebase\` from the AI step and do not restart the dev server.
680
645
  - Respond with:
681
- env file への書き込みが完了しました。 frontend / backend dev server を再起動してから、 もう一度 /cluebase-doctor を実行してください。 dev server を再起動しないと新 env が反映されません。
646
+ \`.env.cluebase\` の再生成が必要です。CLI setupを再実行し、frontend / backend dev server を再起動してから、もう一度 /cluebase-doctor を実行してください。
682
647
 
683
648
  When the user replies "NG" or you could not resolve all values:
684
649
  - Skip the auto-fix and fall through to manual instructions (step 4).
@@ -690,12 +655,12 @@ Respond with exactly:
690
655
  STEP 9(疎通確認)で問題が見つかりました(checks N_FAILED 件 / passed N_PASSED 件)。 以下を順番に直してください:
691
656
 
692
657
  [For each non-empty bucket, print one numbered line in this exact form:]
693
- <N>. <bucket-specific human-readable instruction in Japanese — e.g. "frontend env file (.env.local 等) に framework に合う public env 名で CLUEBASE_API_BASE_URL=<setup screen Step 2 の値> を追加し、 frontend dev server を再起動してください">
658
+ <N>. <bucket-specific human-readable instruction in Japanese — e.g. "CLI setupを再実行して .env.cluebase [frontend] に framework に合う public env 名を生成し、frontend dev server を再起動してください">
694
659
 
695
- 修正後にもう一度 /cluebase-doctor を実行すると再チェックされます。 setup 画面の Step 2 (環境変数) の値もコピー元として使えます。
660
+ 修正後にもう一度 /cluebase-doctor を実行すると再チェックされます。値が不明な場合は setup 画面の値を使ってCLI setupを再実行してください。
696
661
 
697
662
  Hard rules:
698
- - Do not edit \`.cluebase/setup-manifest.json\` or \`.cluebase/secrets.json\` in this STEP — STEP 9 is verification + (optional) env-file auto-fix only.
663
+ - Do not edit \`.cluebase/setup-manifest.json\` or \`.env.cluebase\` in this STEP — STEP 9 is verification only.
699
664
  - Never write CLUEBASE_API_KEY into browser-public frontend SDK configuration. The value is only for server runtime and the authenticated Cluebase setup screen.
700
665
  - Never restart dev servers from this STEP. Always ask the user to restart manually after auto-fix.
701
666
  - TRANSPORT_FAILURE buckets are NOT auto-fixable — instruct the user to start the affected dev server / verify the URL and re-run /cluebase-doctor.
@@ -6,7 +6,7 @@
6
6
  // - cluebase-discover-review.md — STEP 2: self-review of STEP 1 output (AI)
7
7
  // - cluebase-discover-context.md — STEP 3: field-semantic enrichment (AI)
8
8
  // - cluebase-discover-check.md — STEP 4: bash validation of discoveries.json
9
- // - cluebase-implement.md — STEP 5: SDK call insertion + env file write
9
+ // - cluebase-implement.md — STEP 5: SDK call insertion + env config check
10
10
  // - cluebase-implement-check.md — STEP 6: bash static check + diff snapshot
11
11
  // - cluebase-implement-review.md — STEP 7: self-review of inserted code (AI)
12
12
  // - cluebase-events.md — STEP 8: guided business value event track (AI)
@@ -88,7 +88,7 @@ const STEP_FILES = [
88
88
  id: "step1_discover",
89
89
  title: "STEP 1 — Discover (boundary enumeration)",
90
90
  singleGoal:
91
- "The ONLY goal of this command is to grep + Read the customer codebase exhaustively and produce the initial `.cluebase/discoveries.json` (lifecycle boundary candidates + db_schema grounding + env file paths). SDK call insertion, env file writes, code changes, and connectivity checks are ALL out of scope.",
91
+ "The ONLY goal of this command is to grep + Read the customer codebase exhaustively and produce the initial `.cluebase/discoveries.json` (lifecycle boundary candidates + db_schema grounding). SDK call insertion, environment file writes, code changes, and connectivity checks are ALL out of scope.",
92
92
  scopeOut: [
93
93
  "STEP 2 (= /cluebase-discover-review): self-review / fix of discoveries.json",
94
94
  "STEP 3 (= /cluebase-discover-context): field-semantic enrichment (`available_fields` / `organization_context`)",
@@ -96,7 +96,7 @@ const STEP_FILES = [
96
96
  "STEP 5+ (= /cluebase-implement and later): any source code change",
97
97
  ],
98
98
  completion:
99
- "`.cluebase/discoveries.json` is written with all boundaries + db_schema + env_files populated, `_progress.completed_substeps` is patched with `step1_discover`, and the Japanese hand-off line for STEP 2 is printed.",
99
+ "`.cluebase/discoveries.json` is written with all boundaries + db_schema, `_progress.completed_substeps` is patched with `step1_discover`, and the Japanese hand-off line for STEP 2 is printed.",
100
100
  nextHandoff: "/cluebase-discover-review",
101
101
  },
102
102
  {
@@ -125,15 +125,15 @@ const STEP_FILES = [
125
125
  id: "step3_context",
126
126
  title: "STEP 3 — Field semantic context enrichment",
127
127
  singleGoal:
128
- "The ONLY goal of this command is to enrich `.cluebase/discoveries.json` with two new pieces: per-site `available_fields` (concrete variable paths reachable at each insertion line) + top-level `organization_context` (the customer's company/organization label) + `env_lines` pre-computed for STEP 5. The customer code is NOT modified.",
128
+ "The ONLY goal of this command is to enrich `.cluebase/discoveries.json` with per-site `available_fields` (concrete variable paths reachable at each insertion line) and top-level `organization_context` (the customer's company/organization label). The customer code and `.env.cluebase` are NOT modified.",
129
129
  scopeOut: [
130
130
  "STEP 1 (= /cluebase-discover): boundary discovery",
131
131
  "STEP 2 (= /cluebase-discover-review): rubric-based self-review",
132
132
  "STEP 4 (= /cluebase-discover-check): bash validation",
133
- "STEP 5+ (= /cluebase-implement and later): code edits, env file writes",
133
+ "STEP 5+ (= /cluebase-implement and later): code edits, `.env.cluebase` handling, and connectivity checks",
134
134
  ],
135
135
  completion:
136
- "`.cluebase/discoveries.json` gains `available_fields` / `organization_context` / `env_lines`, `_progress.completed_substeps` is patched with `step3_context`, and the Japanese hand-off line for STEP 4 is printed.",
136
+ "`.cluebase/discoveries.json` gains `available_fields` / `organization_context`, `_progress.completed_substeps` is patched with `step3_context`, and the Japanese hand-off line for STEP 4 is printed.",
137
137
  nextHandoff: "/cluebase-discover-check",
138
138
  },
139
139
  {
@@ -147,7 +147,7 @@ const STEP_FILES = [
147
147
  "The ONLY goal of this command is to mechanically validate `.cluebase/discoveries.json` via the `setup-discover-check` CLI helper, surface any error/warning, and (when an error is found) instruct the user which earlier STEP to re-run by removing the matching substepId from `_progress.completed_substeps`. No AI judgment, no code edits.",
148
148
  scopeOut: [
149
149
  "STEP 1-3: writing or modifying `.cluebase/discoveries.json` itself (only substepId removal allowed when re-running an earlier STEP)",
150
- "STEP 5+ (= /cluebase-implement and later): code edits, env file writes",
150
+ "STEP 5+ (= /cluebase-implement and later): code edits, `.env.cluebase` handling, and connectivity checks",
151
151
  ],
152
152
  completion:
153
153
  "`setup-discover-check` reports passed=true, `_progress.completed_substeps` is patched with `step4_check`, and the Japanese hand-off line for STEP 5 is printed. On failure: the user is told exactly which earlier STEP to re-run.",
@@ -161,7 +161,7 @@ const STEP_FILES = [
161
161
  id: "step5_implement",
162
162
  title: "STEP 5 — Implement (SDK call insertion + env file write)",
163
163
  singleGoal:
164
- "The ONLY goal of this command is to insert Cluebase SDK lifecycle calls (cluebase.init / cluebase.identify / cluebase.group / cluebase.reset + observer wire-ups) at the file:line positions recorded in `.cluebase/discoveries.json`, add the SDK as a package-manifest dependency, run the install command, and write the pre-computed `env_lines` into the customer's frontend/backend env files. NO discoveries.json edits, NO bash validation, NO connectivity checks.",
164
+ "The ONLY goal of this command is to insert Cluebase SDK lifecycle calls (cluebase.init / cluebase.identify / cluebase.group / cluebase.reset + observer wire-ups) at the file:line positions recorded in `.cluebase/discoveries.json`, add the SDK as a package-manifest dependency, run the install command, and verify the CLI-owned `.env.cluebase` configuration. Do not create or edit service-local `.env*` files. NO discoveries.json edits, NO bash validation, NO connectivity checks.",
165
165
  scopeOut: [
166
166
  "STEP 1-4: modifying `.cluebase/discoveries.json` (it is read-only here)",
167
167
  "STEP 6 (= /cluebase-implement-check): bash static check + diff snapshot generation",
@@ -170,7 +170,7 @@ const STEP_FILES = [
170
170
  "STEP 9 (= /cluebase-doctor): real-server connectivity check",
171
171
  ],
172
172
  completion:
173
- "All lifecycle / observer call sites in discoveries.json are inserted via Edit, SDK install is run successfully, env files are upserted with `env_lines`, `_progress.completed_substeps` is patched with `step5_implement`, and the Japanese hand-off line for STEP 6 is printed.",
173
+ "All lifecycle / observer call sites in discoveries.json are inserted via Edit, SDK install is run successfully, `.env.cluebase` is verified without exposing its secret, `_progress.completed_substeps` is patched with `step5_implement`, and the Japanese hand-off line for STEP 6 is printed.",
174
174
  nextHandoff: "/cluebase-implement-check",
175
175
  },
176
176
  {
@@ -237,7 +237,7 @@ const STEP_FILES = [
237
237
  id: "step9_doctor",
238
238
  title: "STEP 9 — Production-level setup verification",
239
239
  singleGoal:
240
- "The ONLY goal of this command is to run `setup-doctor --local` via Bash. It probes Cluebase token issue, browser ingest, and backend ingest with SDK-equivalent payloads from customer env files, checks downstream browser/backend batch publish evidence, scans for forbidden customer-backend Cluebase routes, and runs 30 data-quality checks. `/cluebase-doctor` is intentionally STATELESS — re-run any time after fixes.",
240
+ "The ONLY goal of this command is to run `setup-doctor --local` via Bash. It reads the root `.env.cluebase` sections, probes Cluebase token issue, browser ingest, and backend ingest with SDK-equivalent payloads, checks downstream browser/backend batch publish evidence, scans for forbidden customer-backend Cluebase routes, and runs 30 data-quality checks. `/cluebase-doctor` is intentionally STATELESS — re-run any time after fixes.",
241
241
  scopeOut: [
242
242
  "STEP 1-8 (= /cluebase-discover .. /cluebase-events): anything that modifies discoveries.json, customer code, business-events.json, or runs the static check",
243
243
  "Persisting progress (this STEP is stateless — DO NOT write to the `_progress` array; it is intentionally re-runnable)",