kodingo-cli 1.1.9 → 1.1.11
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/dist/cli.js +14 -0
- package/dist/commands/build.js +178 -0
- package/dist/commands/scan-repo.js +3 -0
- package/dist/commands/security-scan.js +27 -5
- package/dist/utils/dependency-parser.js +1 -1
- package/dist/utils/graph-builder.js +2 -0
- package/dist/utils/security-code-analyzer.js +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const sync_graph_1 = require("./commands/sync-graph");
|
|
|
18
18
|
const sync_dependencies_1 = require("./commands/sync-dependencies");
|
|
19
19
|
const security_scan_1 = require("./commands/security-scan");
|
|
20
20
|
const init_1 = require("./commands/init");
|
|
21
|
+
const build_1 = require("./commands/build");
|
|
21
22
|
const install_hook_1 = require("./commands/install-hook");
|
|
22
23
|
const update_1 = require("./commands/update");
|
|
23
24
|
const sync_1 = require("./commands/sync");
|
|
@@ -31,6 +32,19 @@ const { version } = require("../package.json");
|
|
|
31
32
|
program.name("kodingo").description("Kodingo CLI").version(version);
|
|
32
33
|
// ── init ──────────────────────────────────────────────────────────────────────
|
|
33
34
|
(0, init_1.registerInitCommand)(program);
|
|
35
|
+
// ── build ─────────────────────────────────────────────────────────────────────
|
|
36
|
+
program
|
|
37
|
+
.command("build")
|
|
38
|
+
.description("Run Kortex Build's local agent loop against this repo (test-driver for kodingo-agent)")
|
|
39
|
+
.argument("<task>", "what to build or fix")
|
|
40
|
+
.option("--repo <path>", "repo path (defaults to the current repo)")
|
|
41
|
+
.option("--max-turns <n>", "override the agent loop's turn budget")
|
|
42
|
+
.action(async (task, options) => {
|
|
43
|
+
await (0, build_1.buildCommand)(String(task), {
|
|
44
|
+
...(options.repo ? { repo: String(options.repo) } : {}),
|
|
45
|
+
...(options.maxTurns ? { maxTurns: Number.parseInt(String(options.maxTurns), 10) } : {}),
|
|
46
|
+
});
|
|
47
|
+
});
|
|
34
48
|
// ── update ────────────────────────────────────────────────────────────────────
|
|
35
49
|
program
|
|
36
50
|
.command("update")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* kodingo build
|
|
4
|
+
*
|
|
5
|
+
* Runs Kortex Build's real local agent loop (kodingo-agent) against the
|
|
6
|
+
* current repo. Per this repo's own stated identity — "a test driver and
|
|
7
|
+
* plumbing layer, not the final product UX" — this command is deliberately
|
|
8
|
+
* minimal: it exists to exercise kodingo-agent headlessly against a real
|
|
9
|
+
* repo, not to be a polished second Build UX (that's kodingo-vscode's job).
|
|
10
|
+
*
|
|
11
|
+
* The loop runs entirely in this process, on this machine's real files and
|
|
12
|
+
* shell. Every model decision is one real HTTP call to kodingo-api's
|
|
13
|
+
* POST /build/:id/agent-turn — this command never calls a model directly.
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.buildCommand = buildCommand;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const readline = __importStar(require("node:readline"));
|
|
53
|
+
const kodingo_agent_1 = require("kodingo-agent");
|
|
54
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
55
|
+
function findRepoRoot(startPath) {
|
|
56
|
+
let current = startPath;
|
|
57
|
+
while (true) {
|
|
58
|
+
if (fs.existsSync(path.join(current, ".git")))
|
|
59
|
+
return current;
|
|
60
|
+
const parent = path.dirname(current);
|
|
61
|
+
if (parent === current)
|
|
62
|
+
return startPath;
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function prompt(rl, question) {
|
|
67
|
+
return new Promise((resolve) => rl.question(question, (ans) => resolve(ans.trim())));
|
|
68
|
+
}
|
|
69
|
+
async function confirmTrust(repoRoot) {
|
|
70
|
+
if ((0, kodingo_agent_1.isProjectTrusted)(repoRoot)) {
|
|
71
|
+
console.log(`✔ ${repoRoot} is already trusted for Kortex Build.\n`);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
75
|
+
try {
|
|
76
|
+
console.log(`\nKortex Build will read, edit, and run allowed commands in:\n ${repoRoot}\n`);
|
|
77
|
+
const answer = (await prompt(rl, "Allow? [o]nce / [a]lways / [c]ancel: ")).toLowerCase();
|
|
78
|
+
if (answer.startsWith("a")) {
|
|
79
|
+
(0, kodingo_agent_1.trustProject)(repoRoot);
|
|
80
|
+
console.log("✔ Always allowed for this repo.\n");
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (answer.startsWith("o"))
|
|
84
|
+
return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
rl.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// writeWorkspaceConfig (kodingo init) stores workspace entries keyed by the
|
|
92
|
+
// raw repo root path, but getPersistence() reads them back through a
|
|
93
|
+
// lowercased normalizePath() — a pre-existing mismatch on any path with
|
|
94
|
+
// uppercase characters (e.g. /Users/... on macOS), unrelated to this
|
|
95
|
+
// command. Checking both the raw key and the global fallback here gives
|
|
96
|
+
// this command the best real chance of finding a token either way, without
|
|
97
|
+
// trying to fix that mismatch itself.
|
|
98
|
+
function resolveApiConfig(repoRoot) {
|
|
99
|
+
const config = (0, persistence_config_1.readConfig)();
|
|
100
|
+
const ws = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
|
|
101
|
+
const apiUrl = ws?.apiUrl ?? config.apiUrl;
|
|
102
|
+
const token = ws?.token ?? config.token;
|
|
103
|
+
if (!apiUrl || !token)
|
|
104
|
+
return null;
|
|
105
|
+
return { apiUrl, token };
|
|
106
|
+
}
|
|
107
|
+
async function buildCommand(input, options = {}) {
|
|
108
|
+
const repoRoot = options.repo ? path.resolve(options.repo) : findRepoRoot(process.cwd());
|
|
109
|
+
const apiConfig = resolveApiConfig(repoRoot);
|
|
110
|
+
if (!apiConfig) {
|
|
111
|
+
console.error(`✖ No Kodingo project connected for ${repoRoot}. Run \`kodingo init\` in this repo first.`);
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const allowed = await confirmTrust(repoRoot);
|
|
116
|
+
if (!allowed) {
|
|
117
|
+
console.log("✖ Cancelled.");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const apiBaseUrl = apiConfig.apiUrl.replace(/\/$/, "");
|
|
121
|
+
// Bring-your-own, if configured (~/.kodingo/byo-model.json — same file
|
|
122
|
+
// kodingo-vscode already uses) — never logged, only forwarded.
|
|
123
|
+
const byoModelConfig = (0, kodingo_agent_1.getByoModelConfig)();
|
|
124
|
+
if (byoModelConfig) {
|
|
125
|
+
console.log(`Using bring-your-own model${byoModelConfig.label ? ` (${byoModelConfig.label})` : ""} — no shared quota consumed.`);
|
|
126
|
+
}
|
|
127
|
+
console.log(`Starting Kortex Build against ${repoRoot} ...`);
|
|
128
|
+
const startRes = await fetch(`${apiBaseUrl}/build/agent-start`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": apiConfig.token },
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
input,
|
|
133
|
+
repo: path.basename(repoRoot),
|
|
134
|
+
...(byoModelConfig ? { modelConfig: byoModelConfig } : {}),
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
const startBody = (await startRes.json());
|
|
138
|
+
if (!startRes.ok || !startBody.sessionId) {
|
|
139
|
+
console.error(`✖ Could not start Build session: ${startBody.error ?? startRes.status}`);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const sessionId = startBody.sessionId;
|
|
144
|
+
console.log(`Session: ${sessionId}`);
|
|
145
|
+
const modelPort = (0, kodingo_agent_1.createHttpModelCallPort)({ apiBaseUrl, projectToken: apiConfig.token, sessionId });
|
|
146
|
+
const initialMessages = [
|
|
147
|
+
{
|
|
148
|
+
role: "system",
|
|
149
|
+
content: "You are Kortex Build, a coding agent working directly on the developer's real local repository. " +
|
|
150
|
+
"Explore before you edit: read files, grep for symbols, and look around as needed — looking is just " +
|
|
151
|
+
"another turn, you can call tools as many times as it takes to actually understand the real code " +
|
|
152
|
+
"before changing it. Only use edit_file for surgical, minimal changes to existing files; use " +
|
|
153
|
+
"write_file only for brand-new files. Before reporting done, verify your change with run_command.",
|
|
154
|
+
},
|
|
155
|
+
{ role: "user", content: input },
|
|
156
|
+
];
|
|
157
|
+
const result = await (0, kodingo_agent_1.runAgentLoop)({
|
|
158
|
+
cwd: repoRoot,
|
|
159
|
+
tools: (0, kodingo_agent_1.createDefaultToolSet)(),
|
|
160
|
+
initialMessages,
|
|
161
|
+
modelPort,
|
|
162
|
+
reporter: (0, kodingo_agent_1.createTerminalReporter)(),
|
|
163
|
+
...(options.maxTurns ? { budgets: { maxTurns: options.maxTurns } } : {}),
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
await fetch(`${apiBaseUrl}/build/${sessionId}/agent-complete`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": apiConfig.token },
|
|
169
|
+
body: JSON.stringify({ status: result.status, message: result.error }),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Best-effort — the loop's own outcome already printed to the terminal regardless.
|
|
174
|
+
}
|
|
175
|
+
if (result.status !== "completed") {
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -66,6 +66,9 @@ const SUPPORTED_EXTENSIONS = [
|
|
|
66
66
|
const IGNORED_DIRS = [
|
|
67
67
|
"node_modules", ".git", ".kortex", "dist", "build",
|
|
68
68
|
"out", ".next", "coverage", "__pycache__", "vendor",
|
|
69
|
+
".venv", "venv", "env", ".env", "site-packages",
|
|
70
|
+
"virtualenv", ".tox", "eggs", ".eggs", "wheels",
|
|
71
|
+
"target", ".gradle", ".idea", ".vscode", "bin", "obj",
|
|
69
72
|
];
|
|
70
73
|
const MAX_SYMBOLS = 200;
|
|
71
74
|
const BATCH_SIZE = 5;
|
|
@@ -18,7 +18,7 @@ const graph_builder_1 = require("../utils/graph-builder");
|
|
|
18
18
|
const security_code_analyzer_1 = require("../utils/security-code-analyzer");
|
|
19
19
|
const context_assembler_1 = require("../utils/context-assembler");
|
|
20
20
|
const ast_parser_1 = require("../utils/ast-parser");
|
|
21
|
-
const IGNORED_DIRS = new Set(["node_modules", ".git", ".kortex", "dist", "build", "vendor", "out", ".next"]);
|
|
21
|
+
const IGNORED_DIRS = new Set(["node_modules", ".git", ".kortex", "dist", "build", "vendor", "out", ".next", ".venv", "venv", "env", "site-packages", "virtualenv", ".tox", "target", ".gradle", ".idea", ".vscode", "bin", "obj"]);
|
|
22
22
|
const SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".php", ".java", ".kt", ".rb", ".c", ".cpp", ".html", ".phtml", ".twig", ".blade.php"];
|
|
23
23
|
function walkFiles(dir) {
|
|
24
24
|
const results = [];
|
|
@@ -131,13 +131,35 @@ async function securityScanCommand(options = {}) {
|
|
|
131
131
|
}
|
|
132
132
|
catch { }
|
|
133
133
|
const allCandidates = (0, security_code_analyzer_1.analyzeForSecurityCandidates)(repoRoot, graph, symbols, dynamicRules);
|
|
134
|
-
//
|
|
134
|
+
// Fetch already-confirmed open findings so we don't re-scan them —
|
|
135
|
+
// they stay visible in the dashboard as open until fixed, but we use
|
|
136
|
+
// our 30-candidate budget on NEW issues each week.
|
|
137
|
+
let confirmedFingerprints = new Set();
|
|
138
|
+
try {
|
|
139
|
+
const existingRes = await fetch(`${apiUrl}/security/findings?repo=${encodeURIComponent(repoName)}`, {
|
|
140
|
+
headers: { "X-Kodingo-Token": token },
|
|
141
|
+
});
|
|
142
|
+
if (existingRes.ok) {
|
|
143
|
+
const { findings: existingFindings } = await existingRes.json();
|
|
144
|
+
existingFindings.forEach((f) => {
|
|
145
|
+
confirmedFingerprints.add(`${f.finding_type}:${f.endpoint ?? f.symbol ?? ""}:${f.vulnerability_id ?? ""}`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
// Filter out already-confirmed findings, then sort by severity and cap at 30
|
|
135
151
|
const SEVERITY_ORDER = { critical: 0, high: 1, moderate: 2, low: 3 };
|
|
136
|
-
const
|
|
152
|
+
const newCandidates = allCandidates.filter(c => {
|
|
153
|
+
const fp = `${c.patternType}:${c.symbol ?? c.file}:${c.file}`;
|
|
154
|
+
return !confirmedFingerprints.has(fp);
|
|
155
|
+
});
|
|
156
|
+
const candidates = newCandidates
|
|
137
157
|
.sort((a, b) => (SEVERITY_ORDER[a.severity ?? 'low'] ?? 3) - (SEVERITY_ORDER[b.severity ?? 'low'] ?? 3))
|
|
138
158
|
.slice(0, 30);
|
|
139
|
-
if (!options.silent
|
|
140
|
-
|
|
159
|
+
if (!options.silent) {
|
|
160
|
+
if (allCandidates.length > 0) {
|
|
161
|
+
console.log(` Found ${allCandidates.length} candidates (${allCandidates.length - newCandidates.length} already tracked as open) — reviewing ${Math.min(candidates.length, 30)} new ones\n`);
|
|
162
|
+
}
|
|
141
163
|
}
|
|
142
164
|
if (candidates.length === 0) {
|
|
143
165
|
if (!options.silent)
|
|
@@ -154,7 +154,7 @@ function parseComposerLock(repoRoot, filePath) {
|
|
|
154
154
|
}
|
|
155
155
|
return entries;
|
|
156
156
|
}
|
|
157
|
-
const IGNORED_DIRS = new Set(["node_modules", ".git", ".kortex", "dist", "build", "vendor", "out", ".next"]);
|
|
157
|
+
const IGNORED_DIRS = new Set(["node_modules", ".git", ".kortex", "dist", "build", "vendor", "out", ".next", ".venv", "venv", "env", "site-packages", "virtualenv", ".tox", "target", ".gradle", ".idea", ".vscode", "bin", "obj"]);
|
|
158
158
|
function findManifestFiles(repoRoot) {
|
|
159
159
|
const result = { packageJson: [], packageLock: [], composerJson: [], composerLock: [] };
|
|
160
160
|
function walk(dir) {
|
|
@@ -70,6 +70,8 @@ const TS_JS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
|
70
70
|
const IGNORED_DIRS = [
|
|
71
71
|
"node_modules", ".git", ".kortex", "dist", "build",
|
|
72
72
|
"out", ".next", "coverage", "__pycache__", "vendor",
|
|
73
|
+
".venv", "venv", "env", "site-packages", "virtualenv",
|
|
74
|
+
".tox", "target", ".gradle", ".idea", ".vscode", "bin", "obj",
|
|
73
75
|
];
|
|
74
76
|
function walkFiles(dir) {
|
|
75
77
|
const results = [];
|
|
@@ -98,7 +98,7 @@ function analyzeForSecurityCandidates(repoRoot, graph, symbols, dynamicRules = [
|
|
|
98
98
|
".java": "java", ".kt": "kotlin", ".rb": "ruby", ".c": "c", ".cpp": "cpp",
|
|
99
99
|
".html": "html", ".js": "javascript", ".ts": "typescript", ".jsx": "javascript", ".tsx": "typescript"
|
|
100
100
|
};
|
|
101
|
-
const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", "vendor", "__pycache__", ".next", "out", "coverage"]);
|
|
101
|
+
const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", "vendor", "__pycache__", ".next", "out", "coverage", ".venv", "venv", "env", "site-packages", "virtualenv", ".tox", "target", ".gradle", ".idea", ".vscode", "bin", "obj"]);
|
|
102
102
|
// Get recently changed files from git (last 30 days) — scan these first
|
|
103
103
|
let priorityFiles = [];
|
|
104
104
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodingo-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"description": "Kodingo CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@typescript-eslint/types": "^8.61.1",
|
|
26
26
|
"@typescript-eslint/typescript-estree": "^8.61.1",
|
|
27
27
|
"commander": "^12.1.0",
|
|
28
|
+
"kodingo-agent": "^0.1.0",
|
|
28
29
|
"kodingo-core": "^0.2.1",
|
|
29
30
|
"open": "^11.0.0",
|
|
30
31
|
"simple-git": "^3.30.0",
|