@clue-ai/cli 0.0.7 → 0.0.9

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.
@@ -5,10 +5,10 @@ import { findLifecycleGuardViolations } from "./lifecycle-guard.mjs";
5
5
  import { listAllowedSourceFiles } from "./path-policy.mjs";
6
6
 
7
7
  const API_NAMES = new Set([
8
- "ClueInit",
9
- "ClueIdentify",
10
- "ClueSetAccount",
11
- "ClueLogout",
8
+ "ClueInit",
9
+ "ClueIdentify",
10
+ "ClueSetAccount",
11
+ "ClueLogout",
12
12
  ]);
13
13
 
14
14
  const SOURCE_EXTENSIONS = [".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
@@ -17,208 +17,213 @@ const MAX_FILE_CHARS = 12_000;
17
17
  const MAX_TOTAL_CHARS = 360_000;
18
18
 
19
19
  const nonEmpty = (value, field) => {
20
- if (typeof value !== "string" || value.trim() === "") {
21
- throw new Error(`${field} is required`);
22
- }
23
- return value.trim();
20
+ if (typeof value !== "string" || value.trim() === "") {
21
+ throw new Error(`${field} is required`);
22
+ }
23
+ return value.trim();
24
24
  };
25
25
 
26
26
  const safeRelativePath = (repoRoot, filePath) => {
27
- const root = resolve(repoRoot);
28
- const absolutePath = resolve(root, filePath);
29
- const relativePath = relative(root, absolutePath);
30
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
31
- throw new Error(`edit path escapes repo root: ${filePath}`);
32
- }
33
- return { absolutePath, relativePath };
27
+ const root = resolve(repoRoot);
28
+ const absolutePath = resolve(root, filePath);
29
+ const relativePath = relative(root, absolutePath);
30
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
31
+ throw new Error(`edit path escapes repo root: ${filePath}`);
32
+ }
33
+ return { absolutePath, relativePath };
34
34
  };
35
35
 
36
36
  const assertApiName = (apiName) => {
37
- if (!API_NAMES.has(apiName)) {
38
- throw new Error(`unsupported lifecycle API: ${apiName}`);
39
- }
40
- return apiName;
37
+ if (!API_NAMES.has(apiName)) {
38
+ throw new Error(`unsupported lifecycle API: ${apiName}`);
39
+ }
40
+ return apiName;
41
41
  };
42
42
 
43
43
  const assertNoForbiddenInstrumentation = (replacement) => {
44
- if (replacement.includes("ClueTrack")) {
45
- throw new Error("init tool must not add broad ClueTrack instrumentation");
46
- }
47
- if (/data-clue-(id|key)/i.test(replacement)) {
48
- throw new Error("init tool must not add data-clue-id or data-clue-key");
49
- }
44
+ if (replacement.includes("ClueTrack")) {
45
+ throw new Error("init tool must not add broad ClueTrack instrumentation");
46
+ }
47
+ if (/data-clue-(id|key)/i.test(replacement)) {
48
+ throw new Error("init tool must not add data-clue-id or data-clue-key");
49
+ }
50
50
  };
51
51
 
52
52
  const assertLifecycleCallsAreGuarded = (replacement) => {
53
- const violations = findLifecycleGuardViolations(replacement);
54
- if (violations.length > 0) {
55
- throw new Error(
56
- `Clue lifecycle calls must be failure-isolated with try/catch, try/except, or an explicit safe Clue helper: ${JSON.stringify(violations)}`,
57
- );
58
- }
53
+ const violations = findLifecycleGuardViolations(replacement);
54
+ if (violations.length > 0) {
55
+ throw new Error(
56
+ `Clue lifecycle calls must be failure-isolated with try/catch, try/except, or an explicit safe Clue helper: ${JSON.stringify(violations)}`,
57
+ );
58
+ }
59
59
  };
60
60
 
61
61
  const normalizeLifecycleInsertion = (input) => ({
62
- api_name: assertApiName(nonEmpty(input.api_name, "api_name")),
63
- file_path: nonEmpty(input.file_path, "file_path"),
64
- confidence: Math.max(0, Math.min(1, Number(input.confidence ?? 0))),
65
- reason: nonEmpty(input.reason, "reason"),
62
+ api_name: assertApiName(nonEmpty(input.api_name, "api_name")),
63
+ file_path: nonEmpty(input.file_path, "file_path"),
64
+ confidence: Math.max(0, Math.min(1, Number(input.confidence ?? 0))),
65
+ reason: nonEmpty(input.reason, "reason"),
66
66
  });
