@clue-ai/cli 0.0.5 → 0.0.6

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,289 @@
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 BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
11
+ const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
10
12
 
11
13
  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");
14
+ const absolutePath = join(resolve(repoRoot), path);
15
+ await mkdir(dirname(absolutePath), { recursive: true });
16
+ await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
15
17
  };
16
18
 
17
19
  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
- };
20
+ if (!detection.detected || detection.candidates.length === 0) {
21
+ return {
22
+ candidate: null,
23
+ blockers: detection.blockers.length
24
+ ? detection.blockers
25
+ : [
26
+ {
27
+ code: "NO_SETUP_CANDIDATE",
28
+ message: "No setup candidate was mechanically detected.",
29
+ },
30
+ ],
31
+ };
32
+ }
33
+ return {
34
+ candidate: detection.candidates[0],
35
+ blockers: [],
36
+ };
35
37
  };
36
38
 
37
39
  const DEFAULT_SETUP_LIFECYCLE = [
38
- "init",
39
- "identify",
40
- "set-account",
41
- "logout",
42
- "event-sent",
40
+ "init",
41
+ "identify",
42
+ "set-account",
43
+ "logout",
44
+ "event-sent",
43
45
  ];
44
46
 
45
47
  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
- }));
48
+ const frontendServices = Array.isArray(detection.services?.frontend)
49
+ ? detection.services.frontend
50
+ : [];
51
+ const backendServices = Array.isArray(detection.services?.backend)
52
+ ? detection.services.backend
53
+ : [
54
+ {
55
+ kind: "backend",
56
+ framework: fallbackCandidate.framework,
57
+ root_path: fallbackCandidate.backend_root_path,
58
+ service_key: fallbackCandidate.service_key,
59
+ local_url_candidates: [],
60
+ },
61
+ ];
62
+ return [...frontendServices, ...backendServices].map((service) => ({
63
+ kind: service.kind,
64
+ framework: service.framework,
65
+ root_path: service.root_path,
66
+ service_key: service.service_key,
67
+ producer_id: service.service_key,
68
+ expected_lifecycle: DEFAULT_SETUP_LIFECYCLE,
69
+ local_url_candidates: service.local_url_candidates ?? [],
70
+ url_env_name:
71
+ service.kind === "frontend"
72
+ ? `CLUE_LOCAL_${service.service_key.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_FRONTEND_URL`
73
+ : `CLUE_LOCAL_${service.service_key.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_BACKEND_URL`,
74
+ }));
75
+ };
76
+
77
+ const optionalString = (value) =>
78
+ typeof value === "string" && value.trim() ? value.trim() : null;
79
+
80
+ const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
81
+
82
+ const buildEndpoint = (baseUrl, path) => `${trimTrailingSlash(baseUrl)}${path}`;
83
+
84
+ const setupContextFromInput = (input = {}) => ({
85
+ clue_api_key: optionalString(input.clueApiKey),
86
+ clue_api_base_url: optionalString(input.clueApiBaseUrl),
87
+ project_key: optionalString(input.projectKey),
88
+ environment: optionalString(input.environment),
89
+ });
90
+
91
+ const envFileCandidates = (target) => {
92
+ if (target.kind === "frontend") {
93
+ return target.framework === "nextjs" ? [".env.local"] : [".env"];
94
+ }
95
+ return [".env"];
96
+ };
97
+
98
+ const buildServiceEnvBlock = ({ target, setupContext }) => {
99
+ const ingestPath =
100
+ target.kind === "frontend" ? BROWSER_INGEST_PATH : BACKEND_INGEST_PATH;
101
+ const variables = [
102
+ {
103
+ name: "CLUE_INGEST_ENDPOINT",
104
+ value: buildEndpoint(setupContext.clue_api_base_url, ingestPath),
105
+ },
106
+ { name: "CLUE_PROJECT_KEY", value: setupContext.project_key },
107
+ { name: "CLUE_ENVIRONMENT", value: setupContext.environment },
108
+ { name: "CLUE_SERVICE_KEY", value: target.service_key },
109
+ ];
110
+ if (target.kind === "backend") {
111
+ variables.push({ name: "CLUE_API_KEY", value: setupContext.clue_api_key });
112
+ }
113
+ return variables.map(({ name, value }) => `${name}=${value}`).join("\n");
114
+ };
115
+
116
+ const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
117
+ const missingFlags = [
118
+ ["clue_api_key", "--clue-api-key"],
119
+ ["clue_api_base_url", "--clue-api-base-url"],
120
+ ["project_key", "--project-key"],
121
+ ["environment", "--environment"],
122
+ ]
123
+ .filter(([key]) => !setupContext[key])
124
+ .map(([, flag]) => flag);
125
+
126
+ if (missingFlags.length > 0 || manifest.status !== "ready_for_ai") {
127
+ return {
128
+ status: "missing_setup_arguments",
129
+ required_flags: missingFlags,
130
+ message:
131
+ "Run setup with Clue values from the setup screen to print service-specific env blocks.",
132
+ };
133
+ }
134
+
135
+ const watchTargets = manifest.lifecycle_verification.watch_targets;
136
+ return {
137
+ status: "ready",
138
+ message: "各サービスの env ファイルに以下を設定してください。",
139
+ service_env_blocks: watchTargets.map((target) => ({
140
+ kind: target.kind,
141
+ framework: target.framework,
142
+ root_path: target.root_path,
143
+ service_key: target.service_key,
144
+ env_file_candidates: envFileCandidates(target),
145
+ env_block: buildServiceEnvBlock({ target, setupContext }),
146
+ })),
147
+ ci_github: {
148
+ secrets: [
149
+ { name: "CLUE_API_KEY", value: setupContext.clue_api_key },
150
+ {
151
+ name: "CLUE_AI_PROVIDER_API_KEY",
152
+ value: "<openai-or-anthropic-api-key>",
153
+ },
154
+ ],
155
+ variables: [
156
+ { name: "CLUE_AI_PROVIDER", value: "<openai-or-anthropic>" },
157
+ { name: "CLUE_AI_MODEL", value: "<selected-provider-model>" },
158
+ ],
159
+ },
160
+ };
73
161
  };
