@clue-ai/cli 0.0.4 → 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.
@@ -11,10 +11,16 @@ Required fields:
11
11
  - `allowed_source_paths`
12
12
  - `excluded_source_paths`
13
13
 
14
- Required secrets:
14
+ Required environment:
15
15
 
16
16
  - `CLUE_API_KEY`
17
+ - `AI_PROVIDER`
17
18
  - `AI_PROVIDER_API_KEY`
19
+ - `CLUE_AI_MODEL`
20
+
21
+ `AI_PROVIDER` must be `openai` or `anthropic`. `AI_PROVIDER_API_KEY` must be the
22
+ API key for the selected provider. `CLUE_AI_MODEL` must be the model name for the
23
+ selected provider.
18
24
 
19
25
  Behavior:
20
26
 
@@ -11,10 +11,16 @@ Required fields:
11
11
  - `allowed_source_paths`
12
12
  - `excluded_source_paths`
13
13
 
14
- Required secrets:
14
+ Required environment:
15
15
 
16
16
  - `CLUE_API_KEY`
17
+ - `AI_PROVIDER`
17
18
  - `AI_PROVIDER_API_KEY`
19
+ - `CLUE_AI_MODEL`
20
+
21
+ `AI_PROVIDER` must be `openai` or `anthropic`. `AI_PROVIDER_API_KEY` must be the
22
+ API key for the selected provider. `CLUE_AI_MODEL` must be the model name for the
23
+ selected provider.
18
24
 
19
25
  Behavior:
20
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clue-ai/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clue-ai": "bin/clue-cli.mjs"
@@ -0,0 +1,146 @@
1
+ const SUPPORTED_AI_PROVIDERS = new Set(["openai", "anthropic"]);
2
+
3
+ const normalizeProvider = (value) => {
4
+ const provider = String(value || "openai")
5
+ .trim()
6
+ .toLowerCase();
7
+ if (!SUPPORTED_AI_PROVIDERS.has(provider)) {
8
+ throw new Error(
9
+ `CLUE_AI_PROVIDER must be one of: ${Array.from(SUPPORTED_AI_PROVIDERS).join(", ")}`,
10
+ );
11
+ }
12
+ return provider;
13
+ };
14
+
15
+ const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
16
+
17
+ const providerBaseUrl = ({ provider, env = {}, request = {} }) => {
18
+ const explicit =
19
+ request.ai_provider_base_url || env.CLUE_AI_PROVIDER_BASE_URL || null;
20
+ if (explicit) {
21
+ return trimTrailingSlash(explicit);
22
+ }
23
+ return provider === "anthropic"
24
+ ? "https://api.anthropic.com/v1"
25
+ : "https://api.openai.com/v1";
26
+ };
27
+
28
+ const providerModel = ({ env = {}, request = {} }) => {
29
+ const configuredModel =
30
+ env.CLUE_INIT_AI_MODEL || env.CLUE_AI_MODEL || request.ai_model;
31
+ const model = String(configuredModel || "").trim();
32
+ if (!model) {
33
+ throw new Error("CLUE_AI_MODEL is required");
34
+ }
35
+ return model;
36
+ };
37
+
38
+ export const resolveAiProviderConfig = ({ env = {}, request = {}, apiKey }) => {
39
+ const provider = normalizeProvider(
40
+ request.ai_provider || env.CLUE_AI_PROVIDER,
41
+ );
42
+ const resolvedApiKey = apiKey || env.CLUE_AI_PROVIDER_API_KEY;
43
+ if (!resolvedApiKey) {
44
+ throw new Error("CLUE_AI_PROVIDER_API_KEY is required");
45
+ }
46
+ return {
47
+ provider,
48
+ apiKey: resolvedApiKey,
49
+ baseUrl: providerBaseUrl({ provider, env, request }),
50
+ model: providerModel({ env, request }),
51
+ };
52
+ };
53
+
54
+ const parseOpenAiJson = async ({ response, emptyMessage }) => {
55
+ const body = await response.json();
56
+ const content = body?.choices?.[0]?.message?.content;
57
+ if (typeof content !== "string" || content.trim() === "") {
58
+ throw new Error(emptyMessage);
59
+ }
60
+ return JSON.parse(content);
61
+ };
62
+
63
+ const parseAnthropicJson = async ({ response, toolName, emptyMessage }) => {
64
+ const body = await response.json();
65
+ const toolUse = Array.isArray(body?.content)
66
+ ? body.content.find(
67
+ (entry) => entry?.type === "tool_use" && entry.name === toolName,
68
+ )
69
+ : null;
70
+ if (toolUse?.input && typeof toolUse.input === "object") {
71
+ return toolUse.input;
72
+ }
73
+ const text = Array.isArray(body?.content)
74
+ ? body.content
75
+ .filter(
76
+ (entry) => entry?.type === "text" && typeof entry.text === "string",
77
+ )
78
+ .map((entry) => entry.text)
79
+ .join("")
80
+ .trim()
81
+ : "";
82
+ if (!text) {
83
+ throw new Error(emptyMessage);
84
+ }
85
+ return JSON.parse(text);
86
+ };
87
+
88
+ export const callJsonAiProvider = async ({
89
+ config,
90
+ system,
91
+ user,
92
+ toolName = "return_json",
93
+ toolDescription = "Return schema-valid JSON for this Clue task.",
94
+ failureMessage = "AI provider failed",
95
+ emptyMessage = "AI provider returned empty content",
96
+ }) => {
97
+ const response =
98
+ config.provider === "anthropic"
99
+ ? await fetch(`${config.baseUrl}/messages`, {
100
+ method: "POST",
101
+ headers: {
102
+ "content-type": "application/json",
103
+ "x-api-key": config.apiKey,
104
+ "anthropic-version": "2023-06-01",
105
+ },
106
+ body: JSON.stringify({
107
+ model: config.model,
108
+ max_tokens: 4096,
109
+ system,
110
+ messages: [{ role: "user", content: user }],
111
+ tools: [
112
+ {
113
+ name: toolName,
114
+ description: toolDescription,
115
+ input_schema: {
116
+ type: "object",
117
+ additionalProperties: true,
118
+ },
119
+ },
120
+ ],
121
+ tool_choice: { type: "tool", name: toolName },
122
+ }),
123
+ })
124
+ : await fetch(`${config.baseUrl}/chat/completions`, {
125
+ method: "POST",
126
+ headers: {
127
+ "content-type": "application/json",
128
+ authorization: `Bearer ${config.apiKey}`,
129
+ },
130
+ body: JSON.stringify({
131
+ model: config.model,
132
+ messages: [
133
+ { role: "system", content: system },
134
+ { role: "user", content: user },
135
+ ],
136
+ response_format: { type: "json_object" },
137
+ }),
138
+ });
139
+
140
+ if (!response.ok) {
141
+ throw new Error(`${failureMessage}: ${response.status}`);
142
+ }
143
+ return config.provider === "anthropic"
144
+ ? parseAnthropicJson({ response, toolName, emptyMessage })
145
+ : parseOpenAiJson({ response, emptyMessage });
146
+ };
@@ -2,7 +2,7 @@ export const CLUE_INIT_COMMAND_NAME = "/clue-init";
2
2
 
