@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.
@@ -11,10 +11,17 @@ 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_API_KEY`
17
+ - `CLUE_AI_PROVIDER_API_KEY`
18
+ - `CLUE_AI_PROVIDER`
19
+ - `CLUE_AI_MODEL`
20
+
21
+ Semantic snapshot generation runs inside the customer repository CI with the
22
+ customer-configured AI provider key. Clue must receive only the final
23
+ privacy-safe semantic snapshot, not raw source code, prompts, completions, or AI
24
+ provider keys.
18
25
 
19
26
  Behavior:
20
27
 
@@ -11,10 +11,17 @@ 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_API_KEY`
17
+ - `CLUE_AI_PROVIDER_API_KEY`
18
+ - `CLUE_AI_PROVIDER`
19
+ - `CLUE_AI_MODEL`
20
+
21
+ Semantic snapshot generation runs inside the customer repository CI with the
22
+ customer-configured AI provider key. Clue must receive only the final
23
+ privacy-safe semantic snapshot, not raw source code, prompts, completions, or AI
24
+ provider keys.
18
25
 
19
26
  Behavior:
20
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clue-ai/cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clue-ai": "bin/clue-cli.mjs"
@@ -0,0 +1,147 @@
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 = env.CLUE_AI_PROVIDER_BASE_URL || null;
19
+ if (explicit) {
20
+ return trimTrailingSlash(explicit);
21
+ }
22
+ return provider === "anthropic"
23
+ ? "https://api.anthropic.com/v1"
24
+ : "https://api.openai.com/v1";
25
+ };
26
+
27
+ const providerModel = ({ env = {}, request = {} }) => {
28
+ const configuredModel =
29
+ env.CLUE_INIT_AI_MODEL || env.CLUE_AI_MODEL || request.ai_model;
30
+ const model = String(configuredModel || "").trim();
31
+ if (!model) {
32
+ throw new Error("CLUE_AI_MODEL is required");
33
+ }
34
+ return model;
35
+ };
36
+
37
+ export const resolveAiProviderConfig = ({ env = {}, request = {}, apiKey }) => {
38
+ const provider = normalizeProvider(
39
+ request.ai_provider || env.CLUE_AI_PROVIDER,
40
+ );
41
+ const resolvedApiKey = apiKey || env.CLUE_AI_PROVIDER_API_KEY;
42
+ if (!resolvedApiKey) {
43
+ throw new Error("CLUE_AI_PROVIDER_API_KEY is required");
44
+ }
45
+ return {
46
+ provider,
47
+ apiKey: resolvedApiKey,
48
+ baseUrl: providerBaseUrl({ provider, env, request }),
49
+ model: providerModel({ env, request }),
50
+ };
51
+ };
52
+
53
+ const parseOpenAiJson = async ({ response, emptyMessage }) => {
54
+ const body = await response.json();
55
+ const content = body?.choices?.[0]?.message?.content;
56
+ if (typeof content !== "string" || content.trim() === "") {
57
+ throw new Error(emptyMessage);
58
+ }
59
+ return JSON.parse(content);
60
+ };
61
+
62
+ const parseAnthropicJson = async ({ response, toolName, emptyMessage }) => {
63
+ const body = await response.json();
64
+ const toolUse = Array.isArray(body?.content)
65
+ ? body.content.find(
66
+ (entry) => entry?.type === "tool_use" && entry.name === toolName,
67
+ )
68
+ : null;
69
+ if (toolUse?.input && typeof toolUse.input === "object") {
70
+ return toolUse.input;
71
+ }
72
+ const text = Array.isArray(body?.content)
73
+ ? body.content
74
+ .filter(
75
+ (entry) => entry?.type === "text" && typeof entry.text === "string",
76
+ )
77
+ .map((entry) => entry.text)
78
+ .join("")
79
+ .trim()
80
+ : "";
81
+ if (!text) {
82
+ throw new Error(emptyMessage);
83
+ }
84
+ return JSON.parse(text);
85
+ };
86
+
87
+ export const callJsonAiProvider = async ({
88
+ config,
89
+ system,
90
+ user,
91
+ toolName = "return_json",
92
+ toolDescription = "Return schema-valid JSON for this Clue task.",
93
+ failureMessage = "AI provider failed",
94
+ emptyMessage = "AI provider returned empty content",
95
+ }) => {
96
+ const response =
97
+ config.provider === "anthropic"
98
+ ? await fetch(`${config.baseUrl}/messages`, {
99
+ method: "POST",
100
+ headers: {
101
+ "content-type": "application/json",
102
+ "x-api-key": config.apiKey,
103
+ "anthropic-version": "2023-06-01",
104
+ },
105
+ body: JSON.stringify({
106
+ model: config.model,
107
+ max_tokens: 4096,
108
+ temperature: 0,
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
+ temperature: 0,
133
+ messages: [
134
+ { role: "system", content: system },
135
+ { role: "user", content: user },
136
+ ],
137
+ response_format: { type: "json_object" },
138
+ }),
139
+ });
140
+
141
+ if (!response.ok) {
142
+ throw new Error(`${failureMessage}: ${response.status}`);
143
+ }
144
+ return config.provider === "anthropic"
145
+ ? parseAnthropicJson({ response, toolName, emptyMessage })
146
+ : parseOpenAiJson({ response, emptyMessage });
147
+ };
@@ -2,8 +2,9 @@ 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
+ export const REQUIRED_VARIABLE_NAMES = ["CLUE_AI_PROVIDER", "CLUE_AI_MODEL"];
7
8
 