67
67
 
68
68
  const normalizePlan = (input) => {
69
- if (!input || typeof input !== "object") {
70
- throw new Error("AI lifecycle plan must be an object");
71
- }
72
- if (!Array.isArray(input.edits)) {
73
- throw new Error("AI lifecycle plan must include edits");
74
- }
75
- const edits = input.edits.map((edit) => ({
76
- file_path: nonEmpty(edit.file_path, "edit.file_path"),
77
- find: nonEmpty(edit.find, "edit.find"),
78
- replace: nonEmpty(edit.replace, "edit.replace"),
79
- }));
80
- const lifecycleInsertions = Array.isArray(input.lifecycle_insertions)
81
- ? input.lifecycle_insertions.map(normalizeLifecycleInsertion)
82
- : [];
83
- return {
84
- edits,
85
- lifecycleInsertions,
86
- warnings: Array.isArray(input.warnings)
87
- ? input.warnings
88
- .filter((warning) => typeof warning === "string" && warning.trim())
89
- .map((warning) => warning.trim())
90
- : [],
91
- };
69
+ if (!input || typeof input !== "object") {
70
+ throw new Error("AI lifecycle plan must be an object");
71
+ }
72
+ if (!Array.isArray(input.edits)) {
73
+ throw new Error("AI lifecycle plan must include edits");
74
+ }
75
+ const edits = input.edits.map((edit) => ({
76
+ file_path: nonEmpty(edit.file_path, "edit.file_path"),
77
+ find: nonEmpty(edit.find, "edit.find"),
78
+ replace: nonEmpty(edit.replace, "edit.replace"),
79
+ }));
80
+ const lifecycleInsertions = Array.isArray(input.lifecycle_insertions)
81
+ ? input.lifecycle_insertions.map(normalizeLifecycleInsertion)
82
+ : [];
83
+ return {
84
+ edits,
85
+ lifecycleInsertions,
86
+ warnings: Array.isArray(input.warnings)
87
+ ? input.warnings
88
+ .filter((warning) => typeof warning === "string" && warning.trim())
89
+ .map((warning) => warning.trim())
90
+ : [],
91
+ };
92
92
  };
93
93
 
94
94
  export const applyLifecyclePlan = async ({ repoRoot, plan: rawPlan }) => {
95
- const plan = normalizePlan(rawPlan);
96
- for (const edit of plan.edits) {
97
- const { absolutePath } = safeRelativePath(repoRoot, edit.file_path);
98
- assertNoForbiddenInstrumentation(edit.replace);
99
- const current = await readFile(absolutePath, "utf8");
100
- const occurrences = current.split(edit.find).length - 1;
101
- if (occurrences !== 1) {
102
- throw new Error(
103
- `edit.find must match exactly once in ${edit.file_path}; matched ${occurrences}`,
104
- );
105
- }
106
- assertLifecycleCallsAreGuarded(edit.replace);
107
- await writeFile(
108
- absolutePath,
109
- current.replace(edit.find, edit.replace),
110
- "utf8",
111
- );
112
- }
113
- return {
114
- lifecycleInsertions: plan.lifecycleInsertions,
115
- warnings: plan.warnings,
116
- };
95
+ const plan = normalizePlan(rawPlan);
96
+ for (const edit of plan.edits) {
97
+ const { absolutePath } = safeRelativePath(repoRoot, edit.file_path);
98
+ assertNoForbiddenInstrumentation(edit.replace);
99
+ const current = await readFile(absolutePath, "utf8");
100
+ const occurrences = current.split(edit.find).length - 1;
101
+ if (occurrences !== 1) {
102
+ throw new Error(
103
+ `edit.find must match exactly once in ${edit.file_path}; matched ${occurrences}`,
104
+ );
105
+ }
106
+ assertLifecycleCallsAreGuarded(edit.replace);
107
+ await writeFile(
108
+ absolutePath,
109
+ current.replace(edit.find, edit.replace),
110
+ "utf8",
111
+ );
112
+ }
113
+ return {
114
+ lifecycleInsertions: plan.lifecycleInsertions,
115
+ warnings: plan.warnings,
116
+ };
117
117
  };
