kodingo-cli 1.0.11 → 1.0.13
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 +24 -0
- package/dist/commands/capture.js +3 -0
- package/dist/commands/init.js +17 -0
- package/dist/commands/scan-claude.js +179 -0
- package/dist/commands/scan-repo.js +209 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,8 @@ const affirm_1 = require("./commands/affirm");
|
|
|
12
12
|
const ignore_1 = require("./commands/ignore");
|
|
13
13
|
const deny_1 = require("./commands/deny");
|
|
14
14
|
const scan_git_1 = require("./commands/scan-git");
|
|
15
|
+
const scan_claude_1 = require("./commands/scan-claude");
|
|
16
|
+
const scan_repo_1 = require("./commands/scan-repo");
|
|
15
17
|
const init_1 = require("./commands/init");
|
|
16
18
|
const install_hook_1 = require("./commands/install-hook");
|
|
17
19
|
const update_1 = require("./commands/update");
|
|
@@ -202,4 +204,26 @@ program
|
|
|
202
204
|
payload.repo = String(options.repo);
|
|
203
205
|
await (0, install_hook_1.installHookCommand)(payload);
|
|
204
206
|
});
|
|
207
|
+
// ── scan-claude ──────────────────────────────────────────────────────────────
|
|
208
|
+
program
|
|
209
|
+
.command("scan-claude")
|
|
210
|
+
.description("Scan Claude Code session files and extract decisions into Kortex memory")
|
|
211
|
+
.option("--repo <path>", "repo path to scan")
|
|
212
|
+
.action(async (options) => {
|
|
213
|
+
const payload = {};
|
|
214
|
+
if (options.repo)
|
|
215
|
+
payload.repo = String(options.repo);
|
|
216
|
+
await (0, scan_claude_1.scanClaudeCommand)(payload);
|
|
217
|
+
});
|
|
218
|
+
// ── scan-repo ─────────────────────────────────────────────────────────────────
|
|
219
|
+
program
|
|
220
|
+
.command("scan-repo")
|
|
221
|
+
.description("Bulk scan existing codebase and capture memory for functions not yet in Kortex")
|
|
222
|
+
.option("--repo <path>", "repo path to scan")
|
|
223
|
+
.action(async (options) => {
|
|
224
|
+
const payload = {};
|
|
225
|
+
if (options.repo)
|
|
226
|
+
payload.repo = String(options.repo);
|
|
227
|
+
await (0, scan_repo_1.scanRepoCommand)(payload);
|
|
228
|
+
});
|
|
205
229
|
program.parse(process.argv);
|
package/dist/commands/capture.js
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -47,6 +47,7 @@ exports.registerInitCommand = registerInitCommand;
|
|
|
47
47
|
const readline = __importStar(require("node:readline"));
|
|
48
48
|
const persistence_config_1 = require("../utils/persistence-config");
|
|
49
49
|
const path_1 = __importDefault(require("path"));
|
|
50
|
+
const scan_repo_1 = require("./scan-repo");
|
|
50
51
|
const fs_1 = __importDefault(require("fs"));
|
|
51
52
|
function findRepoRoot(startPath) {
|
|
52
53
|
let current = startPath;
|
|
@@ -85,6 +86,18 @@ function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
// ── Command ───────────────────────────────────────────────────────────────────
|
|
89
|
+
async function runInitialScan(repoRoot) {
|
|
90
|
+
try {
|
|
91
|
+
console.log("\n🔍 Scanning existing codebase for functions without memory...");
|
|
92
|
+
const captured = await (0, scan_repo_1.scanRepoCommand)({ repo: repoRoot, silent: false });
|
|
93
|
+
if (captured === 0) {
|
|
94
|
+
console.log(" No existing functions found — Kortex will capture them as you write and commit code.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Never fail init because of a scan error
|
|
99
|
+
}
|
|
100
|
+
}
|
|
88
101
|
function registerInitCommand(program) {
|
|
89
102
|
program
|
|
90
103
|
.command("init")
|
|
@@ -117,6 +130,8 @@ function registerInitCommand(program) {
|
|
|
117
130
|
const repoRoot = findRepoRoot(process.cwd());
|
|
118
131
|
ensureGitignoreEntry(repoRoot, ".kortex/");
|
|
119
132
|
console.log(`✔ Cloud mode configured. Config saved to ${persistence_config_1.CONFIG_PATH}`);
|
|
133
|
+
// Auto-scan existing codebase silently
|
|
134
|
+
await runInitialScan(repoRoot);
|
|
120
135
|
return;
|
|
121
136
|
}
|
|
122
137
|
// ── Interactive flow ──────────────────────────────────────────────────
|
|
@@ -161,6 +176,8 @@ function registerInitCommand(program) {
|
|
|
161
176
|
console.log(` API URL : ${apiUrl}`);
|
|
162
177
|
console.log(` Config : ${persistence_config_1.CONFIG_PATH}`);
|
|
163
178
|
console.log("\nYou're ready to use kodingo with the cloud API.");
|
|
179
|
+
// Auto-scan existing codebase for functions without memory
|
|
180
|
+
await runInitialScan(repoRoot);
|
|
164
181
|
}
|
|
165
182
|
catch (err) {
|
|
166
183
|
console.error(`\n✖ Init failed: ${err.message}`);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* kodingo scan-claude
|
|
4
|
+
*
|
|
5
|
+
* Reads Claude Code session files from .claude/sessions/ in the repo root
|
|
6
|
+
* and extracts decisions, rationale, and rejected approaches that never
|
|
7
|
+
* made it into a commit — signal that would otherwise be lost forever.
|
|
8
|
+
*
|
|
9
|
+
* Claude Code writes JSONL session files at:
|
|
10
|
+
* <repo>/.claude/sessions/<session-id>.jsonl
|
|
11
|
+
*
|
|
12
|
+
* Each line is a message in the conversation. We scan for:
|
|
13
|
+
* - Assistant messages containing architectural decisions or tradeoffs
|
|
14
|
+
* - Tool results showing code that was written then abandoned
|
|
15
|
+
* - Explicit reasoning blocks
|
|
16
|
+
*/
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.scanClaudeCommand = scanClaudeCommand;
|
|
22
|
+
const fs_1 = __importDefault(require("fs"));
|
|
23
|
+
const path_1 = __importDefault(require("path"));
|
|
24
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
25
|
+
// ── Heuristics for extracting decisions ──────────────────────────────────────
|
|
26
|
+
const DECISION_PATTERNS = [
|
|
27
|
+
/\b(?:decided|decision|chose|choosing|going with|we['']ll use|using|opted for|approach is)\b/i,
|
|
28
|
+
/\b(?:tradeoff|trade-off|because|reason|rationale|the issue is|the problem is)\b/i,
|
|
29
|
+
/\b(?:instead of|rather than|alternative|we could|we should|don['']t use|avoid)\b/i,
|
|
30
|
+
/\b(?:architecture|pattern|design|structure|strategy|approach)\b/i,
|
|
31
|
+
];
|
|
32
|
+
const REJECTION_PATTERNS = [
|
|
33
|
+
/\b(?:rejected|abandoned|won['']t work|doesn['']t work|not going to|let['']s not|avoid this)\b/i,
|
|
34
|
+
/\b(?:tried|attempted|doesn['']t fit|too complex|too slow|bad idea)\b/i,
|
|
35
|
+
];
|
|
36
|
+
function scoreText(text) {
|
|
37
|
+
let score = 0;
|
|
38
|
+
for (const pat of DECISION_PATTERNS) {
|
|
39
|
+
if (pat.test(text))
|
|
40
|
+
score += 2;
|
|
41
|
+
}
|
|
42
|
+
for (const pat of REJECTION_PATTERNS) {
|
|
43
|
+
if (pat.test(text))
|
|
44
|
+
score += 1;
|
|
45
|
+
}
|
|
46
|
+
return score;
|
|
47
|
+
}
|
|
48
|
+
function extractTextFromContent(content) {
|
|
49
|
+
if (typeof content === "string")
|
|
50
|
+
return content;
|
|
51
|
+
return content
|
|
52
|
+
.filter(b => b.type === "text" && b.text)
|
|
53
|
+
.map(b => b.text)
|
|
54
|
+
.join("\n");
|
|
55
|
+
}
|
|
56
|
+
// Split assistant message into paragraph-sized chunks
|
|
57
|
+
function splitIntoParagraphs(text) {
|
|
58
|
+
return text
|
|
59
|
+
.split(/\n{2,}/)
|
|
60
|
+
.map(p => p.trim())
|
|
61
|
+
.filter(p => p.length > 80);
|
|
62
|
+
}
|
|
63
|
+
function buildTitle(text) {
|
|
64
|
+
const first = text.split(/[.\n]/)[0].trim();
|
|
65
|
+
return first.length > 100 ? first.slice(0, 97) + "..." : first;
|
|
66
|
+
}
|
|
67
|
+
// ── Session parser ────────────────────────────────────────────────────────────
|
|
68
|
+
function parseSessionFile(filePath) {
|
|
69
|
+
const messages = [];
|
|
70
|
+
const lines = fs_1.default.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(line);
|
|
74
|
+
// Claude Code JSONL format: { role, content, ... }
|
|
75
|
+
if (parsed.role && parsed.content !== undefined) {
|
|
76
|
+
messages.push(parsed);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// skip malformed lines
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return messages;
|
|
84
|
+
}
|
|
85
|
+
function extractMemoriesFromSession(messages, sessionId, repoPath) {
|
|
86
|
+
const memories = [];
|
|
87
|
+
for (const msg of messages) {
|
|
88
|
+
if (msg.role !== "assistant")
|
|
89
|
+
continue;
|
|
90
|
+
const text = extractTextFromContent(msg.content);
|
|
91
|
+
if (!text)
|
|
92
|
+
continue;
|
|
93
|
+
const paragraphs = splitIntoParagraphs(text);
|
|
94
|
+
for (const para of paragraphs) {
|
|
95
|
+
const score = scoreText(para);
|
|
96
|
+
if (score < 2)
|
|
97
|
+
continue;
|
|
98
|
+
const isRejection = REJECTION_PATTERNS.some(p => p.test(para));
|
|
99
|
+
memories.push({
|
|
100
|
+
type: isRejection ? "note" : "decision",
|
|
101
|
+
title: buildTitle(para),
|
|
102
|
+
content: para,
|
|
103
|
+
tags: ["claude-session", isRejection ? "rejected-approach" : "ai-reasoning"],
|
|
104
|
+
repoPath,
|
|
105
|
+
status: "proposed",
|
|
106
|
+
confidence: Math.min(0.3 + score * 0.05, 0.6),
|
|
107
|
+
externalId: `claude-session:${sessionId}:${Buffer.from(para.slice(0, 40)).toString("base64").slice(0, 16)}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return memories;
|
|
112
|
+
}
|
|
113
|
+
// ── Command ───────────────────────────────────────────────────────────────────
|
|
114
|
+
async function scanClaudeCommand(options = {}) {
|
|
115
|
+
const repoRoot = resolveRepoRoot(options);
|
|
116
|
+
const sessionsDir = path_1.default.join(repoRoot, ".claude", "sessions");
|
|
117
|
+
if (!fs_1.default.existsSync(sessionsDir)) {
|
|
118
|
+
console.log(`No Claude Code sessions found at ${sessionsDir}`);
|
|
119
|
+
console.log("Claude Code sessions are created when you use Claude Code in this repo.");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const sessionFiles = fs_1.default.readdirSync(sessionsDir)
|
|
123
|
+
.filter(f => f.endsWith(".jsonl"))
|
|
124
|
+
.map(f => path_1.default.join(sessionsDir, f));
|
|
125
|
+
if (!sessionFiles.length) {
|
|
126
|
+
console.log("No session files found.");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const { initDb, saveMemory, getMemoryByExternalId } = (0, persistence_config_1.getPersistence)();
|
|
130
|
+
await initDb();
|
|
131
|
+
let totalSaved = 0;
|
|
132
|
+
let totalSkipped = 0;
|
|
133
|
+
for (const filePath of sessionFiles) {
|
|
134
|
+
const sessionId = path_1.default.basename(filePath, ".jsonl");
|
|
135
|
+
let messages;
|
|
136
|
+
try {
|
|
137
|
+
messages = parseSessionFile(filePath);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
console.warn(` Skipping unreadable session: ${sessionId}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const candidates = extractMemoriesFromSession(messages, sessionId, repoRoot);
|
|
144
|
+
for (const candidate of candidates) {
|
|
145
|
+
// Deduplicate by externalId
|
|
146
|
+
if (candidate.externalId) {
|
|
147
|
+
const existing = await getMemoryByExternalId(candidate.externalId, repoRoot);
|
|
148
|
+
if (existing) {
|
|
149
|
+
totalSkipped++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
await saveMemory(candidate);
|
|
154
|
+
totalSaved++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
console.log(`Claude session scan complete.`);
|
|
158
|
+
console.log(` Sessions scanned : ${sessionFiles.length}`);
|
|
159
|
+
console.log(` Memories saved : ${totalSaved}`);
|
|
160
|
+
console.log(` Already stored : ${totalSkipped}`);
|
|
161
|
+
}
|
|
162
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
163
|
+
function resolveRepoRoot(options) {
|
|
164
|
+
if (options.repo?.trim()) {
|
|
165
|
+
return findRepoRoot(path_1.default.resolve(options.repo.trim()));
|
|
166
|
+
}
|
|
167
|
+
return findRepoRoot(process.cwd());
|
|
168
|
+
}
|
|
169
|
+
function findRepoRoot(startPath) {
|
|
170
|
+
let current = startPath;
|
|
171
|
+
while (true) {
|
|
172
|
+
if (fs_1.default.existsSync(path_1.default.join(current, ".git")))
|
|
173
|
+
return current;
|
|
174
|
+
const parent = path_1.default.dirname(current);
|
|
175
|
+
if (parent === current)
|
|
176
|
+
return startPath;
|
|
177
|
+
current = parent;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* kodingo scan-repo
|
|
4
|
+
*
|
|
5
|
+
* Bulk scans an entire repository for functions that have no Kortex memory yet.
|
|
6
|
+
* Designed to be run once on an existing codebase after `kodingo init`.
|
|
7
|
+
* Also triggered automatically at the end of `kodingo init` if existing code is found.
|
|
8
|
+
*
|
|
9
|
+
* Walks all supported files, extracts symbols, checks which ones have no memory,
|
|
10
|
+
* and submits them to the inference endpoint in batches.
|
|
11
|
+
*
|
|
12
|
+
* Caps at 50 symbols per run to avoid overwhelming the API.
|
|
13
|
+
* Safe to run multiple times — skips symbols that already have memory.
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.scanRepoCommand = scanRepoCommand;
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
23
|
+
const API_BASE = "https://kodingo-api.onrender.com";
|
|
24
|
+
const SUPPORTED_EXTENSIONS = [
|
|
25
|
+
".ts", ".tsx", ".js", ".jsx",
|
|
26
|
+
".py", ".go", ".rs", ".php",
|
|
27
|
+
".java", ".kt", ".rb", ".c", ".cpp",
|
|
28
|
+
];
|
|
29
|
+
const SYMBOL_PATTERNS = [
|
|
30
|
+
/^(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
31
|
+
/^(?:export\s+)?(?:default\s+)?class\s+(\w+)/,
|
|
32
|
+
/^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\(/,
|
|
33
|
+
/^def\s+(\w+)\s*\(/,
|
|
34
|
+
/^func\s+(\w+)\s*\(/,
|
|
35
|
+
/^(?:pub\s+)?fn\s+(\w+)\s*\(/,
|
|
36
|
+
];
|
|
37
|
+
const IGNORED_DIRS = [
|
|
38
|
+
"node_modules", ".git", ".kortex", "dist", "build",
|
|
39
|
+
"out", ".next", "coverage", "__pycache__", "vendor",
|
|
40
|
+
];
|
|
41
|
+
const MAX_SYMBOLS = 50;
|
|
42
|
+
const BATCH_SIZE = 5;
|
|
43
|
+
const BATCH_DELAY_MS = 1200;
|
|
44
|
+
function findRepoRoot(startPath) {
|
|
45
|
+
let current = startPath;
|
|
46
|
+
while (true) {
|
|
47
|
+
if (fs_1.default.existsSync(path_1.default.join(current, ".git")))
|
|
48
|
+
return current;
|
|
49
|
+
const parent = path_1.default.dirname(current);
|
|
50
|
+
if (parent === current)
|
|
51
|
+
return startPath;
|
|
52
|
+
current = parent;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function walkFiles(dir) {
|
|
56
|
+
const results = [];
|
|
57
|
+
try {
|
|
58
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (IGNORED_DIRS.includes(entry.name))
|
|
61
|
+
continue;
|
|
62
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
results.push(...walkFiles(fullPath));
|
|
65
|
+
}
|
|
66
|
+
else if (entry.isFile() && SUPPORTED_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
|
|
67
|
+
results.push(fullPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
function extractSymbols(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
const text = fs_1.default.readFileSync(filePath, "utf-8");
|
|
77
|
+
const lines = text.split("\n");
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
const trimmed = lines[i].trim();
|
|
81
|
+
for (const pat of SYMBOL_PATTERNS) {
|
|
82
|
+
const m = trimmed.match(pat);
|
|
83
|
+
if (m?.[1]) {
|
|
84
|
+
entries.push({
|
|
85
|
+
name: m[1],
|
|
86
|
+
code: lines.slice(i, i + 40).join("\n"),
|
|
87
|
+
file: filePath,
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return entries;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function hasExistingMemory(token, symbol, repo) {
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`${API_BASE}/memory?symbol=${encodeURIComponent(symbol)}&repo=${encodeURIComponent(repo)}&limit=1`, { headers: { "X-Kodingo-Token": token } });
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
return false;
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
return (data.total ?? 0) > 0;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function inferAndCapture(token, symbol, repo) {
|
|
112
|
+
try {
|
|
113
|
+
const inferRes = await fetch(`${API_BASE}/infer`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
116
|
+
body: JSON.stringify({ symbol: symbol.name, code: symbol.code }),
|
|
117
|
+
});
|
|
118
|
+
if (!inferRes.ok)
|
|
119
|
+
return false;
|
|
120
|
+
const inferred = await inferRes.json();
|
|
121
|
+
const saveRes = await fetch(`${API_BASE}/memory`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
type: inferred.type ?? "context",
|
|
126
|
+
title: inferred.title ?? `${symbol.name} — scanned`,
|
|
127
|
+
content: inferred.content ?? `Function \`${symbol.name}\` detected during repo scan.`,
|
|
128
|
+
symbol: symbol.name,
|
|
129
|
+
repo,
|
|
130
|
+
tags: [...(inferred.tags ?? []), "scan-repo"],
|
|
131
|
+
status: "proposed",
|
|
132
|
+
confidence: 0.35,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
return saveRes.ok;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function scanRepoCommand(options = {}) {
|
|
142
|
+
const config = (0, persistence_config_1.readConfig)();
|
|
143
|
+
if (config.mode !== "cloud" || !config.token) {
|
|
144
|
+
if (!options.silent)
|
|
145
|
+
console.error("✖ scan-repo requires cloud mode. Run `kodingo init` first.");
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
const repoRoot = options.repo
|
|
149
|
+
? path_1.default.resolve(options.repo)
|
|
150
|
+
: findRepoRoot(process.cwd());
|
|
151
|
+
const repoName = path_1.default.basename(repoRoot);
|
|
152
|
+
if (!options.silent) {
|
|
153
|
+
console.log(`\n🔍 Kortex — scanning ${repoName} for existing functions...\n`);
|
|
154
|
+
}
|
|
155
|
+
// Walk all files and extract symbols
|
|
156
|
+
const files = walkFiles(repoRoot);
|
|
157
|
+
const allSymbols = [];
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
allSymbols.push(...extractSymbols(file));
|
|
160
|
+
}
|
|
161
|
+
if (allSymbols.length === 0) {
|
|
162
|
+
if (!options.silent)
|
|
163
|
+
console.log(" No functions found — repo appears empty. Nothing to scan.");
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (!options.silent) {
|
|
167
|
+
console.log(` Found ${allSymbols.length} functions across ${files.length} files`);
|
|
168
|
+
console.log(` Checking which ones need memory...\n`);
|
|
169
|
+
}
|
|
170
|
+
// Filter to symbols without existing memory, cap at MAX_SYMBOLS
|
|
171
|
+
const toCapture = [];
|
|
172
|
+
for (const sym of allSymbols) {
|
|
173
|
+
if (toCapture.length >= MAX_SYMBOLS)
|
|
174
|
+
break;
|
|
175
|
+
const exists = await hasExistingMemory(config.token, sym.name, repoName);
|
|
176
|
+
if (!exists)
|
|
177
|
+
toCapture.push(sym);
|
|
178
|
+
}
|
|
179
|
+
if (toCapture.length === 0) {
|
|
180
|
+
if (!options.silent)
|
|
181
|
+
console.log(" ✔ All functions already have Kortex memory. Nothing to capture.");
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
if (!options.silent) {
|
|
185
|
+
console.log(` Capturing memory for ${toCapture.length} functions...\n`);
|
|
186
|
+
}
|
|
187
|
+
// Process in batches
|
|
188
|
+
let captured = 0;
|
|
189
|
+
for (let i = 0; i < toCapture.length; i += BATCH_SIZE) {
|
|
190
|
+
const batch = toCapture.slice(i, i + BATCH_SIZE);
|
|
191
|
+
const results = await Promise.all(batch.map(sym => inferAndCapture(config.token, sym, repoName)));
|
|
192
|
+
const batchCaptured = results.filter(Boolean).length;
|
|
193
|
+
captured += batchCaptured;
|
|
194
|
+
if (!options.silent) {
|
|
195
|
+
const progress = Math.min(i + BATCH_SIZE, toCapture.length);
|
|
196
|
+
process.stdout.write(`\r Progress: ${progress}/${toCapture.length} functions processed`);
|
|
197
|
+
}
|
|
198
|
+
if (i + BATCH_SIZE < toCapture.length) {
|
|
199
|
+
await new Promise(r => setTimeout(r, BATCH_DELAY_MS));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!options.silent) {
|
|
203
|
+
console.log(`\n\n✔ Scan complete — ${captured} memories captured for ${repoName}`);
|
|
204
|
+
if (allSymbols.length > MAX_SYMBOLS) {
|
|
205
|
+
console.log(` Note: ${allSymbols.length - MAX_SYMBOLS} additional functions will be captured automatically as you open files.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return captured;
|
|
209
|
+
}
|