8
9
  export const CLUE_INIT_COMMAND_FIELDS = [
9
10
  "project_key",
@@ -22,6 +23,7 @@ export const CLUE_INIT_COMMAND = {
22
23
  report_contract: "clueInitToolReportSchema",
23
24
  required_fields: CLUE_INIT_COMMAND_FIELDS,
24
25
  required_secret_names: REQUIRED_SECRET_NAMES,
26
+ required_variable_names: REQUIRED_VARIABLE_NAMES,
25
27
  };
26
28
 
27
29
  export const commandSpecs = [CLUE_INIT_COMMAND];
@@ -55,15 +57,14 @@ const deriveServiceKeyFromBackendRootPath = (backendRootPath) => {
55
57
  .replace(/[^a-z0-9_-]+/g, "-")
56
58
  .replace(/^-+|-+$/g, "");
57
59
  if (!normalized) {
58
- throw new Error(`service_key cannot be derived from backend_root_path for ${CLUE_INIT_COMMAND_NAME}`);
60
+ throw new Error(
61
+ `service_key cannot be derived from backend_root_path for ${CLUE_INIT_COMMAND_NAME}`,
62
+ );
59
63
  }
60
64
  return normalized;
61
65
  };
62
66
 
63
- export const buildClueInitRequestFromCommandInput = ({
64
- targetTool,
65
- input,
66
- }) => {
67
+ export const buildClueInitRequestFromCommandInput = ({ targetTool, input }) => {
67
68
  const backendRootPath = requireField(input, "backend_root_path");
68
69
  const serviceKey =
69
70
  typeof input?.service_key === "string" && input.service_key.trim()
@@ -85,7 +86,8 @@ export const buildClueInitRequestFromCommandInput = ({
85
86
  [],
86
87
  "excluded_source_paths",
87
88
  ),
88
- ...(typeof input?.ci_workflow_path === "string" && input.ci_workflow_path.trim()
89
+ ...(typeof input?.ci_workflow_path === "string" &&
90
+ input.ci_workflow_path.trim()
89
91
  ? { ci_workflow_path: input.ci_workflow_path.trim() }
90
92
  : {}),
91
93
  };
package/src/contracts.mjs CHANGED
@@ -2,7 +2,6 @@ import { createRequire } from "node:module";
2
2
 
3
3
  const require = createRequire(import.meta.url);
4
4
  const schemaPackage = require("./public-schema.cjs");
5
-
6
5
  const {
7
6
  clueInitToolRequestSchema,
8
7
  clueInitToolReportSchema,
@@ -48,7 +47,11 @@ const stringArray = (value, field, { min = 0 } = {}) => {
48
47
  };
49
48
 
50
49
  export const validateInitRequest = (input) => {
51
- const parsed = parseWithSchema(clueInitToolRequestSchema, input, "init request");
50
+ const parsed = parseWithSchema(
51
+ clueInitToolRequestSchema,
52
+ input,
53
+ "init request",
54
+ );
52
55
  return {
53
56
  ...parsed,
54
57
  allowed_source_paths: [...new Set(parsed.allowed_source_paths)],
@@ -56,14 +59,20 @@ export const validateInitRequest = (input) => {
56
59
  };
57
60
  };
58
61
 
59
- export const buildInitReport = ({ request, lifecycleInsertions, warnings = [] }) =>
62
+ export const buildInitReport = ({
63
+ request,
64
+ lifecycleInsertions,
65
+ requiredVariables = [],
66
+ warnings = [],
67
+ }) =>
60
68
  parseWithSchema(
61
69
  clueInitToolReportSchema,
62
70
  {
63
71
  target_tool: request.target_tool,
64
72
  ci_workflow_path: request.ci_workflow_path,
65
73
  ci_workflow_added: true,
66
- required_secrets: ["CLUE_API_KEY", "AI_PROVIDER_API_KEY"],
74
+ required_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
75
+ required_variables: requiredVariables,
67
76
  lifecycle_insertions: lifecycleInsertions,
68
77
  semantic_generation_timing: "after_merge_ci",
69
78
  semantic_preview_generated: false,
@@ -83,7 +92,10 @@ export const validateSemanticCiRequest = (input) => ({
83
92
  service_key: nonEmpty(input.service_key, "service_key"),
84
93
  repository: {
85
94
  provider: nonEmpty(input.repository?.provider, "repository.provider"),
86
- repository_id: nonEmpty(input.repository?.repository_id, "repository.repository_id"),
95
+ repository_id: nonEmpty(
96
+ input.repository?.repository_id,
97
+ "repository.repository_id",
98
+ ),
87
99
  ...(optionalNonEmpty(input.repository?.owner)
88
100
  ? { owner: optionalNonEmpty(input.repository.owner) }
89
101
  : {}),
@@ -93,11 +105,15 @@ export const validateSemanticCiRequest = (input) => ({
93
105
  ...(optionalNonEmpty(input.repository?.default_branch)
94
106
  ? { default_branch: optionalNonEmpty(input.repository.default_branch) }
95
107
  : {}),
96
- merge_commit: nonEmpty(input.repository?.merge_commit, "repository.merge_commit"),
108
+ merge_commit: nonEmpty(
109
+ input.repository?.merge_commit,
110
+ "repository.merge_commit",
111
+ ),
97
112
  ...(optionalNonEmpty(input.repository?.merged_at)
98
113
  ? { merged_at: optionalNonEmpty(input.repository.merged_at) }
99
114
  : {}),
100
- ...(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
101
117
  ? {}
102
118
  : { pull_request_number: input.repository.pull_request_number }),
103
119
  ...(optionalNonEmpty(input.repository?.workflow_run_id)
@@ -105,25 +121,44 @@ export const validateSemanticCiRequest = (input) => ({
105
121
  : {}),
106
122
  },
107
123
  service: {
108
- 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
+ ),
109
128
  root_path: input.service?.root_path ?? null,
110
129
  framework: input.service?.framework ?? "fastapi",
111
130
  language: input.service?.language ?? "python",
112
131
  },
113
- allowed_source_paths: stringArray(input.allowed_source_paths, "allowed_source_paths", { min: 1 }),
114
- excluded_source_paths: stringArray(input.excluded_source_paths ?? [], "excluded_source_paths"),
115
- clue_api_base_url: nonEmpty(input.clue_api_base_url, "clue_api_base_url"),
116
- ai_provider_base_url: optionalNonEmpty(input.ai_provider_base_url) ?? "https://api.openai.com/v1",
117
- ai_model: optionalNonEmpty(input.ai_model) ?? "gpt-5.4-mini",
118
- });
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: null,
143
+ ai_provider: optionalNonEmpty(input.ai_provider),
144
+ ai_model: optionalNonEmpty(input.ai_model),
145
+ });
119
146
 
120
147
  export const buildOperationSourceKey = (method, pathTemplate) =>
121
148
  `route.${method.toUpperCase()}.${pathTemplate}`;
122
149
 
123
150
  export const validateSemanticSnapshotRequest = (input) => {
124
- return parseWithSchema(semanticSnapshotRequestSchema, input, "semantic snapshot");
151
+ return parseWithSchema(
152
+ semanticSnapshotRequestSchema,
153
+ input,
154
+ "semantic snapshot",
155
+ );
125
156
  };
126
157
 
127
158
  export const validateSemanticSnapshotResponse = (input) => {
128
- return parseWithSchema(semanticSnapshotResponseSchema, input, "semantic snapshot response");
159
+ return parseWithSchema(
160
+ semanticSnapshotResponseSchema,
161
+ input,
162
+ "semantic snapshot response",
163
+ );
129
164
  };