kodingo-cli 1.0.38 → 1.1.1

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
@@ -14,6 +14,9 @@ const deny_1 = require("./commands/deny");
14
14
  const scan_git_1 = require("./commands/scan-git");
15
15
  const scan_claude_1 = require("./commands/scan-claude");
16
16
  const scan_repo_1 = require("./commands/scan-repo");
17
+ const sync_graph_1 = require("./commands/sync-graph");
18
+ const sync_dependencies_1 = require("./commands/sync-dependencies");
19
+ const security_scan_1 = require("./commands/security-scan");
17
20
  const init_1 = require("./commands/init");
18
21
  const install_hook_1 = require("./commands/install-hook");
19
22
  const update_1 = require("./commands/update");
@@ -231,6 +234,36 @@ program
231
234
  payload.repo = String(options.repo);
232
235
  await (0, scan_repo_1.scanRepoCommand)(payload);
233
236
  });
237
+ program
238
+ .command("sync-graph")
239
+ .description("Build and sync the structural relationship graph for the current repo")
240
+ .option("--repo <path>", "repo path to scan")
241
+ .action(async (options) => {
242
+ const payload = {};
243
+ if (options.repo)
244
+ payload.repo = String(options.repo);
245
+ await (0, sync_graph_1.syncGraphCommand)(payload);
246
+ });
247
+ program
248
+ .command("sync-dependencies")
249
+ .description("Extract and sync declared dependencies for security scanning")
250
+ .option("--repo <path>", "repo path to scan")
251
+ .action(async (options) => {
252
+ const payload = {};
253
+ if (options.repo)
254
+ payload.repo = String(options.repo);
255
+ await (0, sync_dependencies_1.syncDependenciesCommand)(payload);
256
+ });
257
+ program
258
+ .command("security-scan")
259
+ .description("Scan synced dependencies for known vulnerabilities")
260
+ .option("--repo <path>", "repo path to scan")
261
+ .action(async (options) => {
262
+ const payload = {};
263
+ if (options.repo)
264
+ payload.repo = String(options.repo);
265
+ await (0, security_scan_1.securityScanCommand)(payload);
266
+ });
234
267
  program
235
268
  .command("context")
236
269
  .description("Get affirmed memories for a file — designed for AI agents")
