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 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);
@@ -78,3 +78,6 @@ async function readStdin() {
78
78
  }
79
79
  // review test
80
80
  // review test
81
+ function testKodingoSaveWatcher() {
82
+ return false;
83
+ }
@@ -2,9 +2,11 @@
2
2
  /**
3
3
  * kodingo init
4
4
  *
5
- * Interactively configures ~/.kodingo/config.json.
6
- * Supports both local (psql) and cloud (kodingo-api) modes.
7
- * On successful cloud init, ensures .kortex/ is in .gitignore.
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 path_1 = __importDefault(require("path"));
50
- const fs_1 = __importDefault(require("fs"));
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 (fs_1.default.existsSync(path_1.default.join(current, ".git")))
93
+ if (fs.existsSync(path.join(current, ".git")))
55
94
  return current;
56
- const parent = path_1.default.dirname(current);
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 = path_1.default.join(repoRoot, ".gitignore");
103
+ const gitignorePath = path.join(repoRoot, ".gitignore");
76
104
  const line = entry.endsWith("\n") ? entry : `${entry}\n`;
77
- if (fs_1.default.existsSync(gitignorePath)) {
78
- const contents = fs_1.default.readFileSync(gitignorePath, "utf-8");
79
- if (contents.split("\n").some((l) => l.trim() === entry.trim()))
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
- fs_1.default.appendFileSync(gitignorePath, `\n# Kortex — auto-generated context file\n${line}`, "utf-8");
109
+ fs.appendFileSync(gitignorePath, `\n# Kortex\n${line}`, "utf-8");
82
110
  }
83
111
  else {
84
- fs_1.default.writeFileSync(gitignorePath, `# Kortex — auto-generated context file\n${line}`, "utf-8");
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("Configure kodingo CLI (local psql or cloud API)")
92
- .option("--local", "Switch to local mode (psql)")
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
- const current = (0, persistence_config_1.readConfig)();
103
- // ── Non-interactive flags ─────────────────────────────────────────────
104
- if (opts.local) {
105
- (0, persistence_config_1.writeConfig)({ mode: "local" });
106
- console.log("✔ Switched to local mode — using local psql database.");
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
- if (opts.cloud && opts.apiUrl && opts.token) {
110
- console.log("Verifying connection to kodingo-api...");
111
- await verifyCloudConnection(opts.apiUrl, opts.token);
112
- (0, persistence_config_1.writeConfig)({
113
- mode: "cloud",
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
- // ── Interactive flow ──────────────────────────────────────────────────
123
- console.log("\nKodingo CLI Setup\n");
124
- console.log(`Current mode: ${current.mode}`);
125
- if (current.apiUrl)
126
- console.log(`Current API URL: ${current.apiUrl}`);
127
- console.log();
128
- const modeInput = await prompt(rl, "Choose mode (l)ocal or (c)loud [default: local]: ");
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
- // Cloud mode collect API URL and token
138
- const defaultUrl = current.apiUrl ?? "";
139
- const apiUrlInput = await prompt(rl, `API URL${defaultUrl ? ` [${defaultUrl}]` : ""}: `);
140
- const apiUrl = apiUrlInput || defaultUrl;
141
- if (!apiUrl) {
142
- console.error("✖ API URL is required for cloud mode.");
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
- const tokenInput = await prompt(rl, "API Token (X-Kodingo-Token): ");
146
- const token = tokenInput || current.token || "";
147
- if (!token) {
148
- console.error("✖ API token is required for cloud mode.");
149
- process.exit(1);
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
- console.log("\nVerifying connection to kodingo-api...");
152
- await verifyCloudConnection(apiUrl, token);
153
- const config = { mode: "cloud", apiUrl, token };
154
- (0, persistence_config_1.writeConfig)(config);
155
- // Save workspace-specific config scoped to this repo
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
- (0, persistence_config_1.writeWorkspaceConfig)(repoRoot, { mode: "cloud", apiUrl, token });
158
- // Ensure .kortex/ is gitignored it's a generated file, not source
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(`\n✔ Cloud mode configured successfully.`);
161
- console.log(` API URL : ${apiUrl}`);
162
- console.log(` Config : ${persistence_config_1.CONFIG_PATH}`);
163
- console.log("\nYou're ready to use kodingo with the cloud API.");
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✖ Init failed: ${err.message}`);
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.12",
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.0.0",
33
+ "@types/node": "^22.19.20",
33
34
  "@types/uuid": "^10.0.0",
34
35
  "typescript": "^5.6.0"
35
36
  }