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 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
- // Cap at 30 candidates for LLM judgment — prioritise critical and high severity
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 candidates = allCandidates
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 && allCandidates.length > 30) {
140
- console.log(` Found ${allCandidates.length} candidates — reviewing top 30 by severity\n`);
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.9",
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",