@@ -289,7 +289,24 @@ function registerInitCommand(program) {
289
289
  catch { }
290
290
  console.log(`✔ Connected. Token saved to ${persistence_config_1.CONFIG_PATH}`);
291
291
  console.log("\nKortex is now watching this repo.");
292
- console.log("Run `kodingo install-hook` to capture memories on every commit.\n");
292
+ // Install the git hook automatically — no manual step required.
293
+ // This is what triggers scan-git and sync-dependencies on every future
294
+ // commit, silently in the background.
295
+ try {
296
+ const { installHookCommand } = await Promise.resolve().then(() => __importStar(require("./install-hook")));
297
+ await installHookCommand({ repo: repoRoot });
298
+ console.log("✔ Git hook installed — memories and dependency data will stay current automatically on every commit.\n");
299
+ }
300
+ catch (err) {
301
+ console.log(`⚠ Could not auto-install git hook (${err.message}). Run \`kodingo install-hook\` manually to enable automatic capture.\n`);
302
+ }
303
+ // Initial dependency sync — so security scanning has data immediately,
304
+ // rather than waiting for the next commit to trigger the hook.
305
+ try {
306
+ const { syncDependenciesCommand } = await Promise.resolve().then(() => __importStar(require("./sync-dependencies")));
307
+ await syncDependenciesCommand({ repo: repoRoot, silent: true });
308
+ }
309
+ catch { }
293
310
  // Auto-scan existing codebase
294
311
  try {
295
312
  const { scanRepoCommand } = await Promise.resolve().then(() => __importStar(require("./scan-repo")));
@@ -18,6 +18,8 @@ async function installHookCommand(options) {
18
18
  "# <kodingo>",
19
19
  "# Kodingo: auto-infer decision memory from git commits",
20
20
  `"${kodingoBin}" scan-git --repo "$(git rev-parse --show-toplevel)" &`,
21
+ "# Kodingo Security: keep dependency data current for vulnerability scanning",
22
+ `"${kodingoBin}" sync-dependencies --repo "$(git rev-parse --show-toplevel)" &`,
21
23
  "# </kodingo>",
22
24
  ].join("\n");
23
25
  // Pre-commit hook — runs synchronously to enforce guardrails
@@ -55,6 +55,8 @@ const path_1 = __importDefault(require("path"));
55
55
  const persistence_config_1 = require("../utils/persistence-config");
56
56
  const os = __importStar(require("os"));
57
57
  const ast_parser_1 = require("../utils/ast-parser");
58
+ const graph_builder_1 = require("../utils/graph-builder");
59
+ const context_assembler_1 = require("../utils/context-assembler");
58
60
  const API_BASE = "https://kodingo-api.onrender.com";
59
61
  const SUPPORTED_EXTENSIONS = [
60
62
  ".ts", ".tsx", ".js", ".jsx",
@@ -124,7 +126,8 @@ async function fetchExistingSymbols(token, repo, apiUrl) {
124
126
  if (!res.ok)
125
127
  break;
126
128
  const data = await res.json();
127
- for (const m of data.memories ?? []) {
129
+ const page = data.data ?? [];
130
+ for (const m of page) {
128
131
  if (m.symbol) {
129
132
  // Store both symbol-only and symbol::repo keys for accurate matching
130
133
  known.add(m.symbol);
@@ -133,8 +136,8 @@ async function fetchExistingSymbols(token, repo, apiUrl) {
133
136
  }
134
137
  }
135
138
  offset += limit;
136
- if (offset >= (data.total ?? 0))
137
- break;
139
+ if (page.length < limit)
140
+ break; // fewer than a full page means we've reached the end
138
141
  }
139
142
  }
140
143
  catch { }
@@ -150,12 +153,29 @@ async function sendSignal(token, apiUrl, memoryId, weightClass) {
150
153
  }
151
154
  catch { }
152
155
  }
153
- async function inferAndCapture(token, symbol, repo) {
156
+ async function inferAndCapture(token, symbol, repo, graph, repoRoot) {
154
157
  try {
155
- const inferRes = await fetch(`${API_BASE}/infer`, {
158
+ // Use contextual inference when the graph knows this symbol — richer, structure-grounded reasoning.
159
+ // Falls back to isolated /infer for symbols the graph didn't resolve (e.g. parse failures).
160
+ const useContextual = graph && symbol.fingerprint && graph.nodes[symbol.fingerprint];
161
+ const endpoint = useContextual ? "/infer/contextual" : "/infer";
162
+ let body = { symbol: symbol.name, code: symbol.code };
163
+ if (useContextual && graph && symbol.fingerprint) {
164
+ const assembled = (0, context_assembler_1.assembleContext)(repoRoot, graph, symbol.fingerprint);
165
+ if (assembled) {
166
+ body = {
167
+ symbol: symbol.name,
168
+ code: symbol.code,
169
+ callers: assembled.callers.map(c => ({ name: c.name, snippet: c.snippet, confidence: c.confidence })),
170
+ callees: assembled.callees.map(c => ({ name: c.name, snippet: c.snippet, confidence: c.confidence })),
171
+ weakestConfidence: assembled.weakestConfidence,
172
+ };
173
+ }
174
+ }
175
+ const inferRes = await fetch(`${API_BASE}${endpoint}`, {
156
176
  method: "POST",
157
177
  headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
158
- body: JSON.stringify({ symbol: symbol.name, code: symbol.code }),
178
+ body: JSON.stringify(body),
159
179
  });
160
180
  if (!inferRes.ok)
161
181
  return false;
@@ -217,8 +237,27 @@ async function scanRepoCommand(options = {}) {
217
237
  }
218
238
  // Fetch all existing memory symbols in one bulk call
219
239
  const { known: existingSymbols, idMap: existingIds } = await fetchExistingSymbols(token, repoName, apiUrl);
220
- // Shuffle so repeated runs cover different parts of the codebase
221
- const shuffled = [...allSymbols].sort(() => Math.random() - 0.5);
240
+ // Build (or reuse) the structural graph — used to order symbols by
241
+ // centrality (most structurally critical first) instead of randomly,
242
+ // and to assemble context for richer, structure-grounded inference.
243
+ let graph = null;
244
+ try {
245
+ graph = await (0, graph_builder_1.buildGraph)(repoRoot);
246
+ (0, graph_builder_1.saveGraph)(repoRoot, graph);
247
+ }
248
+ catch {
249
+ graph = null;
250
+ }
251
+ const ordered = graph
252
+ ? [...allSymbols].sort((a, b) => {
253
+ const centralityA = a.fingerprint && graph.nodes[a.fingerprint] ? graph.nodes[a.fingerprint].centrality : -1;
254
+ const centralityB = b.fingerprint && graph.nodes[b.fingerprint] ? graph.nodes[b.fingerprint].centrality : -1;
255
+ if (centralityA !== centralityB)
256
+ return centralityB - centralityA;
257
+ return Math.random() - 0.5;
258
+ })
259
+ : [...allSymbols].sort(() => Math.random() - 0.5);
260
+ const shuffled = ordered;
222
261
  // Filter locally — no per-symbol API calls needed
223
262
  const toCapture = [];
224
263
  const seen = new Set();
@@ -253,7 +292,7 @@ async function scanRepoCommand(options = {}) {
253
292
  let captured = 0;
254
293
  for (let i = 0; i < toCapture.length; i += BATCH_SIZE) {
255
294
  const batch = toCapture.slice(i, i + BATCH_SIZE);
256
- const results = await Promise.all(batch.map(sym => inferAndCapture(token, sym, repoName)));
295
+ const results = await Promise.all(batch.map(sym => inferAndCapture(token, sym, repoName, graph, repoRoot)));
257
296
  const batchCaptured = results.filter(Boolean).length;
258
297
  captured += batchCaptured;
259
298
  if (!options.silent) {
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /**
3
+ * kodingo security-scan
4
+ *
5
+ * Triggers a vulnerability scan against whatever dependencies were last
6
+ * synced via 'kodingo sync-dependencies', then prints a ranked report.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.securityScanCommand = securityScanCommand;
13
+ const path_1 = __importDefault(require("path"));
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const persistence_config_1 = require("../utils/persistence-config");
16
+ const repo_scope_1 = require("../utils/repo-scope");
17
+ const graph_builder_1 = require("../utils/graph-builder");
18
+ const security_code_analyzer_1 = require("../utils/security-code-analyzer");
19
+ const context_assembler_1 = require("../utils/context-assembler");
20
+ const ast_parser_1 = require("../utils/ast-parser");
21
+ const IGNORED_DIRS = new Set(["node_modules", ".git", ".kortex", "dist", "build", "vendor", "out", ".next"]);
22
+ const SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".php", ".java", ".kt", ".rb", ".c", ".cpp"];
23
+ function walkFiles(dir) {
24
+ const results = [];
25
+ try {
26
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (IGNORED_DIRS.has(entry.name))
29
+ continue;
30
+ const fullPath = path_1.default.join(dir, entry.name);
31
+ if (entry.isDirectory()) {
32
+ results.push(...walkFiles(fullPath));
33
+ }
34
+ else if (entry.isFile() && SUPPORTED_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
35
+ results.push(fullPath);
36
+ }
37
+ }
38
+ }
39
+ catch { }
40
+ return results;
41
+ }
42
+ const SEVERITY_ICON = {
43
+ critical: "🔴", high: "🟠", moderate: "🟡", low: "🔵", unknown: "⚪",
44
+ };
45
+ async function securityScanCommand(options = {}) {
46
+ const repoRoot = (0, repo_scope_1.resolveRepoScope)(options.repo ? { repo: options.repo } : {}) ?? process.cwd();
47
+ const config = (0, persistence_config_1.readConfig)();
48
+ const wsConfig = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
49
+ const token = wsConfig?.token ?? "";
50
+ const apiUrl = wsConfig?.apiUrl ?? config.apiUrl ?? "https://kodingo-api.onrender.com";
51
+ if (!token) {
52
+ if (!options.silent)
53
+ console.error("✖ This repo is not connected to Kodingo. Run `kodingo init` first.");
54
+ return false;
55
+ }
56
+ const repoName = path_1.default.basename(repoRoot);
57
+ if (!options.silent)
58
+ console.log(`\n🔒 Kortex Security — scanning ${repoName} for known vulnerabilities...\n`);
59
+ try {
60
+ const scanRes = await fetch(`${apiUrl}/security/scan`, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
63
+ body: JSON.stringify({ repo: repoName }),
64
+ });
65
+ if (!scanRes.ok) {
66
+ if (!options.silent)
67
+ console.error(` ✖ Scan failed: ${scanRes.status} ${await scanRes.text()}`);
68
+ return false;
69
+ }
70
+ const scanData = await scanRes.json();
71
+ if (scanData.message) {
72
+ if (!options.silent)
73
+ console.log(` ${scanData.message}`);
74
+ return true;
75
+ }
76
+ if (!options.silent)
77
+ console.log(` Checked ${scanData.scanned} dependencies against known vulnerability databases\n`);
78
+ const findingsRes = await fetch(`${apiUrl}/security/findings?repo=${encodeURIComponent(repoName)}`, {
79
+ headers: { "X-Kodingo-Token": token },
80
+ });
81
+ if (!findingsRes.ok)
82
+ return true; // scan itself succeeded even if the report fetch fails
83
+ const { findings } = await findingsRes.json();
84
+ if (findings.length === 0) {
85
+ if (!options.silent)
86
+ console.log(" ✔ No known vulnerabilities found in your dependencies.");
87
+ return true;
88
+ }
89
+ if (!options.silent) {
90
+ console.log(` ⚠ ${findings.length} vulnerabilit${findings.length === 1 ? "y" : "ies"} found:\n`);
91
+ for (const f of findings) {
92
+ const icon = SEVERITY_ICON[f.severity] ?? "⚪";
93
+ console.log(` ${icon} ${f.severity.toUpperCase()} — ${f.package_name}@${f.version}`);
94
+ console.log(` ${f.summary}`);
95
+ if (f.fixed_version)
96
+ console.log(` Fix: upgrade to ${f.fixed_version} or later`);
97
+ if (f.alsoAffectsRepoCount && f.alsoAffectsRepoCount > 0) {
98
+ console.log(` ⚠ Also present in ${f.alsoAffectsRepoCount} other repo${f.alsoAffectsRepoCount === 1 ? "" : "s"} in this project`);
99
+ }
100
+ console.log(` ${f.vulnerability_id}\n`);
101
+ }
102
+ }
103
+ // ── Code-level analysis — Layer 1 (local) + Layer 2 (server LLM judgment) ──
104
+ if (!options.silent)
105
+ console.log(`\n🔍 Analysing code for vulnerable patterns and missing auth...\n`);
106
+ try {
107
+ const graph = await (0, graph_builder_1.buildGraph)(repoRoot);
108
+ (0, graph_builder_1.saveGraph)(repoRoot, graph);
109
+ const files = walkFiles(repoRoot);
110
+ const symbols = [];
111
+ for (const file of files) {
112
+ try {
113
+ const source = fs_1.default.readFileSync(file, "utf-8");
114
+ symbols.push(...(await (0, ast_parser_1.parseFile)(file, source)));
115
+ }
116
+ catch { }
117
+ }
118
+ const candidates = (0, security_code_analyzer_1.analyzeForSecurityCandidates)(repoRoot, graph, symbols);
119
+ if (candidates.length === 0) {
120
+ if (!options.silent)
121
+ console.log(" ✔ No suspicious patterns detected.");
122
+ }
123
+ else {
124
+ const codeScanRes = await fetch(`${apiUrl}/security/scan-code`, {
125
+ method: "POST",
126
+ headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
127
+ body: JSON.stringify({ repo: repoName, candidates }),
128
+ });
129
+ if (codeScanRes.ok) {
130
+ const codeResult = await codeScanRes.json();
131
+ if (!options.silent) {
132
+ console.log(` Checked ${codeResult.scanned} candidates — ${codeResult.findingsCreated} confirmed, ${codeResult.dismissed} dismissed as false positives after review\n`);
133
+ }
134
+ }
135
+ else if (!options.silent) {
136
+ console.error(` ✖ Code analysis failed: ${codeScanRes.status}`);
137
+ }
138
+ }
139
+ }
140
+ catch (err) {
141
+ if (!options.silent)
142
+ console.error(` ✖ Code analysis failed: ${err.message}`);
143
+ }
144
+ // ── Decision drift — does current code still honour past decisions? ──────
145
+ if (!options.silent)
146
+ console.log(`\n📜 Checking for drift from past decisions...\n`);
147
+ try {
148
+ const graph = await (0, graph_builder_1.buildGraph)(repoRoot);
149
+ const files = walkFiles(repoRoot);
150
+ const symbols = [];
151
+ for (const file of files) {
152
+ try {
153
+ const source = fs_1.default.readFileSync(file, "utf-8");
154
+ symbols.push(...(await (0, ast_parser_1.parseFile)(file, source)));
155
+ }
156
+ catch { }
157
+ }
158
+ const symbolNames = Array.from(new Set(symbols.map(s => s.name)));
159
+ if (symbolNames.length > 0) {
160
+ const candidatesRes = await fetch(`${apiUrl}/security/decision-drift-candidates`, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
163
+ body: JSON.stringify({ repo: repoName, symbols: symbolNames.slice(0, 3000) }),
164
+ });
165
+ if (candidatesRes.ok) {
166
+ const { matches } = await candidatesRes.json();
167
+ if (matches.length === 0) {
168
+ if (!options.silent)
169
+ console.log(" No symbols with affirmed decisions found yet.");
170
+ }
171
+ else {
172
+ const checks = [];
173
+ for (const match of matches) {
174
+ const sym = symbols.find(s => s.name === match.symbol);
175
+ if (!sym)
176
+ continue;
177
+ const assembled = (0, context_assembler_1.assembleContext)(repoRoot, graph, sym.fingerprint);
178
+ if (!assembled)
179
+ continue;
180
+ checks.push({
181
+ symbol: match.symbol,
182
+ file: sym.file,
183
+ fingerprint: sym.fingerprint,
184
+ currentBody: assembled.targetBody,
185
+ decisionTitle: match.decisionTitle,
186
+ decisionContent: match.decisionContent,
187
+ });
188
+ }
189
+ if (checks.length > 0) {
190
+ const driftRes = await fetch(`${apiUrl}/security/scan-decision-drift`, {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
193
+ body: JSON.stringify({ repo: repoName, checks }),
194
+ });
195
+ if (driftRes.ok) {
196
+ const driftResult = await driftRes.json();
197
+ if (!options.silent) {
198
+ console.log(` Checked ${driftResult.scanned} symbols against past decisions — ${driftResult.findingsCreated} drifted, ${driftResult.noDrift} still honoured\n`);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ catch (err) {
207
+ if (!options.silent)
208
+ console.error(` ✖ Decision drift check failed: ${err.message}`);
209
+ }
210
+ return true;
211
+ }
212
+ catch (err) {
213
+ if (!options.silent)
214
+ console.error(` ✖ Scan failed: ${err.message}`);
215
+ return false;
216
+ }
217
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * kodingo sync-dependencies
4
+ *
5
+ * Extracts declared dependencies from the current repo (package.json/lock,
6
+ * composer.json/lock) and syncs them to the server for security scanning.
7
+ * Run this before 'kodingo security-scan' — the scan works off whatever was
8
+ * last synced here.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.syncDependenciesCommand = syncDependenciesCommand;
15
+ const path_1 = __importDefault(require("path"));
16
+ const persistence_config_1 = require("../utils/persistence-config");
17
+ const repo_scope_1 = require("../utils/repo-scope");
18
+ const dependency_parser_1 = require("../utils/dependency-parser");
19
+ async function syncDependenciesCommand(options = {}) {
20
+ const repoRoot = (0, repo_scope_1.resolveRepoScope)(options.repo ? { repo: options.repo } : {}) ?? process.cwd();
21
+ const config = (0, persistence_config_1.readConfig)();
22
+ const wsConfig = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
23
+ const token = wsConfig?.token ?? "";
24
+ const apiUrl = wsConfig?.apiUrl ?? config.apiUrl ?? "https://kodingo-api.onrender.com";
25
+ if (!token) {
26
+ if (!options.silent)
27
+ console.error("✖ This repo is not connected to Kodingo. Run `kodingo init` first.");
28
+ return false;
29
+ }
30
+ const repoName = path_1.default.basename(repoRoot);
31
+ if (!options.silent)
32
+ console.log(`\n🔒 Kortex Security — scanning dependencies for ${repoName}...\n`);
33
+ const deps = (0, dependency_parser_1.extractDependencies)(repoRoot);
34
+ if (deps.length === 0) {
35
+ if (!options.silent)
36
+ console.log(" No package.json or composer.json found. Nothing to sync.");
37
+ return true;
38
+ }
39
+ if (!options.silent)
40
+ console.log(` Found ${deps.length} dependencies`);
41
+ try {
42
+ const res = await fetch(`${apiUrl}/security/sync`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
45
+ body: JSON.stringify({
46
+ repo: repoName,
47
+ dependencies: deps.map(d => ({
48
+ ecosystem: d.ecosystem,
49
+ packageName: d.packageName,
50
+ version: d.version,
51
+ manifestFile: d.manifestFile,
52
+ })),
53
+ }),
54
+ });
55
+ if (!res.ok) {
56
+ if (!options.silent)
57
+ console.error(` ✖ Sync failed: ${res.status} ${await res.text()}`);
58
+ return false;
59
+ }
60
+ if (!options.silent)
61
+ console.log(`\n✔ Synced ${deps.length} dependencies for ${repoName}`);
62
+ return true;
63
+ }
64
+ catch (err) {
65
+ if (!options.silent)
66
+ console.error(` ✖ Sync failed: ${err.message}`);
67
+ return false;
68
+ }
69
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * kodingo sync-graph
4
+ *
5
+ * Builds the structural relationship graph for the current repo, diffs it
6
+ * against the previously saved local graph, and syncs only the diff to the
7
+ * server. Safe to run repeatedly — nodes are upserted, edges are additive
8
+ * with soft-delete on removal.
9
+ *
10
+ * Designed to be fast on repeat runs since only changed files need re-parsing
11
+ * in a future incremental version; for now it does a full local rebuild but
12
+ * only transmits the diff over the network.
13
+ */
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.syncGraphCommand = syncGraphCommand;
19
+ const path_1 = __importDefault(require("path"));
20
+ const persistence_config_1 = require("../utils/persistence-config");
21
+ const repo_scope_1 = require("../utils/repo-scope");
22
+ const graph_builder_1 = require("../utils/graph-builder");
23
+ async function syncGraphCommand(options = {}) {
24
+ const repoRoot = (0, repo_scope_1.resolveRepoScope)(options.repo ? { repo: options.repo } : {}) ?? process.cwd();
25
+ const config = (0, persistence_config_1.readConfig)();
26
+ const wsConfig = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
27
+ const token = wsConfig?.token ?? "";
28
+ const apiUrl = wsConfig?.apiUrl ?? config.apiUrl ?? "https://kodingo-api.onrender.com";
29
+ if (!token) {
30
+ if (!options.silent)
31
+ console.error("✖ This repo is not connected to Kodingo. Run `kodingo init` first.");
32
+ return false;
33
+ }
34
+ const repoName = path_1.default.basename(repoRoot);
35
+ if (!options.silent)
36
+ console.log(`\n🧠 Kortex — building structural graph for ${repoName}...\n`);
37
+ const previous = (0, graph_builder_1.loadGraph)(repoRoot);
38
+ const current = await (0, graph_builder_1.buildGraph)(repoRoot);
39
+ const diff = (0, graph_builder_1.diffGraphs)(previous, current);
40
+ if (diff.added.length === 0 && diff.removed.length === 0 && diff.edgesAdded.length === 0 && diff.edgesRemoved.length === 0) {
41
+ if (!options.silent)
42
+ console.log(" ✔ No structural changes detected. Graph is up to date.");
43
+ return true;
44
+ }
45
+ if (!options.silent) {
46
+ console.log(` ${diff.added.length} symbols, ${diff.edgesAdded.length} new relationships detected`);
47
+ }
48
+ // Chunk large syncs to respect the server's per-request caps (2000 nodes / 10000 edges)
49
+ const NODE_CHUNK = 1500;
50
+ const EDGE_CHUNK = 8000;
51
+ try {
52
+ for (let i = 0; i < diff.added.length || i === 0; i += NODE_CHUNK) {
53
+ const nodeChunk = diff.added.slice(i, i + NODE_CHUNK);
54
+ const edgeChunk = i === 0 ? diff.edgesAdded.slice(0, EDGE_CHUNK) : [];
55
+ const res = await fetch(`${apiUrl}/graph/sync`, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
58
+ body: JSON.stringify({
59
+ repo: repoName,
60
+ added: nodeChunk.map(n => ({
61
+ fingerprint: n.fingerprint,
62
+ name: n.name,
63
+ file: path_1.default.relative(repoRoot, n.file),
64
+ type: n.type,
65
+ language: n.language,
66
+ startLine: n.startLine,
67
+ endLine: n.endLine,
68
+ resolutionTier: n.resolutionTier,
69
+ fanIn: n.fanIn,
70
+ fanOut: n.fanOut,
71
+ centrality: n.centrality,
72
+ rawCalls: n.rawCalls,
73
+ })),
74
+ removed: i === 0 ? diff.removed : [],
75
+ edgesAdded: edgeChunk.map(e => ({
76
+ fromFingerprint: e.from,
77
+ toFingerprint: e.to,
78
+ edgeType: e.type,
79
+ confidence: e.confidence,
80
+ file: path_1.default.relative(repoRoot, e.file),
81
+ line: e.line,
82
+ })),
83
+ edgesRemoved: i === 0 ? diff.edgesRemoved.map(e => ({ fromFingerprint: e.from, toFingerprint: e.to })) : [],
84
+ }),
85
+ });
86
+ if (!res.ok) {
87
+ if (!options.silent)
88
+ console.error(` ✖ Sync failed: ${res.status} ${await res.text()}`);
89
+ return false;
90
+ }
91
+ if (diff.added.length === 0)
92
+ break; // ensure single-iteration exit when nothing to chunk
93
+ }
94
+ }
95
+ catch (err) {
96
+ if (!options.silent)
97
+ console.error(` ✖ Sync failed: ${err.message}`);
98
+ return false;
99
+ }
100
+ (0, graph_builder_1.saveGraph)(repoRoot, current);
101
+ if (!options.silent) {
102
+ console.log(`\n✔ Graph synced — ${Object.keys(current.nodes).length} symbols, ${current.edges.length} relationships tracked for ${repoName}`);
103
+ }
104
+ return true;
105
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const graph_builder_1 = require("./utils/graph-builder");
4
+ async function main() {
5
+ const repoRoot = process.argv[2];
6
+ const graph = await (0, graph_builder_1.buildGraph)(repoRoot);
7
+ const node = Object.values(graph.nodes).find(n => n.name === "projectRoutes");
8
+ console.log(node);
9
+ }
10
+ main();
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const ast_parser_1 = require("./utils/ast-parser");
8
+ async function main() {
9
+ const filePath = process.argv[2];
10
+ const source = fs_1.default.readFileSync(filePath, "utf-8");
11
+ const symbols = await (0, ast_parser_1.parseFile)(filePath, source);
12
+ for (const s of symbols) {
13
+ console.log(`${s.name}: calls = [${s.calls.join(", ")}]`);
14
+ }
15
+ }
16
+ main();