3
3
  export const REQUIRED_SECRET_NAMES = [
4
4
  "CLUE_API_KEY",
5
- "AI_PROVIDER_API_KEY",
5
+ "CLUE_AI_PROVIDER_API_KEY",
6
6
  ];
7
7
 
8
8
  export const CLUE_INIT_COMMAND_FIELDS = [
@@ -55,15 +55,14 @@ const deriveServiceKeyFromBackendRootPath = (backendRootPath) => {
55
55
  .replace(/[^a-z0-9_-]+/g, "-")
56
56
  .replace(/^-+|-+$/g, "");
57
57
  if (!normalized) {
58
- throw new Error(`service_key cannot be derived from backend_root_path for ${CLUE_INIT_COMMAND_NAME}`);
58
+ throw new Error(
59
+ `service_key cannot be derived from backend_root_path for ${CLUE_INIT_COMMAND_NAME}`,
60
+ );
59
61
  }
60
62
  return normalized;
61
63
  };
62
64
 
63
- export const buildClueInitRequestFromCommandInput = ({
64
- targetTool,
65
- input,
66
- }) => {
65
+ export const buildClueInitRequestFromCommandInput = ({ targetTool, input }) => {
67
66
  const backendRootPath = requireField(input, "backend_root_path");
68
67
  const serviceKey =
69
68
  typeof input?.service_key === "string" && input.service_key.trim()
@@ -85,7 +84,8 @@ export const buildClueInitRequestFromCommandInput = ({
85
84
  [],
86
85
  "excluded_source_paths",
87
86
  ),
88
- ...(typeof input?.ci_workflow_path === "string" && input.ci_workflow_path.trim()
87
+ ...(typeof input?.ci_workflow_path === "string" &&
88
+ input.ci_workflow_path.trim()
89
89
  ? { ci_workflow_path: input.ci_workflow_path.trim() }
90
90
  : {}),
91
91
  };
package/src/contracts.mjs CHANGED
@@ -7,6 +7,7 @@ const {
7
7
  clueInitToolRequestSchema,
8
8
  clueInitToolReportSchema,
9
9
  semanticSnapshotRequestSchema,
10
+ semanticSnapshotResponseSchema,
10
11
  } = schemaPackage;
11
12
 
12
13
  const formatSchemaError = (result, field) => {
@@ -47,7 +48,11 @@ const stringArray = (value, field, { min = 0 } = {}) => {
47
48
  };
48
49
 
49
50
  export const validateInitRequest = (input) => {
50
- const parsed = parseWithSchema(clueInitToolRequestSchema, input, "init request");
51
+ const parsed = parseWithSchema(
52
+ clueInitToolRequestSchema,
53
+ input,
54
+ "init request",
55
+ );
51
56
  return {
52
57
  ...parsed,
53
58
  allowed_source_paths: [...new Set(parsed.allowed_source_paths)],
@@ -55,14 +60,18 @@ export const validateInitRequest = (input) => {
55
60
  };
56
61
  };
57
62
 
58
- export const buildInitReport = ({ request, lifecycleInsertions, warnings = [] }) =>
63
+ export const buildInitReport = ({
64
+ request,
65
+ lifecycleInsertions,
66
+ warnings = [],
67
+ }) =>
59
68
  parseWithSchema(
60
69
  clueInitToolReportSchema,
61
70
  {
62
71
  target_tool: request.target_tool,
63
72
  ci_workflow_path: request.ci_workflow_path,
64
73
  ci_workflow_added: true,
65
- required_secrets: ["CLUE_API_KEY", "AI_PROVIDER_API_KEY"],
74
+ required_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
66
75
  lifecycle_insertions: lifecycleInsertions,
67
76
  semantic_generation_timing: "after_merge_ci",
68
77
  semantic_preview_generated: false,
@@ -80,9 +89,13 @@ export const validateSemanticCiRequest = (input) => ({
80
89
  project_key: nonEmpty(input.project_key, "project_key"),
81
90
  environment: nonEmpty(input.environment, "environment"),
82
91
  service_key: nonEmpty(input.service_key, "service_key"),
92
+ ai_provider: optionalNonEmpty(input.ai_provider) ?? "openai",
83
93
  repository: {
84
94
  provider: nonEmpty(input.repository?.provider, "repository.provider"),
85
- repository_id: nonEmpty(input.repository?.repository_id, "repository.repository_id"),
95
+ repository_id: nonEmpty(
96
+ input.repository?.repository_id,
97
+ "repository.repository_id",
98
+ ),
86
99
  ...(optionalNonEmpty(input.repository?.owner)
87
100
  ? { owner: optionalNonEmpty(input.repository.owner) }
88
101
  : {}),
@@ -92,11 +105,15 @@ export const validateSemanticCiRequest = (input) => ({
92
105
  ...(optionalNonEmpty(input.repository?.default_branch)
93
106
  ? { default_branch: optionalNonEmpty(input.repository.default_branch) }
94
107
  : {}),
95
- merge_commit: nonEmpty(input.repository?.merge_commit, "repository.merge_commit"),
108
+ merge_commit: nonEmpty(
109
+ input.repository?.merge_commit,
110
+ "repository.merge_commit",
111
+ ),
96
112
  ...(optionalNonEmpty(input.repository?.merged_at)
97
113
  ? { merged_at: optionalNonEmpty(input.repository.merged_at) }
98
114
  : {}),
99
- ...(input.repository?.pull_request_number === undefined || input.repository?.pull_request_number === null
115
+ ...(input.repository?.pull_request_number === undefined ||
116
+ input.repository?.pull_request_number === null
100
117
  ? {}
101
118
  : { pull_request_number: input.repository.pull_request_number }),
102
119
  ...(optionalNonEmpty(input.repository?.workflow_run_id)
@@ -104,21 +121,43 @@ export const validateSemanticCiRequest = (input) => ({
104
121
  : {}),
105
122
  },
106
123
  service: {
107
- service_key: nonEmpty(input.service?.service_key ?? input.service_key, "service.service_key"),
124
+ service_key: nonEmpty(
125
+ input.service?.service_key ?? input.service_key,
126
+ "service.service_key",
127
+ ),
108
128
  root_path: input.service?.root_path ?? null,
109
129
  framework: input.service?.framework ?? "fastapi",
110
130
  language: input.service?.language ?? "python",
111
131
  },
112
- allowed_source_paths: stringArray(input.allowed_source_paths, "allowed_source_paths", { min: 1 }),
113
- excluded_source_paths: stringArray(input.excluded_source_paths ?? [], "excluded_source_paths"),
114
- clue_api_base_url: nonEmpty(input.clue_api_base_url, "clue_api_base_url"),
115
- ai_provider_base_url: optionalNonEmpty(input.ai_provider_base_url) ?? "https://api.openai.com/v1",
116
- ai_model: optionalNonEmpty(input.ai_model) ?? "gpt-5.4-mini",
117
- });
132
+ allowed_source_paths: stringArray(
133
+ input.allowed_source_paths,
134
+ "allowed_source_paths",
135
+ { min: 1 },
136
+ ),
137
+ excluded_source_paths: stringArray(
138
+ input.excluded_source_paths ?? [],
139
+ "excluded_source_paths",
140
+ ),
141
+ clue_api_base_url: nonEmpty(input.clue_api_base_url, "clue_api_base_url"),
142
+ ai_provider_base_url: optionalNonEmpty(input.ai_provider_base_url) ?? null,
143
+ ai_model: nonEmpty(input.ai_model, "ai_model"),
144
+ });
118
145
 
119
146
  export const buildOperationSourceKey = (method, pathTemplate) =>
120
147
  `route.${method.toUpperCase()}.${pathTemplate}`;
121
148
 
122
149
  export const validateSemanticSnapshotRequest = (input) => {
123
- return parseWithSchema(semanticSnapshotRequestSchema, input, "semantic snapshot");
150
+ return parseWithSchema(
151
+ semanticSnapshotRequestSchema,
152
+ input,
153
+ "semantic snapshot",
154
+ );
155
+ };
156
+
157
+ export const validateSemanticSnapshotResponse = (input) => {
158
+ return parseWithSchema(
159
+ semanticSnapshotResponseSchema,
160
+ input,
161
+ "semantic snapshot response",
162
+ );
124
163
  };
package/src/init-tool.mjs CHANGED
@@ -1,20 +1,110 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { buildInitReport, validateInitRequest } from "./contracts.mjs";
4
- import { applyLifecyclePlan, planLifecycleInsertions } from "./lifecycle-init.mjs";
4
+ import {
5
+ applyLifecyclePlan,
6
+ planLifecycleInsertions,
7
+ } from "./lifecycle-init.mjs";
8
+
9
+ const DEFAULT_SEMANTIC_WORKFLOW_PATH =
10
+ ".github/workflows/clue-semantic-snapshot.yml";
11
+
12
+ const nonEmpty = (value, field) => {
13
+ if (typeof value !== "string" || value.trim() === "") {
14
+ throw new Error(`${field} is required`);
15
+ }
16
+ return value.trim();
17
+ };
18
+
19
+ const normalizeStringArray = (value, field, { min = 0 } = {}) => {
20
+ if (!Array.isArray(value)) {
21
+ throw new Error(`${field} must be an array`);
22
+ }
23
+ const result = value
24
+ .filter((entry) => typeof entry === "string" && entry.trim())
25
+ .map((entry) => entry.trim());
26
+ if (result.length < min) {
27
+ throw new Error(`${field} must include at least ${min} item(s)`);
28
+ }
29
+ return [...new Set(result)];
30
+ };
31
+
32
+ const splitCsv = (value) =>
33
+ typeof value === "string"
34
+ ? value
35
+ .split(",")
36
+ .map((entry) => entry.trim())
37
+ .filter(Boolean)
38
+ : [];
39
+
40
+ const deriveServiceKeyFromPath = (backendRootPath) => {
41
+ const segment = backendRootPath
42
+ .split("/")
43
+ .map((part) => part.trim())
44
+ .filter(Boolean)
45
+ .at(-1);
46
+ const normalized = segment
47
+ ?.toLowerCase()
48
+ .replace(/[^a-z0-9_-]+/g, "-")
49
+ .replace(/^-+|-+$/g, "");
50
+ if (!normalized) {
51
+ throw new Error("service-key cannot be derived from backend-root-path");
52
+ }
53
+ return normalized;
54
+ };
55
+
56
+ export const buildSemanticWorkflowRequestFromFlags = (flags) => {
57
+ const backendRootPath = nonEmpty(flags.backendRootPath, "backend-root-path");
58
+ const allowedSourcePaths = splitCsv(flags.allowedSourcePaths);
59
+ const projectKey =
60
+ typeof flags.projectKey === "string" && flags.projectKey.trim()
61
+ ? flags.projectKey.trim()
62
+ : "${{ vars.CLUE_PROJECT_KEY }}";
63
+ const environment =
64
+ typeof flags.environment === "string" && flags.environment.trim()
65
+ ? flags.environment.trim()
66
+ : "${{ vars.CLUE_ENVIRONMENT }}";
67
+ const clueApiBaseUrl =
68
+ typeof flags.clueApiBaseUrl === "string" && flags.clueApiBaseUrl.trim()
69
+ ? flags.clueApiBaseUrl.trim()
70
+ : "${{ vars.CLUE_API_BASE_URL }}";
71
+ return {
72
+ project_key: projectKey,
73
+ environment,
74
+ clue_api_base_url: clueApiBaseUrl,
75
+ ci_workflow_path:
76
+ typeof flags.workflowPath === "string" && flags.workflowPath.trim()
77
+ ? flags.workflowPath.trim()
78
+ : DEFAULT_SEMANTIC_WORKFLOW_PATH,
79
+ service_key:
80
+ typeof flags.serviceKey === "string" && flags.serviceKey.trim()
81
+ ? flags.serviceKey.trim()
82
+ : deriveServiceKeyFromPath(backendRootPath),
83
+ framework: nonEmpty(flags.framework, "framework"),
84
+ allowed_source_paths: normalizeStringArray(
85
+ allowedSourcePaths.length ? allowedSourcePaths : [backendRootPath],
86
+ "allowed-source-paths",
87
+ { min: 1 },
88
+ ),
89
+ excluded_source_paths: normalizeStringArray(
90
+ splitCsv(flags.excludedSourcePaths),
91
+ "excluded-source-paths",
92
+ ),
93
+ };
94
+ };
95
+
96
+ const usesGithubVariable = (value) =>
97
+ typeof value === "string" && value.includes("${{ vars.");
5
98
 
6
99
  const workflowRequestPayload = (request) =>
7
100
  JSON.stringify(
8
101
  {
9
- project_key: "${{ vars.CLUE_PROJECT_KEY }}",
10
- environment: "${{ vars.CLUE_ENVIRONMENT }}",
102
+ project_key: request.project_key ?? "${{ vars.CLUE_PROJECT_KEY }}",
103
+ environment: request.environment ?? "${{ vars.CLUE_ENVIRONMENT }}",
11
104
  service_key: request.service_key,
12
105
  repository: {
13
106
  provider: "github",
14
107
  repository_id: "${{ github.repository_id }}",
15
- owner: "${{ github.repository_owner }}",
16
- name: "${{ github.event.repository.name }}",
17
- default_branch: "${{ github.event.repository.default_branch }}",
18
108
  merge_commit: "${{ github.sha }}",
19
109
  workflow_run_id: "${{ github.run_id }}",
20
110
  },
@@ -26,40 +116,87 @@ const workflowRequestPayload = (request) =>
26
116
  },
27
117
  allowed_source_paths: request.allowed_source_paths,
28
118
  excluded_source_paths: request.excluded_source_paths,
29
- clue_api_base_url: "${{ vars.CLUE_API_BASE_URL }}",
119
+ clue_api_base_url:
120
+ request.clue_api_base_url ?? "${{ vars.CLUE_API_BASE_URL }}",
121
+ ai_provider: "${{ vars.CLUE_AI_PROVIDER }}",
30
122
  ai_model: "${{ vars.CLUE_AI_MODEL }}",
31
123
  },
32
124
  null,
33
125
  10,
34
126
  ).trim();
35
127
 
128
+ const indentMultiline = (value, spaces) => {
129
+ const prefix = " ".repeat(spaces);
130
+ return value
131
+ .split("\n")
132
+ .map((line) => `${prefix}${line}`)
133
+ .join("\n");
134
+ };
135
+
36
136
  const workflowTemplate = (request) => `name: Clue Semantic Snapshot
37
137
 
38
138
  on:
39
139
  push:
40
140
 
141
+ permissions:
142
+ contents: read
143
+
41
144
  jobs:
42
145
  semantic-snapshot:
43
146
  if: github.ref_name == github.event.repository.default_branch
44
147
  runs-on: ubuntu-latest
45
148
  steps:
46
149
  - uses: actions/checkout@v4
150
+ with:
151
+ persist-credentials: false
47
152
  - uses: actions/setup-node@v4
48
153
  with:
49
154
  node-version: "20"
50
155
  - name: Run Clue semantic generation
51
- continue-on-error: true
52
156
  env:
53
157
  CLUE_API_KEY: \${{ secrets.CLUE_API_KEY }}
54
- AI_PROVIDER_API_KEY: \${{ secrets.AI_PROVIDER_API_KEY }}
158
+ CLUE_AI_PROVIDER_API_KEY: \${{ secrets.CLUE_AI_PROVIDER_API_KEY }}
159
+ CLUE_AI_PROVIDER: \${{ vars.CLUE_AI_PROVIDER }}
160
+ CLUE_SEMANTIC_REQUEST_JSON: |
161
+ ${indentMultiline(workflowRequestPayload(request), 12)}
55
162
  run: |
56
- mkdir -p .clue
57
- cat > .clue/semantic-request.runtime.json <<'JSON'
58
- ${workflowRequestPayload(request)}
59
- JSON
60
- npx @clue-ai/cli semantic-ci --request .clue/semantic-request.runtime.json --repo .
163
+ npx @clue-ai/cli semantic-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .
61
164
  `;
62
165
 
166
+ export const writeSemanticWorkflow = async ({ repoRoot, request }) => {
167
+ const workflowPath = join(repoRoot, request.ci_workflow_path);
168
+ await mkdir(dirname(workflowPath), { recursive: true });
169
+ await writeFile(workflowPath, workflowTemplate(request), "utf8");
170
+ return {
171
+ ci_workflow_path: request.ci_workflow_path,
172
+ ci_workflow_added: true,
173
+ required_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
174
+ required_variables: [
175
+ "CLUE_AI_PROVIDER",
176
+ ...(usesGithubVariable(
177
+ request.project_key ?? "${{ vars.CLUE_PROJECT_KEY }}",
178
+ )
179
+ ? ["CLUE_PROJECT_KEY"]
180
+ : []),
181
+ ...(usesGithubVariable(
182
+ request.environment ?? "${{ vars.CLUE_ENVIRONMENT }}",
183
+ )
184
+ ? ["CLUE_ENVIRONMENT"]
185
+ : []),
186
+ ...(usesGithubVariable(
187
+ request.clue_api_base_url ?? "${{ vars.CLUE_API_BASE_URL }}",
188
+ )
189
+ ? ["CLUE_API_BASE_URL"]
190
+ : []),
191
+ "CLUE_AI_MODEL",
192
+ ],
193
+ runtime_request_committed: false,
194
+ semantic_generation_timing: "after_merge_ci",
195
+ allowed_source_paths: request.allowed_source_paths,
196
+ excluded_source_paths: request.excluded_source_paths,
197
+ };
198
+ };
199
+
63
200
  export const runInitTool = async ({
64
201
  repoRoot,
65
202
  request: rawRequest,
@@ -67,9 +204,7 @@ export const runInitTool = async ({
67
204
  lifecyclePlanner = planLifecycleInsertions,
68
205
  }) => {
69
206
  const request = validateInitRequest(rawRequest);
70
- const workflowPath = join(repoRoot, request.ci_workflow_path);
71
- await mkdir(dirname(workflowPath), { recursive: true });
72
- await writeFile(workflowPath, workflowTemplate(request), "utf8");
207
+ await writeSemanticWorkflow({ repoRoot, request });
73
208
  const lifecyclePlan = await lifecyclePlanner({ repoRoot, request, env });
74
209
  const lifecycleResult = await applyLifecyclePlan({
75
210
  repoRoot,
@@ -79,8 +214,6 @@ export const runInitTool = async ({
79
214
  return buildInitReport({
80
215
  request,
81
216
  lifecycleInsertions: lifecycleResult.lifecycleInsertions,
82
- warnings: [
83
- ...lifecycleResult.warnings,
84
- ],
217
+ warnings: [...lifecycleResult.warnings],
85
218
  });
86
219
  };