kodingo-cli 1.0.12 → 1.0.14
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 +12 -0
- package/dist/commands/capture.js +3 -0
- package/dist/commands/init.js +196 -87
- package/dist/commands/scan-repo.js +209 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ 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
15
|
const scan_claude_1 = require("./commands/scan-claude");
|
|
16
|
+
const scan_repo_1 = require("./commands/scan-repo");
|
|
16
17
|
const init_1 = require("./commands/init");
|
|
17
18
|
const install_hook_1 = require("./commands/install-hook");
|
|
18
19
|
const update_1 = require("./commands/update");
|
|
@@ -214,4 +215,15 @@ program
|
|
|
214
215
|
payload.repo = String(options.repo);
|
|
215
216
|
await (0, scan_claude_1.scanClaudeCommand)(payload);
|
|
216
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
|
+
});
|
|
217
229
|
program.parse(process.argv);
|
package/dist/commands/capture.js
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* kodingo init
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* GitHub-style browser-based authentication and project connection.
|
|
6
|
+
* - First time on a machine: opens browser to login, saves auth token globally
|
|
7
|
+
* - Subsequent runs: skips login, goes straight to project selection
|
|
8
|
+
* - Project selection: type project name, case-insensitive match
|
|
9
|
+
* - Create new project if name not found
|
|
8
10
|
*/
|
|
9
11
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
12
|
if (k2 === undefined) k2 = k;
|
|
@@ -45,125 +47,232 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
48
|
exports.registerInitCommand = registerInitCommand;
|
|
47
49
|
const readline = __importStar(require("node:readline"));
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const open_1 = __importDefault(require("open"));
|
|
48
54
|
const persistence_config_1 = require("../utils/persistence-config");
|
|
49
|
-
const
|
|
50
|
-
const
|
|
55
|
+
const API_BASE = "https://kodingo-api.onrender.com";
|
|
56
|
+
const APP_URL = "https://kodingo.xyz";
|
|
57
|
+
const CONFIG_DIR = path.join(os.homedir(), ".kodingo");
|
|
58
|
+
const AUTH_TOKEN_PATH = path.join(CONFIG_DIR, "auth.json");
|
|
59
|
+
// ── Auth token storage ────────────────────────────────────────────────────────
|
|
60
|
+
function readAuthToken() {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(AUTH_TOKEN_PATH)) {
|
|
63
|
+
const data = JSON.parse(fs.readFileSync(AUTH_TOKEN_PATH, "utf-8"));
|
|
64
|
+
return data.token ?? null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function saveAuthToken(token, email) {
|
|
71
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
72
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
73
|
+
fs.writeFileSync(AUTH_TOKEN_PATH, JSON.stringify({ token, email }, null, 2), "utf-8");
|
|
74
|
+
}
|
|
75
|
+
function readAuthEmail() {
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(AUTH_TOKEN_PATH)) {
|
|
78
|
+
const data = JSON.parse(fs.readFileSync(AUTH_TOKEN_PATH, "utf-8"));
|
|
79
|
+
return data.email ?? '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
// ── Prompt helper ─────────────────────────────────────────────────────────────
|
|
86
|
+
function prompt(rl, question) {
|
|
87
|
+
return new Promise(resolve => rl.question(question, ans => resolve(ans.trim())));
|
|
88
|
+
}
|
|
89
|
+
// ── Repo root finder ──────────────────────────────────────────────────────────
|
|
51
90
|
function findRepoRoot(startPath) {
|
|
52
91
|
let current = startPath;
|
|
53
92
|
while (true) {
|
|
54
|
-
if (
|
|
93
|
+
if (fs.existsSync(path.join(current, ".git")))
|
|
55
94
|
return current;
|
|
56
|
-
const parent =
|
|
95
|
+
const parent = path.dirname(current);
|
|
57
96
|
if (parent === current)
|
|
58
97
|
return startPath;
|
|
59
98
|
current = parent;
|
|
60
99
|
}
|
|
61
100
|
}
|
|
62
|
-
// ── Prompt helper ─────────────────────────────────────────────────────────────
|
|
63
|
-
function prompt(rl, question) {
|
|
64
|
-
return new Promise((resolve) => rl.question(question, (ans) => resolve(ans.trim())));
|
|
65
|
-
}
|
|
66
|
-
// ── Health check ──────────────────────────────────────────────────────────────
|
|
67
|
-
async function verifyCloudConnection(apiUrl, token) {
|
|
68
|
-
const url = `${apiUrl.replace(/\/$/, "")}/health`;
|
|
69
|
-
const res = await fetch(url, { headers: { "X-Kodingo-Token": token } });
|
|
70
|
-
if (!res.ok)
|
|
71
|
-
throw new Error(`Health check failed (${res.status}) — check your API URL`);
|
|
72
|
-
}
|
|
73
101
|
// ── .gitignore helper ─────────────────────────────────────────────────────────
|
|
74
102
|
function ensureGitignoreEntry(repoRoot, entry) {
|
|
75
|
-
const gitignorePath =
|
|
103
|
+
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
76
104
|
const line = entry.endsWith("\n") ? entry : `${entry}\n`;
|
|
77
|
-
if (
|
|
78
|
-
const contents =
|
|
79
|
-
if (contents.split("\n").some(
|
|
105
|
+
if (fs.existsSync(gitignorePath)) {
|
|
106
|
+
const contents = fs.readFileSync(gitignorePath, "utf-8");
|
|
107
|
+
if (contents.split("\n").some(l => l.trim() === entry.trim()))
|
|
80
108
|
return;
|
|
81
|
-
|
|
109
|
+
fs.appendFileSync(gitignorePath, `\n# Kortex\n${line}`, "utf-8");
|
|
82
110
|
}
|
|
83
111
|
else {
|
|
84
|
-
|
|
112
|
+
fs.writeFileSync(gitignorePath, `# Kortex\n${line}`, "utf-8");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ── Browser login flow ────────────────────────────────────────────────────────
|
|
116
|
+
async function browserLogin() {
|
|
117
|
+
// Request a state token from the API
|
|
118
|
+
const initRes = await fetch(`${API_BASE}/cli/auth/init`, { method: "POST" });
|
|
119
|
+
if (!initRes.ok)
|
|
120
|
+
throw new Error("Failed to start login flow");
|
|
121
|
+
const { state, loginUrl } = await initRes.json();
|
|
122
|
+
console.log("\nPress Enter to open kodingo.xyz in your browser...");
|
|
123
|
+
console.log(`Or open this URL manually: ${loginUrl}\n`);
|
|
124
|
+
await new Promise(resolve => {
|
|
125
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
126
|
+
rl.question("", () => { rl.close(); resolve(); });
|
|
127
|
+
});
|
|
128
|
+
// Open browser
|
|
129
|
+
try {
|
|
130
|
+
await (0, open_1.default)(loginUrl);
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
console.log("Waiting for login...");
|
|
134
|
+
// Poll for completion
|
|
135
|
+
const start = Date.now();
|
|
136
|
+
while (Date.now() - start < 5 * 60 * 1000) {
|
|
137
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
138
|
+
const pollRes = await fetch(`${API_BASE}/cli/auth/poll/${state}`);
|
|
139
|
+
if (!pollRes.ok)
|
|
140
|
+
continue;
|
|
141
|
+
const data = await pollRes.json();
|
|
142
|
+
if (data.status === "complete" && data.token) {
|
|
143
|
+
return { token: data.token, email: data.email ?? "" };
|
|
144
|
+
}
|
|
85
145
|
}
|
|
146
|
+
throw new Error("Login timed out. Please try again.");
|
|
147
|
+
}
|
|
148
|
+
// ── Fetch accessible projects ─────────────────────────────────────────────────
|
|
149
|
+
async function fetchProjects(authToken) {
|
|
150
|
+
const res = await fetch(`${API_BASE}/cli/projects`, {
|
|
151
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
152
|
+
});
|
|
153
|
+
if (!res.ok)
|
|
154
|
+
throw new Error("Failed to fetch projects. Please run `kodingo init` again.");
|
|
155
|
+
const data = await res.json();
|
|
156
|
+
return data.projects ?? [];
|
|
157
|
+
}
|
|
158
|
+
// ── Create a new project ──────────────────────────────────────────────────────
|
|
159
|
+
async function createProject(authToken, name, orgId) {
|
|
160
|
+
const res = await fetch(`${API_BASE}/cli/projects`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` },
|
|
163
|
+
body: JSON.stringify({ name, orgId }),
|
|
164
|
+
});
|
|
165
|
+
const data = await res.json();
|
|
166
|
+
if (!res.ok)
|
|
167
|
+
throw new Error(data.error ?? "Failed to create project");
|
|
168
|
+
return data;
|
|
86
169
|
}
|
|
87
170
|
// ── Command ───────────────────────────────────────────────────────────────────
|
|
88
171
|
function registerInitCommand(program) {
|
|
89
172
|
program
|
|
90
173
|
.command("init")
|
|
91
|
-
.description("
|
|
92
|
-
.option("--
|
|
93
|
-
.option("--cloud", "Switch to cloud mode (kodingo-api)")
|
|
94
|
-
.option("--api-url <url>", "Cloud API URL (skips prompt)")
|
|
95
|
-
.option("--token <token>", "Cloud API token (skips prompt)")
|
|
174
|
+
.description("Connect this repository to a Kodingo project")
|
|
175
|
+
.option("--logout", "Log out and clear saved credentials")
|
|
96
176
|
.action(async (opts) => {
|
|
97
|
-
const rl = readline.createInterface({
|
|
98
|
-
input: process.stdin,
|
|
99
|
-
output: process.stdout,
|
|
100
|
-
});
|
|
177
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
101
178
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
console.log("✔
|
|
179
|
+
// ── Logout ──────────────────────────────────────────────────────────
|
|
180
|
+
if (opts.logout) {
|
|
181
|
+
if (fs.existsSync(AUTH_TOKEN_PATH))
|
|
182
|
+
fs.unlinkSync(AUTH_TOKEN_PATH);
|
|
183
|
+
console.log("✔ Logged out of Kodingo.");
|
|
107
184
|
return;
|
|
108
185
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
apiUrl: opts.apiUrl,
|
|
115
|
-
token: opts.token,
|
|
116
|
-
});
|
|
117
|
-
const repoRoot = findRepoRoot(process.cwd());
|
|
118
|
-
ensureGitignoreEntry(repoRoot, ".kortex/");
|
|
119
|
-
console.log(`✔ Cloud mode configured. Config saved to ${persistence_config_1.CONFIG_PATH}`);
|
|
120
|
-
return;
|
|
186
|
+
// ── Check if already logged in ───────────────────────────────────────
|
|
187
|
+
let authToken = readAuthToken();
|
|
188
|
+
let email = readAuthEmail();
|
|
189
|
+
if (authToken) {
|
|
190
|
+
console.log(`\n✔ Already logged in as ${email || "your Kodingo account"}\n`);
|
|
121
191
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const mode = modeInput.toLowerCase().startsWith("c")
|
|
130
|
-
? "cloud"
|
|
131
|
-
: "local";
|
|
132
|
-
if (mode === "local") {
|
|
133
|
-
(0, persistence_config_1.writeConfig)({ mode: "local" });
|
|
134
|
-
console.log("\n✔ Local mode configured. Using local psql database.");
|
|
135
|
-
return;
|
|
192
|
+
else {
|
|
193
|
+
console.log("\nKodingo — Connect this repository\n");
|
|
194
|
+
const result = await browserLogin();
|
|
195
|
+
authToken = result.token;
|
|
196
|
+
email = result.email;
|
|
197
|
+
saveAuthToken(authToken, email);
|
|
198
|
+
console.log(`\n✔ Logged in as ${email}\n`);
|
|
136
199
|
}
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
142
|
-
console.error("✖
|
|
200
|
+
// ── Fetch projects ───────────────────────────────────────────────────
|
|
201
|
+
const projects = await fetchProjects(authToken);
|
|
202
|
+
// ── Ask for project name ─────────────────────────────────────────────
|
|
203
|
+
const projectName = await prompt(rl, "Enter the project name to connect to this repo: ");
|
|
204
|
+
if (!projectName) {
|
|
205
|
+
console.error("✖ Project name is required.");
|
|
143
206
|
process.exit(1);
|
|
144
207
|
}
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
208
|
+
// ── Case-insensitive search ──────────────────────────────────────────
|
|
209
|
+
const normalised = projectName.toLowerCase().trim();
|
|
210
|
+
const matches = projects.filter(p => p.name.toLowerCase().trim() === normalised);
|
|
211
|
+
let selectedProject = null;
|
|
212
|
+
if (matches.length === 1) {
|
|
213
|
+
selectedProject = matches[0];
|
|
214
|
+
console.log(`\n✔ Found project: ${selectedProject.name} (${matches[0].org_name} · ${matches[0].plan} plan)\n`);
|
|
215
|
+
}
|
|
216
|
+
else if (matches.length > 1) {
|
|
217
|
+
// Multiple orgs with same project name — ask which one
|
|
218
|
+
console.log("\nMultiple projects found with that name:");
|
|
219
|
+
matches.forEach((p, i) => console.log(` ${i + 1}. ${p.name} — ${p.org_name}`));
|
|
220
|
+
const choice = await prompt(rl, "Enter number: ");
|
|
221
|
+
const idx = parseInt(choice) - 1;
|
|
222
|
+
if (idx < 0 || idx >= matches.length) {
|
|
223
|
+
console.error("✖ Invalid choice.");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
selectedProject = matches[idx];
|
|
150
227
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
228
|
+
else {
|
|
229
|
+
// No match — offer to create
|
|
230
|
+
console.log(`\n No project named "${projectName}" found in your account.`);
|
|
231
|
+
const create = await prompt(rl, "Would you like to create it? (y/n): ");
|
|
232
|
+
if (!create.toLowerCase().startsWith("y")) {
|
|
233
|
+
console.log("✖ Cancelled.");
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
// If user has multiple orgs, ask which one
|
|
237
|
+
const orgs = [...new Map(projects.map(p => [p.org_id, { id: p.org_id, name: p.org_name }])).values()];
|
|
238
|
+
let orgId = orgs[0]?.id;
|
|
239
|
+
if (orgs.length > 1) {
|
|
240
|
+
console.log("\nSelect an organisation:");
|
|
241
|
+
orgs.forEach((o, i) => console.log(` ${i + 1}. ${o.name}`));
|
|
242
|
+
const orgChoice = await prompt(rl, "Enter number: ");
|
|
243
|
+
const orgIdx = parseInt(orgChoice) - 1;
|
|
244
|
+
if (orgIdx < 0 || orgIdx >= orgs.length) {
|
|
245
|
+
console.error("✖ Invalid choice.");
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
orgId = orgs[orgIdx].id;
|
|
249
|
+
}
|
|
250
|
+
const created = await createProject(authToken, projectName, orgId);
|
|
251
|
+
selectedProject = created;
|
|
252
|
+
console.log(`\n✔ Project "${created.name}" created\n`);
|
|
253
|
+
}
|
|
254
|
+
// ── Save config ──────────────────────────────────────────────────────
|
|
156
255
|
const repoRoot = findRepoRoot(process.cwd());
|
|
157
|
-
|
|
158
|
-
|
|
256
|
+
const apiUrl = API_BASE;
|
|
257
|
+
(0, persistence_config_1.writeConfig)({ mode: "cloud", apiUrl, token: selectedProject.token });
|
|
258
|
+
(0, persistence_config_1.writeWorkspaceConfig)(repoRoot, { mode: "cloud", apiUrl, token: selectedProject.token });
|
|
159
259
|
ensureGitignoreEntry(repoRoot, ".kortex/");
|
|
160
|
-
console.log(
|
|
161
|
-
console.log(
|
|
162
|
-
console.log(`
|
|
163
|
-
|
|
260
|
+
console.log(`✔ Connected. Token saved to ${persistence_config_1.CONFIG_PATH}`);
|
|
261
|
+
console.log("\nKortex is now watching this repo.");
|
|
262
|
+
console.log("Run `kodingo install-hook` to capture memories on every commit.\n");
|
|
263
|
+
// Auto-scan existing codebase
|
|
264
|
+
try {
|
|
265
|
+
const { scanRepoCommand } = await Promise.resolve().then(() => __importStar(require("./scan-repo")));
|
|
266
|
+
console.log("🔍 Scanning existing codebase for functions without memory...");
|
|
267
|
+
const captured = await scanRepoCommand({ repo: repoRoot, silent: false });
|
|
268
|
+
if (captured === 0) {
|
|
269
|
+
console.log(" No existing functions found — Kortex will capture them as you write and commit code.");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch { }
|
|
164
273
|
}
|
|
165
274
|
catch (err) {
|
|
166
|
-
console.error(`\n✖
|
|
275
|
+
console.error(`\n✖ ${err.message}`);
|
|
167
276
|
process.exit(1);
|
|
168
277
|
}
|
|
169
278
|
finally {
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodingo-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Kodingo CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -24,12 +24,13 @@
|
|
|
24
24
|
"@babel/traverse": "^7.29.0",
|
|
25
25
|
"commander": "^12.1.0",
|
|
26
26
|
"kodingo-core": "^0.1.0",
|
|
27
|
+
"open": "^11.0.0",
|
|
27
28
|
"simple-git": "^3.30.0",
|
|
28
29
|
"uuid": "^10.0.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/babel__traverse": "^7.28.0",
|
|
32
|
-
"@types/node": "^22.
|
|
33
|
+
"@types/node": "^22.19.20",
|
|
33
34
|
"@types/uuid": "^10.0.0",
|
|
34
35
|
"typescript": "^5.6.0"
|
|
35
36
|
}
|