118
118
 
119
119
  const readContextFiles = async ({ repoRoot, request }) => {
120
- const files = await listAllowedSourceFiles({
121
- repoRoot,
122
- allowedSourcePaths: request.allowed_source_paths,
123
- excludedSourcePaths: request.excluded_source_paths,
124
- extensions: SOURCE_EXTENSIONS,
125
- });
126
- const context = [];
127
- let totalChars = 0;
128
- for (const absolutePath of files.slice(0, MAX_CONTEXT_FILES)) {
129
- const text = await readFile(absolutePath, "utf8");
130
- const snippet = text.slice(0, MAX_FILE_CHARS);
131
- totalChars += snippet.length;
132
- if (totalChars > MAX_TOTAL_CHARS) {
133
- break;
134
- }
135
- context.push({
136
- file_path: relative(resolve(repoRoot), absolutePath),
137
- source: snippet,
138
- truncated: text.length > snippet.length,
139
- });
140
- }
141
- return context;
120
+ const files = await listAllowedSourceFiles({
121
+ repoRoot,
122
+ allowedSourcePaths: request.allowed_source_paths,
123
+ excludedSourcePaths: request.excluded_source_paths,
124
+ extensions: SOURCE_EXTENSIONS,
125
+ });
126
+ const context = [];
127
+ let totalChars = 0;
128
+ for (const absolutePath of files.slice(0, MAX_CONTEXT_FILES)) {
129
+ const text = await readFile(absolutePath, "utf8");
130
+ const snippet = text.slice(0, MAX_FILE_CHARS);
131
+ totalChars += snippet.length;
132
+ if (totalChars > MAX_TOTAL_CHARS) {
133
+ break;
134
+ }
135
+ context.push({
136
+ file_path: relative(resolve(repoRoot), absolutePath),
137
+ source: snippet,
138
+ truncated: text.length > snippet.length,
139
+ });
140
+ }
141
+ return context;
142
142
  };
143
143
 
144
144
  const buildLifecyclePrompt = ({ request, files }) =>
