@clue-ai/cli 0.0.3
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.
- package/README.md +30 -0
- package/bin/clue-tool.mjs +83 -0
- package/commands/claude-code/clue-init.md +27 -0
- package/commands/codex/clue-init.md +27 -0
- package/package.json +20 -0
- package/src/command-spec.mjs +73 -0
- package/src/contracts.mjs +124 -0
- package/src/fastapi-analyzer.mjs +340 -0
- package/src/init-tool.mjs +86 -0
- package/src/lifecycle-init.mjs +206 -0
- package/src/path-policy.mjs +75 -0
- package/src/public-schema.cjs +784 -0
- package/src/semantic-ci.mjs +1830 -0
- package/src/setup-tool.mjs +141 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Clue Tool
|
|
2
|
+
|
|
3
|
+
This repository owns the Codex / Claude Code SDK init tool and the client-side CI semantic generation runner.
|
|
4
|
+
|
|
5
|
+
The Clue product repository keeps SDKs, shared schemas, and API contracts. Tool implementation lives here so client repositories do not receive tool source code and the product repository does not become the tool runtime.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
/clue-init
|
|
11
|
+
clue-ai init --request clue-init-request.json --repo .
|
|
12
|
+
clue-ai semantic-ci --request clue-semantic-request.json --repo .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`/clue-init` is the standard user-facing command for Codex and Claude Code
|
|
16
|
+
wrappers. The command collects structured setup inputs and invokes the same
|
|
17
|
+
tool request/report contract as `clue-ai init`.
|
|
18
|
+
|
|
19
|
+
## Required Secrets
|
|
20
|
+
|
|
21
|
+
- `CLUE_API_KEY`
|
|
22
|
+
- `AI_PROVIDER_API_KEY`
|
|
23
|
+
|
|
24
|
+
## Boundaries
|
|
25
|
+
|
|
26
|
+
- The tool may read allowed source paths in the client repository.
|
|
27
|
+
- The tool must not read `.env`, secrets, logs, dumps, build output, or vendor directories.
|
|
28
|
+
- Raw source code is sent only from the client CI runner to the configured AI provider.
|
|
29
|
+
- Raw source code, raw SQL, bind values, function names, class names, file paths, and import graphs are not sent to Clue by default.
|
|
30
|
+
- `clue-layer.yaml` and `clue-semantic.yaml` are not committed to client repositories.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { commandSpecs } from "../src/command-spec.mjs";
|
|
5
|
+
import { runInitTool } from "../src/init-tool.mjs";
|
|
6
|
+
import { runSemanticCi } from "../src/semantic-ci.mjs";
|
|
7
|
+
import { installSetupSkills } from "../src/setup-tool.mjs";
|
|
8
|
+
|
|
9
|
+
const parseArgs = (argv) => {
|
|
10
|
+
const [command = "help", ...tokens] = argv;
|
|
11
|
+
const flags = new Map();
|
|
12
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
13
|
+
const token = tokens[index];
|
|
14
|
+
if (!token.startsWith("--")) continue;
|
|
15
|
+
const key = token.slice(2);
|
|
16
|
+
const next = tokens[index + 1];
|
|
17
|
+
if (next && !next.startsWith("--")) {
|
|
18
|
+
flags.set(key, next);
|
|
19
|
+
index += 1;
|
|
20
|
+
} else {
|
|
21
|
+
flags.set(key, true);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { command, flags };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
28
|
+
|
|
29
|
+
const usage = () => [
|
|
30
|
+
"Usage:",
|
|
31
|
+
" /clue-init",
|
|
32
|
+
" clue-ai setup",
|
|
33
|
+
" clue-ai init --request clue-init-request.json --repo .",
|
|
34
|
+
" clue-ai semantic-ci --request clue-semantic-request.json --repo .",
|
|
35
|
+
].join("\n");
|
|
36
|
+
|
|
37
|
+
const main = async () => {
|
|
38
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
39
|
+
if (command === "help" || flags.has("help")) {
|
|
40
|
+
process.stdout.write(`${usage()}\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (command === "commands") {
|
|
45
|
+
process.stdout.write(`${JSON.stringify({ commands: commandSpecs }, null, 2)}\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const requestPath = flags.get("request");
|
|
50
|
+
const repoRoot = resolve(String(flags.get("repo") || "."));
|
|
51
|
+
if (command === "setup") {
|
|
52
|
+
const report = await installSetupSkills({
|
|
53
|
+
repoRoot,
|
|
54
|
+
target: typeof flags.get("target") === "string" ? flags.get("target") : undefined,
|
|
55
|
+
});
|
|
56
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof requestPath !== "string") {
|
|
61
|
+
throw new Error("--request is required");
|
|
62
|
+
}
|
|
63
|
+
const request = await readJson(resolve(requestPath));
|
|
64
|
+
|
|
65
|
+
if (command === "init") {
|
|
66
|
+
const report = await runInitTool({ repoRoot, request });
|
|
67
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (command === "semantic-ci") {
|
|
72
|
+
const result = await runSemanticCi({ repoRoot, request, env: process.env });
|
|
73
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(`Unknown command: ${command}\n${usage()}`);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
main().catch((error) => {
|
|
81
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# /clue-init
|
|
2
|
+
|
|
3
|
+
Run the Clue SDK initialization tool with structured inputs.
|
|
4
|
+
|
|
5
|
+
Required fields:
|
|
6
|
+
|
|
7
|
+
- `project_key`
|
|
8
|
+
- `service_key`
|
|
9
|
+
- `framework`
|
|
10
|
+
- `backend_root_path`
|
|
11
|
+
- `environment`
|
|
12
|
+
- `allowed_source_paths`
|
|
13
|
+
- `excluded_source_paths`
|
|
14
|
+
|
|
15
|
+
Required secrets:
|
|
16
|
+
|
|
17
|
+
- `CLUE_API_KEY`
|
|
18
|
+
- `AI_PROVIDER_API_KEY`
|
|
19
|
+
|
|
20
|
+
Behavior:
|
|
21
|
+
|
|
22
|
+
1. Collect the required fields as structured input.
|
|
23
|
+
2. Build the canonical init request.
|
|
24
|
+
3. Run `clue-ai init --request <generated-request.json> --repo .`.
|
|
25
|
+
4. Show the generated report and low-confidence review points.
|
|
26
|
+
|
|
27
|
+
Do not ask the user to write a free-form setup prompt.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# /clue-init
|
|
2
|
+
|
|
3
|
+
Run the Clue SDK initialization tool with structured inputs.
|
|
4
|
+
|
|
5
|
+
Required fields:
|
|
6
|
+
|
|
7
|
+
- `project_key`
|
|
8
|
+
- `service_key`
|
|
9
|
+
- `framework`
|
|
10
|
+
- `backend_root_path`
|
|
11
|
+
- `environment`
|
|
12
|
+
- `allowed_source_paths`
|
|
13
|
+
- `excluded_source_paths`
|
|
14
|
+
|
|
15
|
+
Required secrets:
|
|
16
|
+
|
|
17
|
+
- `CLUE_API_KEY`
|
|
18
|
+
- `AI_PROVIDER_API_KEY`
|
|
19
|
+
|
|
20
|
+
Behavior:
|
|
21
|
+
|
|
22
|
+
1. Collect the required fields as structured input.
|
|
23
|
+
2. Build the canonical init request.
|
|
24
|
+
3. Run `clue-ai init --request <generated-request.json> --repo .`.
|
|
25
|
+
4. Show the generated report and low-confidence review points.
|
|
26
|
+
|
|
27
|
+
Do not ask the user to write a free-form setup prompt.
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clue-ai/cli",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"clue-ai": "bin/clue-tool.mjs"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"commands/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"zod": "^4.3.6"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const CLUE_INIT_COMMAND_NAME = "/clue-init";
|
|
2
|
+
|
|
3
|
+
export const REQUIRED_SECRET_NAMES = [
|
|
4
|
+
"CLUE_API_KEY",
|
|
5
|
+
"AI_PROVIDER_API_KEY",
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export const CLUE_INIT_COMMAND_FIELDS = [
|
|
9
|
+
"project_key",
|
|
10
|
+
"service_key",
|
|
11
|
+
"framework",
|
|
12
|
+
"backend_root_path",
|
|
13
|
+
"environment",
|
|
14
|
+
"allowed_source_paths",
|
|
15
|
+
"excluded_source_paths",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const CLUE_INIT_COMMAND = {
|
|
19
|
+
name: CLUE_INIT_COMMAND_NAME,
|
|
20
|
+
description: "Initialize Clue SDK lifecycle capture and semantic CI.",
|
|
21
|
+
supported_tools: ["codex", "claude_code"],
|
|
22
|
+
request_contract: "clueInitToolRequestSchema",
|
|
23
|
+
report_contract: "clueInitToolReportSchema",
|
|
24
|
+
required_fields: CLUE_INIT_COMMAND_FIELDS,
|
|
25
|
+
required_secret_names: REQUIRED_SECRET_NAMES,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const commandSpecs = [CLUE_INIT_COMMAND];
|
|
29
|
+
|
|
30
|
+
const requireField = (input, field) => {
|
|
31
|
+
const value = input?.[field];
|
|
32
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
33
|
+
throw new Error(`${field} is required for ${CLUE_INIT_COMMAND_NAME}`);
|
|
34
|
+
}
|
|
35
|
+
return value.trim();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizeStringArray = (value, fallback, field) => {
|
|
39
|
+
const source = value === undefined ? fallback : value;
|
|
40
|
+
if (!Array.isArray(source)) {
|
|
41
|
+
throw new Error(`${field} must be an array for ${CLUE_INIT_COMMAND_NAME}`);
|
|
42
|
+
}
|
|
43
|
+
return source
|
|
44
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
45
|
+
.map((entry) => entry.trim());
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const buildClueInitRequestFromCommandInput = ({
|
|
49
|
+
targetTool,
|
|
50
|
+
input,
|
|
51
|
+
}) => {
|
|
52
|
+
const backendRootPath = requireField(input, "backend_root_path");
|
|
53
|
+
return {
|
|
54
|
+
target_tool: targetTool,
|
|
55
|
+
project_key: requireField(input, "project_key"),
|
|
56
|
+
service_key: requireField(input, "service_key"),
|
|
57
|
+
framework: requireField(input, "framework"),
|
|
58
|
+
environment: requireField(input, "environment"),
|
|
59
|
+
allowed_source_paths: normalizeStringArray(
|
|
60
|
+
input?.allowed_source_paths,
|
|
61
|
+
[backendRootPath],
|
|
62
|
+
"allowed_source_paths",
|
|
63
|
+
),
|
|
64
|
+
excluded_source_paths: normalizeStringArray(
|
|
65
|
+
input?.excluded_source_paths,
|
|
66
|
+
[],
|
|
67
|
+
"excluded_source_paths",
|
|
68
|
+
),
|
|
69
|
+
...(typeof input?.ci_workflow_path === "string" && input.ci_workflow_path.trim()
|
|
70
|
+
? { ci_workflow_path: input.ci_workflow_path.trim() }
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const schemaPackage = require("./public-schema.cjs");
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
clueInitToolRequestSchema,
|
|
8
|
+
clueInitToolReportSchema,
|
|
9
|
+
semanticSnapshotRequestSchema,
|
|
10
|
+
} = schemaPackage;
|
|
11
|
+
|
|
12
|
+
const formatSchemaError = (result, field) => {
|
|
13
|
+
const issues = result.error.issues.map((issue) => {
|
|
14
|
+
const path = issue.path.length ? issue.path.join(".") : field;
|
|
15
|
+
return `${path}: ${issue.message}`;
|
|
16
|
+
});
|
|
17
|
+
return `${field} is invalid: ${issues.join("; ")}`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const parseWithSchema = (schema, input, field) => {
|
|
21
|
+
const result = schema.safeParse(input);
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
throw new Error(formatSchemaError(result, field));
|
|
24
|
+
}
|
|
25
|
+
return result.data;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const nonEmpty = (value, field) => {
|
|
29
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
30
|
+
throw new Error(`${field} is required`);
|
|
31
|
+
}
|
|
32
|
+
return value.trim();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const optionalNonEmpty = (value) =>
|
|
36
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
37
|
+
|
|
38
|
+
const stringArray = (value, field, { min = 0 } = {}) => {
|
|
39
|
+
if (!Array.isArray(value)) {
|
|
40
|
+
throw new Error(`${field} must be an array`);
|
|
41
|
+
}
|
|
42
|
+
const result = value.map((entry) => nonEmpty(entry, field));
|
|
43
|
+
if (result.length < min) {
|
|
44
|
+
throw new Error(`${field} must include at least ${min} item(s)`);
|
|
45
|
+
}
|
|
46
|
+
return [...new Set(result)];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const validateInitRequest = (input) => {
|
|
50
|
+
const parsed = parseWithSchema(clueInitToolRequestSchema, input, "init request");
|
|
51
|
+
return {
|
|
52
|
+
...parsed,
|
|
53
|
+
allowed_source_paths: [...new Set(parsed.allowed_source_paths)],
|
|
54
|
+
excluded_source_paths: [...new Set(parsed.excluded_source_paths ?? [])],
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const buildInitReport = ({ request, lifecycleInsertions, warnings = [] }) =>
|
|
59
|
+
parseWithSchema(
|
|
60
|
+
clueInitToolReportSchema,
|
|
61
|
+
{
|
|
62
|
+
target_tool: request.target_tool,
|
|
63
|
+
ci_workflow_path: request.ci_workflow_path,
|
|
64
|
+
ci_workflow_added: true,
|
|
65
|
+
required_secrets: ["CLUE_API_KEY", "AI_PROVIDER_API_KEY"],
|
|
66
|
+
lifecycle_insertions: lifecycleInsertions,
|
|
67
|
+
semantic_generation_timing: "after_merge_ci",
|
|
68
|
+
semantic_preview_generated: false,
|
|
69
|
+
client_repo_generated_files_committed: false,
|
|
70
|
+
clue_track_inserted: false,
|
|
71
|
+
clue_dom_tag_inserted: false,
|
|
72
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
73
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
74
|
+
warnings,
|
|
75
|
+
},
|
|
76
|
+
"init report",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export const validateSemanticCiRequest = (input) => ({
|
|
80
|
+
project_key: nonEmpty(input.project_key, "project_key"),
|
|
81
|
+
environment: nonEmpty(input.environment, "environment"),
|
|
82
|
+
service_key: nonEmpty(input.service_key, "service_key"),
|
|
83
|
+
repository: {
|
|
84
|
+
provider: nonEmpty(input.repository?.provider, "repository.provider"),
|
|
85
|
+
repository_id: nonEmpty(input.repository?.repository_id, "repository.repository_id"),
|
|
86
|
+
...(optionalNonEmpty(input.repository?.owner)
|
|
87
|
+
? { owner: optionalNonEmpty(input.repository.owner) }
|
|
88
|
+
: {}),
|
|
89
|
+
...(optionalNonEmpty(input.repository?.name)
|
|
90
|
+
? { name: optionalNonEmpty(input.repository.name) }
|
|
91
|
+
: {}),
|
|
92
|
+
...(optionalNonEmpty(input.repository?.default_branch)
|
|
93
|
+
? { default_branch: optionalNonEmpty(input.repository.default_branch) }
|
|
94
|
+
: {}),
|
|
95
|
+
merge_commit: nonEmpty(input.repository?.merge_commit, "repository.merge_commit"),
|
|
96
|
+
...(optionalNonEmpty(input.repository?.merged_at)
|
|
97
|
+
? { merged_at: optionalNonEmpty(input.repository.merged_at) }
|
|
98
|
+
: {}),
|
|
99
|
+
...(input.repository?.pull_request_number === undefined || input.repository?.pull_request_number === null
|
|
100
|
+
? {}
|
|
101
|
+
: { pull_request_number: input.repository.pull_request_number }),
|
|
102
|
+
...(optionalNonEmpty(input.repository?.workflow_run_id)
|
|
103
|
+
? { workflow_run_id: optionalNonEmpty(input.repository.workflow_run_id) }
|
|
104
|
+
: {}),
|
|
105
|
+
},
|
|
106
|
+
service: {
|
|
107
|
+
service_key: nonEmpty(input.service?.service_key ?? input.service_key, "service.service_key"),
|
|
108
|
+
root_path: input.service?.root_path ?? null,
|
|
109
|
+
framework: input.service?.framework ?? "fastapi",
|
|
110
|
+
language: input.service?.language ?? "python",
|
|
111
|
+
},
|
|
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
|
+
});
|
|
118
|
+
|
|
119
|
+
export const buildOperationSourceKey = (method, pathTemplate) =>
|
|
120
|
+
`route.${method.toUpperCase()}.${pathTemplate}`;
|
|
121
|
+
|
|
122
|
+
export const validateSemanticSnapshotRequest = (input) => {
|
|
123
|
+
return parseWithSchema(semanticSnapshotRequestSchema, input, "semantic snapshot");
|
|
124
|
+
};
|