74
162
 
75
163
  export const runSetupPrepare = async ({
76
- repoRoot,
77
- target,
78
- skillRoot,
79
- setupManifestPath = DEFAULT_SETUP_MANIFEST_PATH,
164
+ repoRoot,
165
+ target,
166
+ skillRoot,
167
+ setupContext: setupContextInput,
168
+ setupManifestPath = DEFAULT_SETUP_MANIFEST_PATH,
80
169
  }) => {
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;
170
+ const resolvedRepoRoot = resolve(repoRoot ?? ".");
171
+ const setupContext = setupContextFromInput(setupContextInput);
172
+ const detection = await runSetupDetect({ repoRoot: resolvedRepoRoot });
173
+ const { candidate, blockers } = firstCandidateOrBlocker(detection);
174
+ if (!candidate) {
175
+ const manifest = {
176
+ status: "blocked",
177
+ target,
178
+ skill_root: skillRoot,
179
+ blockers,
180
+ detection,
181
+ ai_next_scope: "blocked_until_backend_routes_are_detected",
182
+ machine_owned_artifacts: [],
183
+ ai_owned_workstreams: [
184
+ "sdk_lifecycle_implementation_after_blockers_are_resolved",
185
+ ],
186
+ };
187
+ await writeJson({
188
+ repoRoot: resolvedRepoRoot,
189
+ path: setupManifestPath,
190
+ value: manifest,
191
+ });
192
+ return {
193
+ ...manifest,
194
+ environment_instructions: buildEnvironmentInstructions({
195
+ manifest,
196
+ setupContext,
197
+ }),
198
+ };
199
+ }
200
+
201
+ const request = buildSemanticWorkflowRequestFromFlags({
202
+ framework: candidate.framework,
203
+ backendRootPath: candidate.backend_root_path,
204
+ serviceKey: candidate.service_key,
205
+ projectKey: setupContext.project_key,
206
+ environment: setupContext.environment,
207
+ clueApiBaseUrl: setupContext.clue_api_base_url,
208
+ });
209
+ const workflow = await writeSemanticWorkflow({
210
+ repoRoot: resolvedRepoRoot,
211
+ request,
212
+ });
213
+
214
+ const manifest = {
215
+ status: "ready_for_ai",
216
+ target,
217
+ skill_root: skillRoot,
218
+ detected: {
219
+ framework: candidate.framework,
220
+ backend_root_path: candidate.backend_root_path,
221
+ service_key: candidate.service_key,
222
+ },
223
+ service_identity: {
224
+ canonical_field: "service_key",
225
+ backend_env_name: "CLUE_SERVICE_KEY",
226
+ frontend_env_name: "CLUE_SERVICE_KEY",
227
+ producer_id_derivation: "producer_id defaults to service_key",
228
+ },
229
+ clue_context: {
230
+ project_key: setupContext.project_key,
231
+ environment: setupContext.environment,
232
+ clue_api_base_url: setupContext.clue_api_base_url,
233
+ ingest_endpoints: setupContext.clue_api_base_url
234
+ ? {
235
+ browser: buildEndpoint(
236
+ setupContext.clue_api_base_url,
237
+ BROWSER_INGEST_PATH,
238
+ ),
239
+ backend: buildEndpoint(
240
+ setupContext.clue_api_base_url,
241
+ BACKEND_INGEST_PATH,
242
+ ),
243
+ }
244
+ : null,
245
+ },
246
+ lifecycle_verification: {
247
+ watch_target_format:
248
+ "frontend:<service-key>[init,identify,set-account,logout,event-sent]=<frontend-url>,backend:<service-key>[init,identify,set-account,logout,event-sent]=<backend-url>",
249
+ rule: "setup-watch --local uses the structured watch_targets below. Lifecycle checks are fixed for setup verification and are evaluated per service_key.",
250
+ watch_targets: buildWatchTargets(detection, candidate),
251
+ },
252
+ artifacts: {
253
+ ci_workflow_path: workflow.ci_workflow_path,
254
+ setup_manifest_path: setupManifestPath,
255
+ runtime_request_committed: false,
256
+ },
257
+ machine_owned_artifacts: [workflow.ci_workflow_path, setupManifestPath],
258
+ ai_must_not_edit: [workflow.ci_workflow_path],
259
+ ai_owned_workstreams: ["sdk_lifecycle_implementation"],
260
+ required_final_check: {
261
+ command:
262
+ `npx @clue-ai/cli setup-check --framework ${candidate.framework} ` +
263
+ `--backend-root-path ${candidate.backend_root_path} --repo . --target ${target} --require-sdk-lifecycle`,
264
+ },
265
+ required_env_names: [
266
+ "CLUE_SERVICE_KEY",
267
+ "CLUE_API_KEY",
268
+ "CLUE_AI_PROVIDER",
269
+ "CLUE_AI_PROVIDER_API_KEY",
270
+ "CLUE_PROJECT_KEY",
271
+ "CLUE_ENVIRONMENT",
272
+ "CLUE_INGEST_ENDPOINT",
273
+ "CLUE_API_BASE_URL",
274
+ "CLUE_AI_MODEL",
275
+ ],
276
+ };
277
+ await writeJson({
278
+ repoRoot: resolvedRepoRoot,
279
+ path: setupManifestPath,
280
+ value: manifest,
281
+ });
282
+ return {
283
+ ...manifest,
284
+ environment_instructions: buildEnvironmentInstructions({
285
+ manifest,
286
+ setupContext,
287
+ }),
288
+ };
170
289
  };