145
- JSON.stringify({
146
- task: "Add Clue SDK lifecycle API calls to this repository using exact text replacements.",
147
- rules: [
148
- "Return JSON only.",
149
- "Use only exact replacements. Each find string must be copied exactly from source.",
150
- "Add ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout where repository code has clear lifecycle points.",
151
- "Every Clue lifecycle call must be failure-isolated. If Clue fails, the host service must continue without throwing, rejecting, or blocking login/API behavior.",
152
- "Use a small safe Clue helper, local try/catch/try/except wrapper, or direct .catch handler around Clue lifecycle calls.",
153
- "Never await a Clue lifecycle call in a way that can block login, logout, account selection, request handling, page rendering, or API responses.",
154
- "Find all clear login success paths and add ClueIdentify to every one of them. Do not stop after the first login flow.",
155
- "Find all clear account, workspace, organization, or tenant resolution paths and add ClueSetAccount to every one of them.",
156
- "Find all clear logout or session reset paths and add ClueLogout to every one of them.",
157
- "Inspect backend lifecycle points as carefully as frontend lifecycle points. Backend login/session/account code is especially important.",
158
- "For FastAPI backends, add the clue-fastapi-sdk dependency when missing, import clue_init_fastapi plus ClueIdentify/ClueSetAccount/ClueLogout where needed, and initialize the SDK at FastAPI app creation.",
159
- "For Django backends, add the clue-django-sdk dependency when missing, import the Django SDK lifecycle helpers where needed, and initialize the SDK in the Django integration point.",
160
- "For other backend frameworks, use the matching Clue backend SDK if one exists; if no backend SDK exists, report a blocker instead of silently frontend-only setup.",
161
- "Do not add broad ClueTrack instrumentation.",
162
- "Do not add data-clue-id, data-clue-key, or similar DOM tags.",
163
- "Do not create route semantics files or layer files.",
164
- "Do not copy project keys, API keys, service secrets, or environment-specific values into code.",
165
- "Do not send raw email, raw person names, tokens, workspace names, organization names, or tenant names as lifecycle traits unless the repository already has an explicit Clue privacy policy allowing them.",
166
- "Prefer stable ids and non-PII booleans/counts for ClueIdentify and ClueSetAccount traits.",
167
- "Use environment variable names for Clue configuration values.",
168
- "For Python/FastAPI code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_API_KEY, and CLUE_INGEST_ENDPOINT from environment variables.",
169
- "For browser code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, and CLUE_INGEST_ENDPOINT from environment variables through the target framework's safe client config mechanism. Do not hard-code a Next.js-only prefix.",
170
- "Prefer minimal edits that engineers can review in one PR.",
171
- "If a lifecycle point is unclear, skip that edit and include a warning.",
172
- ],
173
- repository_context: {
174
- target_tool: request.target_tool,
175
- framework: request.framework,
176
- project_key_env: "CLUE_PROJECT_KEY",
177
- browser_project_key_env: "CLUE_PROJECT_KEY",
178
- environment_env: "CLUE_ENVIRONMENT",
179
- browser_environment_env: "CLUE_ENVIRONMENT",
180
- clue_api_base_url_env: "CLUE_API_BASE_URL",
181
- clue_ingest_endpoint_env: "CLUE_INGEST_ENDPOINT",
182
- browser_ingest_endpoint_env: "CLUE_INGEST_ENDPOINT",
183
- service_key: request.service_key,
184
- },
185
- output_shape: {
186
- edits: [
187
- {
188
- file_path: "app/main.py",
189
- find: "exact original text",
190
- replace: "exact replacement text",
191
- },
192
- ],
193
- lifecycle_insertions: [
194
- {
195
- api_name: "ClueInit",
196
- file_path: "app/main.py",
197
- confidence: 0.8,
198
- reason: "SDK initialized where FastAPI app is created.",
199
- },
200
- ],
201
- warnings: ["short engineer review note"],
202
- },
203
- files,
204
- });
145
+ JSON.stringify({
146
+ task: "Add Clue SDK lifecycle API calls to this repository using exact text replacements.",
147
+ rules: [
148
+ "Return JSON only.",
149
+ "Use only exact replacements. Each find string must be copied exactly from source.",
150
+ "Add ClueInit, ClueIdentify, ClueSetAccount, and ClueLogout where repository code has clear lifecycle points.",
151
+ "Every Clue lifecycle call must be failure-isolated. If Clue fails, the host service must continue without throwing, rejecting, or blocking login/API behavior.",
152
+ "Use a small safe Clue helper, local try/catch/try/except wrapper, or direct .catch handler around Clue lifecycle calls.",
153
+ "Never await a Clue lifecycle call in a way that can block login, logout, account selection, request handling, page rendering, or API responses.",
154
+ "Find all clear login success paths and add ClueIdentify to every one of them. Do not stop after the first login flow.",
155
+ "Find all clear account, workspace, organization, or tenant resolution paths and add ClueSetAccount to every one of them.",
156
+ "Find all clear logout or session reset paths and add ClueLogout to every one of them.",
157
+ "Inspect backend lifecycle points as carefully as frontend lifecycle points. Backend login/session/account code is especially important.",
158
+ "For FastAPI backends, add the clue-fastapi-sdk dependency when missing, import clue_init_fastapi plus ClueIdentify/ClueSetAccount/ClueLogout where needed, and initialize the SDK at FastAPI app creation.",
159
+ "For Django backends, add the clue-django-sdk dependency when missing, import the Django SDK lifecycle helpers where needed, and initialize the SDK in the Django integration point.",
160
+ "For other backend frameworks, use the matching Clue backend SDK if one exists; if no backend SDK exists, report a blocker instead of silently frontend-only setup.",
161
+ "Do not add broad ClueTrack instrumentation.",
162
+ "Do not add data-clue-id, data-clue-key, or similar DOM tags.",
163
+ "Do not create route semantics files or layer files.",
164
+ "Do not copy project keys, API keys, service secrets, or environment-specific values into code.",
165
+ "Do not send raw email, raw person names, tokens, workspace names, organization names, or tenant names as lifecycle traits unless the repository already has an explicit Clue privacy policy allowing them.",
166
+ "Prefer stable ids and non-PII booleans/counts for ClueIdentify and ClueSetAccount traits.",
167
+ "Use environment variable names for Clue configuration values.",
168
+ "For Python/FastAPI code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_API_KEY, and CLUE_INGEST_ENDPOINT from environment variables.",
169
+ "For browser code, read CLUE_PROJECT_KEY, CLUE_ENVIRONMENT, CLUE_SERVICE_KEY, and CLUE_INGEST_ENDPOINT from environment variables through the target framework's safe client config mechanism. Do not hard-code a Next.js-only prefix.",
170
+ "Never place CLUE_API_KEY in frontend code, frontend env files, browser bundles, or client-readable config.",
171
+ "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.",
172
+ "Configure frontend ClueInit with browserTokenProvider that calls the local backend token endpoint and returns the token string.",
173
+ "The browser token request must include project key, environment, service key, and the current browser origin; the backend must attach x-clue-api-key server-side when calling Clue.",
174
+ "Prefer minimal edits that engineers can review in one PR.",
175
+ "If a lifecycle point is unclear, skip that edit and include a warning.",
176
+ ],
177
+ repository_context: {
178
+ target_tool: request.target_tool,
179
+ framework: request.framework,
180
+ project_key_env: "CLUE_PROJECT_KEY",
181
+ browser_project_key_env: "CLUE_PROJECT_KEY",
182
+ environment_env: "CLUE_ENVIRONMENT",
183
+ browser_environment_env: "CLUE_ENVIRONMENT",
184
+ clue_api_base_url_env: "CLUE_API_BASE_URL",
185
+ clue_ingest_endpoint_env: "CLUE_INGEST_ENDPOINT",
186
+ browser_ingest_endpoint_env: "CLUE_INGEST_ENDPOINT",
187
+ browser_token_endpoint_path: "/api/v1/ingest/browser-tokens",
188
+ service_key: request.service_key,
189
+ },
190
+ output_shape: {
191
+ edits: [
192
+ {
193
+ file_path: "app/main.py",
194
+ find: "exact original text",
195
+ replace: "exact replacement text",
196
+ },
197
+ ],
198
+ lifecycle_insertions: [
199
+ {
200
+ api_name: "ClueInit",
201
+ file_path: "app/main.py",
202
+ confidence: 0.8,
203
+ reason: "SDK initialized where FastAPI app is created.",
204
+ },
205
+ ],
206
+ warnings: ["short engineer review note"],
207
+ },
208
+ files,
209
+ });
205
210
 
