@clue-ai/cli 0.0.5 → 0.0.7

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.
@@ -1,170 +1,384 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import {
4
- buildSemanticWorkflowRequestFromFlags,
5
- writeSemanticWorkflow,
4
+ buildSemanticWorkflowRequestFromFlags,
5
+ writeSemanticWorkflow,
6
6
  } from "./init-tool.mjs";
7
7
  import { runSetupDetect } from "./setup-detect.mjs";
8
8
 
9
9
  const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
10
+ const DEFAULT_ENV_GUIDE_PATH = ".env.clue";
11
+ const BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
12
+ const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
10
13
 
11
14
  const writeJson = async ({ repoRoot, path, value }) => {
12
- const absolutePath = join(resolve(repoRoot), path);
13
- await mkdir(dirname(absolutePath), { recursive: true });
14
- await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
15
+ const absolutePath = join(resolve(repoRoot), path);
16
+ await mkdir(dirname(absolutePath), { recursive: true });
17
+ await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
18
+ };
19
+
20
+ const writeText = async ({ repoRoot, path, value }) => {
21
+ const absolutePath = join(resolve(repoRoot), path);
22
+ await mkdir(dirname(absolutePath), { recursive: true });
23
+ await writeFile(absolutePath, value, "utf8");
15
24
  };
16
25
 