206
211
  export const planLifecycleInsertions = async ({ repoRoot, request, env }) => {
207
- const apiKey = env.CLUE_AI_PROVIDER_API_KEY;
208
- if (!apiKey) {
209
- throw new Error(
210
- "CLUE_AI_PROVIDER_API_KEY is required for lifecycle API insertion",
211
- );
212
- }
213
- const files = await readContextFiles({ repoRoot, request });
214
- return callJsonAiProvider({
215
- config: resolveAiProviderConfig({ env, apiKey }),
216
- system:
217
- "You are a safe code-edit planner for Clue SDK initialization. Return schema-valid JSON only.",
218
- user: buildLifecyclePrompt({ request, files }),
219
- toolName: "return_lifecycle_plan",
220
- toolDescription: "Return the Clue SDK lifecycle insertion plan.",
221
- failureMessage: "AI provider failed during lifecycle planning",
222
- emptyMessage: "AI provider returned empty lifecycle plan",
223
- });
212
+ const apiKey = env.CLUE_AI_PROVIDER_API_KEY;
213
+ if (!apiKey) {
214
+ throw new Error(
215
+ "CLUE_AI_PROVIDER_API_KEY is required for lifecycle API insertion",
216
+ );
217
+ }
218
+ const files = await readContextFiles({ repoRoot, request });
219
+ return callJsonAiProvider({
220
+ config: resolveAiProviderConfig({ env, apiKey }),
221
+ system:
222
+ "You are a safe code-edit planner for Clue SDK initialization. Return schema-valid JSON only.",
223
+ user: buildLifecyclePrompt({ request, files }),
224
+ toolName: "return_lifecycle_plan",
225
+ toolDescription: "Return the Clue SDK lifecycle insertion plan.",
226
+ failureMessage: "AI provider failed during lifecycle planning",
227
+ emptyMessage: "AI provider returned empty lifecycle plan",
228
+ });
224
229
  };