17
26
  const firstCandidateOrBlocker = (detection) => {
18
- if (!detection.detected || detection.candidates.length === 0) {
19
- return {
20
- candidate: null,
21
- blockers: detection.blockers.length
22
- ? detection.blockers
23
- : [
24
- {
25
- code: "NO_SETUP_CANDIDATE",
26
- message: "No setup candidate was mechanically detected.",
27
- },
28
- ],
29
- };
30
- }
31
- return {
32
- candidate: detection.candidates[0],
33
- blockers: [],
34
- };
27
+ if (!detection.detected || detection.candidates.length === 0) {
28
+ return {
29
+ candidate: null,
30
+ blockers: detection.blockers.length
31
+ ? detection.blockers
32
+ : [
33
+ {
34
+ code: "NO_SETUP_CANDIDATE",
35
+ message: "No setup candidate was mechanically detected.",
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ return {
41
+ candidate: detection.candidates[0],
42
+ blockers: [],
43
+ };
35
44
  };
36
45
 
37
46
  const DEFAULT_SETUP_LIFECYCLE = [
38
- "init",
39
- "identify",
40
- "set-account",
41
- "logout",
42
- "event-sent",
47
+ "init",
48
+ "identify",
49
+ "set-account",
50
+ "logout",
51
+ "event-sent",
43
52
  ];
44
53
 
45
54
  const buildWatchTargets = (detection, fallbackCandidate) => {
46
- const frontendServices = Array.isArray(detection.services?.frontend)
47
- ? detection.services.frontend
48
- : [];
49
- const backendServices = Array.isArray(detection.services?.backend)
50
- ? detection.services.backend
51
- : [
52
- {
53
- kind: "backend",
54
- framework: fallbackCandidate.framework,
55
- root_path: fallbackCandidate.backend_root_path,
56
- service_key: fallbackCandidate.service_key,
57
- local_url_candidates: [],
58
- },
59
- ];
60
- return [...frontendServices, ...backendServices].map((service) => ({
61
- kind: service.kind,
62
- framework: service.framework,
63
- root_path: service.root_path,
64
- service_key: service.service_key,
65
- producer_id: service.service_key,
66
- expected_lifecycle: DEFAULT_SETUP_LIFECYCLE,
67
- local_url_candidates: service.local_url_candidates ?? [],
68
- url_env_name:
69
- service.kind === "frontend"
70
- ? `CLUE_LOCAL_${service.service_key.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_FRONTEND_URL`
71
- : `CLUE_LOCAL_${service.service_key.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_BACKEND_URL`,
72
- }));
55
+ const frontendServices = Array.isArray(detection.services?.frontend)
56
+ ? detection.services.frontend
57
+ : [];
58
+ const backendServices = Array.isArray(detection.services?.backend)
59
+ ? detection.services.backend
60
+ : [
61
+ {
62
+ kind: "backend",
63
+ framework: fallbackCandidate.framework,
64
+ root_path: fallbackCandidate.backend_root_path,
65
+ service_key: fallbackCandidate.service_key,
66
+ local_url_candidates: [],
67
+ },
68
+ ];
69
+ return [...frontendServices, ...backendServices].map((service) => ({
70
+ kind: service.kind,
71
+ framework: service.framework,
72
+ root_path: service.root_path,
73
+ service_key: service.service_key,
74
+ producer_id: service.service_key,
75
+ expected_lifecycle: DEFAULT_SETUP_LIFECYCLE,
76
+ local_url_candidates: service.local_url_candidates ?? [],
77
+ url_env_name:
78
+ service.kind === "frontend"
79
+ ? `CLUE_LOCAL_${service.service_key.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_FRONTEND_URL`
80
+ : `CLUE_LOCAL_${service.service_key.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_BACKEND_URL`,
81
+ }));
82
+ };
83
+
84
+ const optionalString = (value) =>
85
+ typeof value === "string" && value.trim() ? value.trim() : null;
86
+
87
+ const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
88
+
89
+ const buildEndpoint = (baseUrl, path) => `${trimTrailingSlash(baseUrl)}${path}`;
90
+
91
+ const setupContextFromInput = (input = {}) => ({
92
+ clue_api_key: optionalString(input.clueApiKey),
93
+ clue_api_base_url: optionalString(input.clueApiBaseUrl),
94
+ project_key: optionalString(input.projectKey),
95
+ environment: optionalString(input.environment),
96
+ });
97
+
98
+ const envFileCandidates = (target) => {
99
+ if (target.kind === "frontend") {
100
+ return target.framework === "nextjs" ? [".env.local"] : [".env"];
101
+ }
102
+ return [".env"];
103
+ };
104
+
105
+ const buildServiceEnvBlock = ({ target, setupContext }) => {
106
+ const ingestPath =
107
+ target.kind === "frontend" ? BROWSER_INGEST_PATH : BACKEND_INGEST_PATH;
108
+ const variables = [
109
+ {
110
+ name: "CLUE_INGEST_ENDPOINT",
111
+ value: buildEndpoint(setupContext.clue_api_base_url, ingestPath),
112
+ },
113
+ { name: "CLUE_PROJECT_KEY", value: setupContext.project_key },
114
+ { name: "CLUE_ENVIRONMENT", value: setupContext.environment },
115
+ { name: "CLUE_SERVICE_KEY", value: target.service_key },
116
+ ];
117
+ if (target.kind === "backend") {
118
+ variables.push({ name: "CLUE_API_KEY", value: setupContext.clue_api_key });
119
+ }
120
+ return variables.map(({ name, value }) => `${name}=${value}`).join("\n");
121
+ };
122
+
123
+ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
124
+ const missingFlags = [
125
+ ["clue_api_key", "--clue-api-key"],
126
+ ["clue_api_base_url", "--clue-api-base-url"],
127
+ ["project_key", "--project-key"],
128
+ ["environment", "--environment"],
129
+ ]
130
+ .filter(([key]) => !setupContext[key])
131
+ .map(([, flag]) => flag);
132
+
133
+ if (missingFlags.length > 0 || manifest.status !== "ready_for_ai") {
134
+ return {
135
+ status: "missing_setup_arguments",
136
+ required_flags: missingFlags,
137
+ message:
138
+ "Run setup with Clue values from the setup screen to print service-specific env blocks.",
139
+ };
140
+ }
141
+
142
+ const watchTargets = manifest.lifecycle_verification.watch_targets;
143
+ return {
144
+ status: "ready",
145
+ env_file_path: DEFAULT_ENV_GUIDE_PATH,
146
+ message: "各サービスの env ファイルに以下を設定してください。",
147
+ service_env_blocks: watchTargets.map((target) => ({
148
+ kind: target.kind,
149
+ framework: target.framework,
150
+ root_path: target.root_path,
151
+ service_key: target.service_key,
152
+ env_file_candidates: envFileCandidates(target),
153
+ env_block: buildServiceEnvBlock({ target, setupContext }),
154
+ })),
155
+ ci_github: {
156
+ secrets: [
157
+ { name: "CLUE_API_KEY", value: setupContext.clue_api_key },
158
+ {
159
+ name: "CLUE_AI_PROVIDER_API_KEY",
160
+ value: "<openai-or-anthropic-api-key>",
161
+ },
162
+ ],
163
+ variables: [
164
+ { name: "CLUE_AI_PROVIDER", value: "<openai-or-anthropic>" },
165
+ { name: "CLUE_AI_MODEL", value: "<selected-provider-model>" },
166
+ ],
167
+ },
168
+ };
169
+ };
170
+
171
+ const buildEnvironmentGuideText = (instructions) => {
172
+ if (!instructions || instructions.status !== "ready") return null;
173
+ const lines = [
174
+ "# Clue setup environment values",
175
+ "# This file contains setup secrets. Do not commit it.",
176
+ "",
177
+ instructions.message,
178
+ "",
179
+ ];
180
+ for (const block of instructions.service_env_blocks) {
181
+ lines.push(
182
+ `[${block.kind}] ${block.root_path} (${block.env_file_candidates.join(" or ")})`,
183
+ block.env_block,
184
+ "",
185
+ );
186
+ }
187
+ lines.push(
188
+ "GitHub Secrets",
189
+ ...instructions.ci_github.secrets.map(
190
+ (entry) => `${entry.name}=${entry.value}`,
191
+ ),
192
+ "",
193
+ );
194
+ if (instructions.ci_github.variables.length > 0) {
195
+ lines.push(
196
+ "GitHub Variables",
197
+ ...instructions.ci_github.variables.map(
198
+ (entry) => `${entry.name}=${entry.value}`,
199
+ ),
200
+ "",
201
+ );
202
+ }
203
+ return `${lines.join("\n")}\n`;
204
+ };
205
+
206
+ const summarizeEnvironmentInstructions = (instructions) => {
207
+ if (!instructions || instructions.status !== "ready") {
208
+ return instructions;
209
+ }
210
+ return {
211
+ status: "ready",
212
+ env_file_path: instructions.env_file_path,
213
+ message:
214
+ `${instructions.env_file_path} を開き、各サービスの env と GitHub Secrets に反映してください。`,
215
+ service_env_block_count: instructions.service_env_blocks.length,
216
+ github_secret_names: instructions.ci_github.secrets.map(
217
+ (entry) => entry.name,
218
+ ),
219
+ github_variable_names: instructions.ci_github.variables.map(
220
+ (entry) => entry.name,
221
+ ),
222
+ };
223
+ };
224
+
225
+ const writeEnvironmentGuideIfReady = async ({ repoRoot, instructions }) => {
226
+ const content = buildEnvironmentGuideText(instructions);
227
+ if (!content) return null;
228
+ await writeText({
229
+ repoRoot,
230
+ path: instructions.env_file_path,
231
+ value: content,
232
+ });
233
+ return instructions.env_file_path;
73
234
  };
74
235
 
75
236
  export const runSetupPrepare = async ({
76
- repoRoot,
77
- target,
78
- skillRoot,
79
- setupManifestPath = DEFAULT_SETUP_MANIFEST_PATH,
237
+ repoRoot,
238
+ target,
239
+ skillRoot,
240
+ setupContext: setupContextInput,
241
+ setupManifestPath = DEFAULT_SETUP_MANIFEST_PATH,
80
242
  }) => {
81
- const resolvedRepoRoot = resolve(repoRoot ?? ".");
82
- const detection = await runSetupDetect({ repoRoot: resolvedRepoRoot });
83
- const { candidate, blockers } = firstCandidateOrBlocker(detection);
84
- if (!candidate) {
85
- const manifest = {
86
- status: "blocked",
87
- target,
88
- skill_root: skillRoot,
89
- blockers,
90
- detection,
91
- ai_next_scope: "blocked_until_backend_routes_are_detected",
92
- machine_owned_artifacts: [],
93
- ai_owned_workstreams: [
94
- "sdk_lifecycle_implementation_after_blockers_are_resolved",
95
- ],
96
- };
97
- await writeJson({
98
- repoRoot: resolvedRepoRoot,
99
- path: setupManifestPath,
100
- value: manifest,
101
- });
102
- return manifest;
103
- }
104
-
105
- const request = buildSemanticWorkflowRequestFromFlags({
106
- framework: candidate.framework,
107
- backendRootPath: candidate.backend_root_path,
108
- serviceKey: candidate.service_key,
109
- });
110
- const workflow = await writeSemanticWorkflow({
111
- repoRoot: resolvedRepoRoot,
112
- request,
113
- });
114
-
115
- const manifest = {
116
- status: "ready_for_ai",
117
- target,
118
- skill_root: skillRoot,
119
- detected: {
120
- framework: candidate.framework,
121
- backend_root_path: candidate.backend_root_path,
122
- service_key: candidate.service_key,
123
- },
124
- service_identity: {
125
- canonical_field: "service_key",
126
- backend_env_name: "CLUE_SERVICE_KEY",
127
- frontend_env_name: "NEXT_PUBLIC_CLUE_SERVICE_KEY",
128
- producer_id_derivation: "producer_id defaults to service_key",
129
- },
130
- lifecycle_verification: {
131
- watch_target_format:
132
- "frontend:<service-key>[init,identify,set-account,logout,event-sent]=<frontend-url>,backend:<service-key>[init,identify,set-account,logout,event-sent]=<backend-url>",
133
- rule:
134
- "setup-watch --local uses the structured watch_targets below. Lifecycle checks are fixed for setup verification and are evaluated per service_key.",
135
- watch_targets: buildWatchTargets(detection, candidate),
136
- },
137
- artifacts: {
138
- ci_workflow_path: workflow.ci_workflow_path,
139
- setup_manifest_path: setupManifestPath,
140
- runtime_request_committed: false,
141
- },
142
- machine_owned_artifacts: [workflow.ci_workflow_path, setupManifestPath],
143
- ai_must_not_edit: [workflow.ci_workflow_path],
144
- ai_owned_workstreams: ["sdk_lifecycle_implementation"],
145
- required_final_check: {
146
- command:
147
- `npx @clue-ai/cli setup-check --framework ${candidate.framework} ` +
148
- `--backend-root-path ${candidate.backend_root_path} --repo . --target ${target} --require-sdk-lifecycle`,
149
- },
150
- required_env_names: [
151
- "NEXT_PUBLIC_CLUE_SERVICE_KEY",
152
- "NEXT_PUBLIC_CLUE_PROJECT_KEY",
153
- "NEXT_PUBLIC_CLUE_ENVIRONMENT",
154
- "NEXT_PUBLIC_CLUE_INGEST_ENDPOINT",
155
- "CLUE_SERVICE_KEY",
156
- "CLUE_API_KEY",
157
- "AI_PROVIDER_API_KEY",
158
- "CLUE_PROJECT_KEY",
159
- "CLUE_ENVIRONMENT",
160
- "CLUE_API_BASE_URL",
161
- "CLUE_AI_MODEL",
162
- ],
163
- };
164
- await writeJson({
165
- repoRoot: resolvedRepoRoot,
166
- path: setupManifestPath,
167
- value: manifest,
168
- });
169
- return manifest;
243
+ const resolvedRepoRoot = resolve(repoRoot ?? ".");
244
+ const setupContext = setupContextFromInput(setupContextInput);
245
+ const detection = await runSetupDetect({ repoRoot: resolvedRepoRoot });
246
+ const { candidate, blockers } = firstCandidateOrBlocker(detection);
247
+ if (!candidate) {
248
+ const manifest = {
249
+ status: "blocked",
250
+ target,
251
+ skill_root: skillRoot,
252
+ blockers,
253
+ detection,
254
+ ai_next_scope: "blocked_until_backend_routes_are_detected",
255
+ machine_owned_artifacts: [],
256
+ ai_owned_workstreams: [
257
+ "sdk_lifecycle_implementation_after_blockers_are_resolved",
258
+ ],
259
+ };
260
+ await writeJson({
261
+ repoRoot: resolvedRepoRoot,
262
+ path: setupManifestPath,
263
+ value: manifest,
264
+ });
265
+ const environmentInstructions = buildEnvironmentInstructions({
266
+ manifest,
267
+ setupContext,
268
+ });
269
+ return {
270
+ ...manifest,
271
+ environment_instructions:
272
+ summarizeEnvironmentInstructions(environmentInstructions),
273
+ };
274
+ }
275
+
276
+ const request = buildSemanticWorkflowRequestFromFlags({
277
+ framework: candidate.framework,
278
+ backendRootPath: candidate.backend_root_path,
279
+ serviceKey: candidate.service_key,
280
+ projectKey: setupContext.project_key,
281
+ environment: setupContext.environment,
282
+ clueApiBaseUrl: setupContext.clue_api_base_url,
283
+ });
284
+ const workflow = await writeSemanticWorkflow({
285
+ repoRoot: resolvedRepoRoot,
286
+ request,
287
+ });
288
+
289
+ const manifest = {
290
+ status: "ready_for_ai",
291
+ target,
292
+ skill_root: skillRoot,
293
+ detected: {
294
+ framework: candidate.framework,
295
+ backend_root_path: candidate.backend_root_path,
296
+ service_key: candidate.service_key,
297
+ },
298
+ service_identity: {
299
+ canonical_field: "service_key",
300
+ backend_env_name: "CLUE_SERVICE_KEY",
301
+ frontend_env_name: "CLUE_SERVICE_KEY",
302
+ producer_id_derivation: "producer_id defaults to service_key",
303
+ },
304
+ clue_context: {
305
+ project_key: setupContext.project_key,
306
+ environment: setupContext.environment,
307
+ clue_api_base_url: setupContext.clue_api_base_url,
308
+ ingest_endpoints: setupContext.clue_api_base_url
309
+ ? {
310
+ browser: buildEndpoint(
311
+ setupContext.clue_api_base_url,
312
+ BROWSER_INGEST_PATH,
313
+ ),
314
+ backend: buildEndpoint(
315
+ setupContext.clue_api_base_url,
316
+ BACKEND_INGEST_PATH,
317
+ ),
318
+ }
319
+ : null,
320
+ },
321
+ lifecycle_verification: {
322
+ watch_target_format:
323
+ "frontend:<service-key>[init,identify,set-account,logout,event-sent]=<frontend-url>,backend:<service-key>[init,identify,set-account,logout,event-sent]=<backend-url>",
324
+ rule: "setup-watch --local uses the structured watch_targets below. Lifecycle checks are fixed for setup verification and are evaluated per service_key.",
325
+ watch_targets: buildWatchTargets(detection, candidate),
326
+ },
327
+ artifacts: {
328
+ ci_workflow_path: workflow.ci_workflow_path,
329
+ setup_manifest_path: setupManifestPath,
330
+ runtime_request_committed: false,
331
+ },
332
+ machine_owned_artifacts: [workflow.ci_workflow_path, setupManifestPath],
333
+ ai_must_not_edit: [workflow.ci_workflow_path],
334
+ ai_owned_workstreams: ["sdk_lifecycle_implementation"],
335
+ required_final_check: {
336
+ command:
337
+ `npx @clue-ai/cli setup-check --framework ${candidate.framework} ` +
338
+ `--backend-root-path ${candidate.backend_root_path} --repo . --target ${target} --require-sdk-lifecycle`,
339
+ },
340
+ required_env_names: [
341
+ "CLUE_SERVICE_KEY",
342
+ "CLUE_API_KEY",
343
+ "CLUE_AI_PROVIDER",
344
+ "CLUE_AI_PROVIDER_API_KEY",
345
+ "CLUE_AI_MODEL",
346
+ "CLUE_PROJECT_KEY",
347
+ "CLUE_ENVIRONMENT",
348
+ "CLUE_INGEST_ENDPOINT",
349
+ "CLUE_API_BASE_URL",
350
+ ],
351
+ };
352
+ const environmentInstructions = buildEnvironmentInstructions({
353
+ manifest,
354
+ setupContext,
355
+ });
356
+ const manifestWithEnvironmentArtifact = {
357
+ ...manifest,
358
+ artifacts: {
359
+ ...manifest.artifacts,
360
+ environment_file_path:
361
+ environmentInstructions.status === "ready"
362
+ ? environmentInstructions.env_file_path
363
+ : null,
364
+ },
365
+ };
366
+ await writeJson({
367
+ repoRoot: resolvedRepoRoot,
368
+ path: setupManifestPath,
369
+ value: manifestWithEnvironmentArtifact,
370
+ });
371
+ const environmentFilePath = await writeEnvironmentGuideIfReady({
372
+ repoRoot: resolvedRepoRoot,
373
+ instructions: environmentInstructions,
374
+ });
375
+ return {
376
+ ...manifestWithEnvironmentArtifact,
377
+ artifacts: {
378
+ ...manifestWithEnvironmentArtifact.artifacts,
379
+ environment_file_path: environmentFilePath,
380
+ },
381
+ environment_instructions:
382
+ summarizeEnvironmentInstructions(environmentInstructions),
383
+ };
170
384
  };