@@ -76,6 +76,10 @@ const clueInitToolReportSchema = zod_1.z.object({
76
76
  semantic_generation_timing: zod_1.z.literal("after_merge_ci"),
77
77
  semantic_preview_generated: zod_1.z.literal(false),
78
78
  client_repo_generated_files_committed: zod_1.z.literal(false),
79
+ frontend_api_key_exposed: zod_1.z.literal(false),
80
+ browser_token_endpoint_required: zod_1.z.literal(true),
81
+ browser_token_provider_required: zod_1.z.literal(true),
82
+ browser_token_endpoint_path: nonEmptyStringSchema,
79
83
  clue_track_inserted: zod_1.z.literal(false),
80
84
  clue_dom_tag_inserted: zod_1.z.literal(false),
81
85
  allowed_source_paths: zod_1.z.array(nonEmptyStringSchema),
@@ -440,10 +440,12 @@ export const runSetupCheck = async ({
440
440
  checks,
441
441
  "sdk_lifecycle",
442
442
  sdkLifecycle.passed,
443
- "SDK lifecycle calls resolve to real source code instead of no-op globals",
443
+ "static SDK lifecycle references are present and guarded; dependency install, import, app startup, and event delivery are not verified by this check",
444
444
  {
445
445
  found_apis: sdkLifecycle.foundApis,
446
446
  missing_apis: sdkLifecycle.missingApis,
447
+ verification_scope:
448
+ "static_source_only_not_dependency_install_or_runtime_event_delivery",
447
449
  backend_lifecycle: sdkLifecycle.backend_lifecycle,
448
450
  has_noop_wrapper: sdkLifecycle.has_noop_wrapper,
449
451
  component_lifecycle_init_files:
@@ -10,6 +10,62 @@ const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
10
10
  const DEFAULT_ENV_GUIDE_PATH = ".env.clue";
11
11
  const BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
12
12
  const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
13
+ const AI_PROVIDER_GUIDES = {
14
+ codex: {
15
+ provider: "openai",
16
+ apiKeyValue: "<openai-api-key>",
17
+ model: "gpt-5.4-mini",
18
+ secretComments: [
19
+ "Codexを選んだ場合はOpenAIのAPI keyを使います。",
20
+ "https://platform.openai.com/api-keys で作成して、GitHub Secretに保存してください。",
21
+ "ここにはClue API keyではなく、OpenAI API keyを入れてください。",
22
+ ],
23
+ providerComments: [
24
+ "setup targetがcodexのため、providerはopenaiで自動設定しています。",
25
+ ],
26
+ modelComments: [
27
+ "まず迷ったら gpt-5.4-mini を使ってください。",
28
+ "精度を優先する場合の例: gpt-5.5",
29
+ "コストを優先する場合の例: gpt-5.4-nano",
30
+ ],
31
+ },
32
+ claude_code: {
33
+ provider: "anthropic",
34
+ apiKeyValue: "<anthropic-api-key>",
35
+ model: "claude-sonnet-4-6",
36
+ secretComments: [
37
+ "Claude Codeを選んだ場合はAnthropicのAPI keyを使います。",
38
+ "https://console.anthropic.com/settings/keys で作成して、GitHub Secretに保存してください。",
39
+ "ここにはClue API keyではなく、Anthropic API keyを入れてください。",
40
+ ],
41
+ providerComments: [
42
+ "setup targetがclaude_codeのため、providerはanthropicで自動設定しています。",
43
+ ],
44
+ modelComments: [
45
+ "まず迷ったら claude-sonnet-4-6 を使ってください。",
46
+ "精度を優先する場合の例: claude-opus-4-7",
47
+ "コストを優先する場合の例: claude-haiku-4-5-20251001",
48
+ ],
49
+ },
50
+ };
51
+ const GENERIC_AI_PROVIDER_GUIDE = {
52
+ provider: "<openai-or-anthropic>",
53
+ apiKeyValue: "<provider-api-key>",
54
+ model: "<provider-model>",
55
+ secretComments: [
56
+ "OpenAIまたはAnthropicのAPI keyを作成し、GitHub Secretに保存してください。",
57
+ "OpenAI: https://platform.openai.com/api-keys",
58
+ "Anthropic: https://console.anthropic.com/settings/keys",
59
+ "ここにはClue API keyではなく、選択したAI providerのAPI keyを入れてください。",
60
+ ],
61
+ providerComments: [
62
+ "codexを使う場合はopenai、claude_codeを使う場合はanthropicを指定してください。",
63
+ ],
64
+ modelComments: [
65
+ "OpenAIの例: gpt-5.4-mini / gpt-5.5 / gpt-5.4-nano",
66
+ "Anthropicの例: claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5-20251001",
67
+ ],
68
+ };
13
69
 
14
70
  const writeJson = async ({ repoRoot, path, value }) => {
15
71
  const absolutePath = join(resolve(repoRoot), path);
@@ -88,6 +144,9 @@ const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
88
144
 
89
145
  const buildEndpoint = (baseUrl, path) => `${trimTrailingSlash(baseUrl)}${path}`;
90
146
 
147
+ const aiProviderGuideForTarget = (target) =>
148
+ AI_PROVIDER_GUIDES[target] ?? GENERIC_AI_PROVIDER_GUIDE;
149
+
91
150
  const setupContextFromInput = (input = {}) => ({
92
151
  clue_api_key: optionalString(input.clueApiKey),
93
152
  clue_api_base_url: optionalString(input.clueApiBaseUrl),
@@ -140,6 +199,7 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
140
199
  }
141
200
 
142
201
  const watchTargets = manifest.lifecycle_verification.watch_targets;
202
+ const aiProviderGuide = aiProviderGuideForTarget(manifest.target);
143
203
  return {
144
204
  status: "ready",
145
205
  env_file_path: DEFAULT_ENV_GUIDE_PATH,
@@ -157,17 +217,33 @@ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
157
217
  { name: "CLUE_API_KEY", value: setupContext.clue_api_key },
158
218
  {
159
219
  name: "CLUE_AI_PROVIDER_API_KEY",
160
- value: "<openai-or-anthropic-api-key>",
220
+ value: aiProviderGuide.apiKeyValue,
221
+ comments: aiProviderGuide.secretComments,
161
222
  },
162
223
  ],
163
224
  variables: [
164
- { name: "CLUE_AI_PROVIDER", value: "<openai-or-anthropic>" },
165
- { name: "CLUE_AI_MODEL", value: "<selected-provider-model>" },
225
+ {
226
+ name: "CLUE_AI_PROVIDER",
227
+ value: aiProviderGuide.provider,
228
+ comments: aiProviderGuide.providerComments,
229
+ },
230
+ {
231
+ name: "CLUE_AI_MODEL",
232
+ value: aiProviderGuide.model,
233
+ comments: aiProviderGuide.modelComments,
234
+ },
166
235
  ],
167
236
  },
168
237
  };
169
238
  };
170
239
 
240
+ const renderEnvironmentEntry = (entry) => [
241
+ ...(Array.isArray(entry.comments)
242
+ ? entry.comments.map((comment) => `# ${comment}`)
243
+ : []),
244
+ `${entry.name}=${entry.value}`,
245
+ ];
246
+
171
247
  const buildEnvironmentGuideText = (instructions) => {
172
248
  if (!instructions || instructions.status !== "ready") return null;
173
249
  const lines = [
@@ -186,17 +262,13 @@ const buildEnvironmentGuideText = (instructions) => {
186
262
  }
187
263
  lines.push(
188
264
  "GitHub Secrets",
189
- ...instructions.ci_github.secrets.map(
190
- (entry) => `${entry.name}=${entry.value}`,
191
- ),
265
+ ...instructions.ci_github.secrets.flatMap(renderEnvironmentEntry),
192
266
  "",
193
267
  );
194
268
  if (instructions.ci_github.variables.length > 0) {
195
269
  lines.push(
196
270
  "GitHub Variables",
197
- ...instructions.ci_github.variables.map(
198
- (entry) => `${entry.name}=${entry.value}`,
199
- ),
271
+ ...instructions.ci_github.variables.flatMap(renderEnvironmentEntry),
200
272
  "",
201
273
  );
202
274
  }