kodingo-cli 1.0.27 → 1.0.29

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.
@@ -7,13 +7,81 @@ exports.scanGitCommand = scanGitCommand;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
+ const ast_parser_1 = require("../utils/ast-parser");
11
+ const persistence_config_1 = require("../utils/persistence-config");
10
12
  const simple_git_1 = __importDefault(require("simple-git"));
11
13
  const git_listener_1 = require("../adapters/git-adapter/git-listener");
12
14
  const db_event_port_1 = require("../ports/db-event-port");
13
15
  const cloud_event_port_1 = require("../ports/cloud-event-port");
14
- const persistence_config_1 = require("../utils/persistence-config");
16
+ const persistence_config_2 = require("../utils/persistence-config");
17
+ const API_BASE_DEFAULT = "https://kodingo-api.onrender.com";
18
+ function getCredentials(repoRoot) {
19
+ const config = (0, persistence_config_1.readConfig)();
20
+ const normRoot = repoRoot.replace(/\/+$/, "");
21
+ let wsEntry = config.workspaces?.[normRoot] ?? config.workspaces?.[normRoot + "/"];
22
+ // Walk parent directories
23
+ let current = normRoot;
24
+ while (!wsEntry && current !== path_1.default.dirname(current)) {
25
+ current = path_1.default.dirname(current);
26
+ wsEntry = config.workspaces?.[current] ?? config.workspaces?.[current + "/"];
27
+ }
28
+ const token = wsEntry?.token ?? config.token ?? "";
29
+ const apiUrl = wsEntry?.apiUrl ?? config.apiUrl ?? API_BASE_DEFAULT;
30
+ return { token, apiUrl };
31
+ }
32
+ async function detectContextDecay(repoRoot, changedFiles) {
33
+ const { token, apiUrl } = getCredentials(repoRoot);
34
+ if (!token)
35
+ return;
36
+ const repoName = path_1.default.basename(repoRoot);
37
+ for (const filePath of changedFiles) {
38
+ if (!fs_1.default.existsSync(filePath))
39
+ continue;
40
+ try {
41
+ const source = fs_1.default.readFileSync(filePath, "utf-8");
42
+ const nodes = await (0, ast_parser_1.parseFile)(filePath, source);
43
+ for (const node of nodes) {
44
+ if (!node.fingerprint)
45
+ continue;
46
+ // Check if this fingerprint exists under a DIFFERENT symbol/repo
47
+ const res = await fetch(`${apiUrl}/memory?fingerprint=${encodeURIComponent(node.fingerprint)}&limit=5`, { headers: { "X-Kodingo-Token": token } });
48
+ if (!res.ok)
49
+ continue;
50
+ const data = await res.json();
51
+ const memories = data.data ?? [];
52
+ for (const m of memories) {
53
+ const sameSymbol = m.symbol === node.name;
54
+ const sameRepo = m.repo === repoName;
55
+ if (!sameSymbol || !sameRepo) {
56
+ // Fingerprint match but different identity — this is a rename/move
57
+ const migrateRes = await fetch(`${apiUrl}/memory/migrate`, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
60
+ body: JSON.stringify({
61
+ fingerprint: node.fingerprint,
62
+ oldSymbol: m.symbol,
63
+ newSymbol: node.name,
64
+ oldRepo: m.repo ?? repoName,
65
+ newRepo: repoName,
66
+ }),
67
+ });
68
+ if (migrateRes.ok) {
69
+ const result = await migrateRes.json();
70
+ if (result.migrated > 0) {
71
+ console.log(` ↳ Kortex: ${result.migrated} memor${result.migrated === 1 ? "y" : "ies"} followed rename: ${m.symbol} → ${node.name}`);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ catch {
79
+ // silent — never block the commit flow
80
+ }
81
+ }
82
+ }
15
83
  async function scanGitCommand(options) {
16
- const { initDb, getMemoryByExternalId } = (0, persistence_config_1.getPersistence)();
84
+ const { initDb, getMemoryByExternalId } = (0, persistence_config_2.getPersistence)();
17
85
  const repoRoot = resolveRepoRoot(options);
18
86
  await initDb();
19
87
  const git = (0, simple_git_1.default)(repoRoot);
@@ -30,7 +98,7 @@ async function scanGitCommand(options) {
30
98
  console.log(`Git scan skipped (already stored) for repo: ${repoRoot} commit: ${commitHash.slice(0, 8)}`);
31
99
  return;
32
100
  }
33
- const eventPort = (0, persistence_config_1.isCloudMode)()
101
+ const eventPort = (0, persistence_config_2.isCloudMode)()
34
102
  ? new cloud_event_port_1.CloudEventPort(repoRoot)
35
103
  : new db_event_port_1.DbEventPort(repoRoot);
36
104
  const adapter = new git_listener_1.GitListenerAdapter(repoRoot, eventPort, 5000);
@@ -54,20 +54,13 @@ const fs_1 = __importDefault(require("fs"));
54
54
  const path_1 = __importDefault(require("path"));
55
55
  const persistence_config_1 = require("../utils/persistence-config");
56
56
  const os = __importStar(require("os"));
57
+ const ast_parser_1 = require("../utils/ast-parser");
57
58
  const API_BASE = "https://kodingo-api.onrender.com";
58
59
  const SUPPORTED_EXTENSIONS = [
59
60
  ".ts", ".tsx", ".js", ".jsx",
60
61
  ".py", ".go", ".rs", ".php",
61
62
  ".java", ".kt", ".rb", ".c", ".cpp",
62
63
  ];
63
- const SYMBOL_PATTERNS = [
64
- /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
65
- /^(?:export\s+)?(?:default\s+)?class\s+(\w+)/,
66
- /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\(/,
67
- /^def\s+(\w+)\s*\(/,
68
- /^func\s+(\w+)\s*\(/,
69
- /^(?:pub\s+)?fn\s+(\w+)\s*\(/,
70
- ];
71
64
  const IGNORED_DIRS = [
72
65
  "node_modules", ".git", ".kortex", "dist", "build",
73
66
  "out", ".next", "coverage", "__pycache__", "vendor",
@@ -105,26 +98,16 @@ function walkFiles(dir) {
105
98
  catch { }
106
99
  return results;
107
100
  }
108
- function extractSymbols(filePath) {
101
+ async function extractSymbols(filePath) {
109
102
  try {
110
- const text = fs_1.default.readFileSync(filePath, "utf-8");
111
- const lines = text.split("\n");
112
- const entries = [];
113
- for (let i = 0; i < lines.length; i++) {
114
- const trimmed = lines[i].trim();
115
- for (const pat of SYMBOL_PATTERNS) {
116
- const m = trimmed.match(pat);
117
- if (m?.[1]) {
118
- entries.push({
119
- name: m[1],
120
- code: lines.slice(i, i + 40).join("\n"),
121
- file: filePath,
122
- });
123
- break;
124
- }
125
- }
126
- }
127
- return entries;
103
+ const source = fs_1.default.readFileSync(filePath, "utf-8");
104
+ const nodes = await (0, ast_parser_1.parseFile)(filePath, source);
105
+ return nodes.map(n => ({
106
+ name: n.name,
107
+ code: n.body.slice(0, 2000), // cap to avoid huge payloads
108
+ file: filePath,
109
+ fingerprint: n.fingerprint,
110
+ }));
128
111
  }
129
112
  catch {
130
113
  return [];
@@ -180,6 +163,7 @@ async function inferAndCapture(token, symbol, repo) {
180
163
  confidence: 0.35,
181
164
  machineId: (0, persistence_config_1.readConfig)().machineId ?? undefined,
182
165
  displayName: os.hostname(),
166
+ fingerprint: symbol.fingerprint ?? undefined,
183
167
  }),
184
168
  });
185
169
  return saveRes.ok;
@@ -210,7 +194,7 @@ async function scanRepoCommand(options = {}) {
210
194
  const files = walkFiles(repoRoot);
211
195
  const allSymbols = [];
212
196
  for (const file of files) {
213
- allSymbols.push(...extractSymbols(file));
197
+ allSymbols.push(...(await extractSymbols(file)));
214
198
  }
215
199
  if (allSymbols.length === 0) {
216
200
  if (!options.silent)
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ /**
3
+ * ast-parser.ts
4
+ *
5
+ * Extracts symbol nodes from source files using proper AST parsing for
6
+ * TypeScript/JavaScript and improved structural parsing for other languages.
7
+ *
8
+ * Returns SymbolNode objects with stable fingerprints that survive renames.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.computeFingerprint = computeFingerprint;
45
+ exports.parseFile = parseFile;
46
+ const path = __importStar(require("path"));
47
+ const crypto_1 = require("crypto");
48
+ // ── Fingerprint ───────────────────────────────────────────────────────────────
49
+ /**
50
+ * Normalise function body and hash it.
51
+ * Strips: whitespace, comments, string literals.
52
+ * Survives: renames, variable name changes, log additions, typo fixes.
53
+ * Breaks on: structural logic changes (intentional).
54
+ */
55
+ function computeFingerprint(body) {
56
+ const normalised = body
57
+ .replace(/\/\/.*$/gm, "") // remove single-line comments
58
+ .replace(/\/\*[\s\S]*?\*\//g, "") // remove multi-line comments
59
+ .replace(/#.*$/gm, "") // remove Python comments
60
+ .replace(/(["\`\'])(?:(?!\1)[\s\S])*?\1/g, "STR") // replace string literals
61
+ .replace(/\b\d+\b/g, "NUM") // replace numeric literals
62
+ .replace(/\s+/g, " ") // collapse whitespace
63
+ .trim();
64
+ return (0, crypto_1.createHash)("sha256").update(normalised).digest("hex").slice(0, 16);
65
+ }
66
+ // ── TypeScript / JavaScript parser ───────────────────────────────────────────
67
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
68
+ const { parse: tsParse } = require("@typescript-eslint/typescript-estree");
69
+ async function parseTypeScript(filePath, source) {
70
+ try {
71
+ const parse = tsParse;
72
+ const isTs = /\.(ts|tsx)$/.test(filePath);
73
+ const ast = parse(source, {
74
+ jsx: true,
75
+ loc: true,
76
+ range: true,
77
+ errorRecovery: true,
78
+ ...(isTs ? { filePath } : {}),
79
+ });
80
+ const symbols = [];
81
+ const lines = source.split("\n");
82
+ function getBody(start, end) {
83
+ return source.slice(start, end);
84
+ }
85
+ function visit(node, className) {
86
+ if (!node || typeof node !== "object")
87
+ return;
88
+ if (node.type === "FunctionDeclaration" && node.id?.name) {
89
+ const body = getBody(node.range[0], node.range[1]);
90
+ symbols.push({
91
+ name: node.id.name,
92
+ type: "function",
93
+ file: filePath,
94
+ startLine: node.loc.start.line,
95
+ endLine: node.loc.end.line,
96
+ body,
97
+ fingerprint: computeFingerprint(body),
98
+ });
99
+ }
100
+ if ((node.type === "VariableDeclarator") &&
101
+ node.id?.name &&
102
+ node.init &&
103
+ (node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression")) {
104
+ const body = getBody(node.init.range[0], node.init.range[1]);
105
+ symbols.push({
106
+ name: node.id.name,
107
+ type: "arrow_function",
108
+ file: filePath,
109
+ startLine: node.loc.start.line,
110
+ endLine: node.loc.end.line,
111
+ body,
112
+ fingerprint: computeFingerprint(body),
113
+ });
114
+ }
115
+ if (node.type === "ClassDeclaration" ||
116
+ node.type === "ClassExpression") {
117
+ const name = node.id?.name;
118
+ if (name) {
119
+ const body = getBody(node.range[0], node.range[1]);
120
+ symbols.push({
121
+ name,
122
+ type: "class",
123
+ file: filePath,
124
+ startLine: node.loc.start.line,
125
+ endLine: node.loc.end.line,
126
+ body,
127
+ fingerprint: computeFingerprint(body),
128
+ });
129
+ // Visit methods inside the class
130
+ if (node.body?.body) {
131
+ for (const member of node.body.body) {
132
+ if ((member.type === "MethodDefinition" || member.type === "PropertyDefinition") &&
133
+ member.key?.name &&
134
+ member.value?.type === "FunctionExpression") {
135
+ const mBody = getBody(member.range[0], member.range[1]);
136
+ symbols.push({
137
+ name: `${name}.${member.key.name}`,
138
+ type: "method",
139
+ file: filePath,
140
+ startLine: member.loc.start.line,
141
+ endLine: member.loc.end.line,
142
+ body: mBody,
143
+ fingerprint: computeFingerprint(mBody),
144
+ });
145
+ }
146
+ }
147
+ }
148
+ }
149
+ return; // already handled children
150
+ }
151
+ // Recurse
152
+ for (const key of Object.keys(node)) {
153
+ if (key === "parent")
154
+ continue;
155
+ const child = node[key];
156
+ if (Array.isArray(child)) {
157
+ for (const c of child)
158
+ visit(c, className);
159
+ }
160
+ else if (child && typeof child === "object" && child.type) {
161
+ visit(child, className);
162
+ }
163
+ }
164
+ }
165
+ visit(ast);
166
+ return symbols;
167
+ }
168
+ catch {
169
+ return [];
170
+ }
171
+ }
172
+ // ── Structural parser for other languages ────────────────────────────────────
173
+ /**
174
+ * Extracts symbols using brace-depth tracking.
175
+ * Works for Go, Rust, Java, Kotlin, PHP, Ruby, C/C++.
176
+ */
177
+ function parseStructural(filePath, source, patterns) {
178
+ const symbols = [];
179
+ const lines = source.split("\n");
180
+ for (let i = 0; i < lines.length; i++) {
181
+ const line = lines[i].trim();
182
+ for (const pattern of patterns) {
183
+ const match = line.match(pattern);
184
+ if (!match?.[1])
185
+ continue;
186
+ const name = match[1];
187
+ const startLine = i + 1;
188
+ // Find the matching closing brace
189
+ let depth = 0;
190
+ let endLine = startLine;
191
+ let started = false;
192
+ for (let j = i; j < lines.length; j++) {
193
+ for (const ch of lines[j]) {
194
+ if (ch === "{") {
195
+ depth++;
196
+ started = true;
197
+ }
198
+ if (ch === "}") {
199
+ depth--;
200
+ }
201
+ }
202
+ if (started && depth === 0) {
203
+ endLine = j + 1;
204
+ break;
205
+ }
206
+ }
207
+ const body = lines.slice(i, endLine).join("\n");
208
+ symbols.push({
209
+ name,
210
+ type: line.match(/class\s/) ? "class" : "function",
211
+ file: filePath,
212
+ startLine,
213
+ endLine,
214
+ body,
215
+ fingerprint: computeFingerprint(body),
216
+ });
217
+ break;
218
+ }
219
+ }
220
+ return symbols;
221
+ }
222
+ /** Python uses indentation-based scoping */
223
+ function parsePython(filePath, source) {
224
+ const symbols = [];
225
+ const lines = source.split("\n");
226
+ const defPattern = /^(async\s+)?def\s+(\w+)\s*\(/;
227
+ const classPattern = /^class\s+(\w+)/;
228
+ for (let i = 0; i < lines.length; i++) {
229
+ const line = lines[i];
230
+ const defMatch = line.match(defPattern);
231
+ const classMatch = line.match(classPattern);
232
+ const match = defMatch || classMatch;
233
+ if (!match)
234
+ continue;
235
+ const name = defMatch ? match[2] : match[1];
236
+ const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
237
+ const startLine = i + 1;
238
+ let endLine = startLine;
239
+ for (let j = i + 1; j < lines.length; j++) {
240
+ const nextLine = lines[j];
241
+ if (nextLine.trim() === "")
242
+ continue;
243
+ const nextIndent = nextLine.match(/^(\s*)/)?.[1].length ?? 0;
244
+ if (nextIndent <= indent && nextLine.trim() !== "") {
245
+ endLine = j;
246
+ break;
247
+ }
248
+ endLine = j + 1;
249
+ }
250
+ const body = lines.slice(i, endLine).join("\n");
251
+ symbols.push({
252
+ name,
253
+ type: classMatch ? "class" : "function",
254
+ file: filePath,
255
+ startLine,
256
+ endLine,
257
+ body,
258
+ fingerprint: computeFingerprint(body),
259
+ });
260
+ }
261
+ return symbols;
262
+ }
263
+ // ── Language dispatch ─────────────────────────────────────────────────────────
264
+ const GO_PATTERNS = [
265
+ /^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/,
266
+ ];
267
+ const RUST_PATTERNS = [
268
+ /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
269
+ ];
270
+ const JAVA_PATTERNS = [
271
+ /^(?:public|private|protected|static|\s)+[\w<>\[\]]+\s+(\w+)\s*\(/,
272
+ /^(?:public|private|protected|abstract|\s)*class\s+(\w+)/,
273
+ ];
274
+ const PHP_PATTERNS = [
275
+ /^(?:public|private|protected|static|\s)*function\s+(\w+)\s*\(/,
276
+ /^(?:abstract\s+)?class\s+(\w+)/,
277
+ ];
278
+ const RUBY_PATTERNS = [
279
+ /^def\s+(\w+)/,
280
+ /^class\s+(\w+)/,
281
+ ];
282
+ const KOTLIN_PATTERNS = [
283
+ /^(?:fun|override\s+fun|suspend\s+fun|private\s+fun|public\s+fun)\s+(\w+)/,
284
+ /^(?:class|data class|object|interface)\s+(\w+)/,
285
+ ];
286
+ async function parseFile(filePath, source) {
287
+ const ext = path.extname(filePath).toLowerCase();
288
+ switch (ext) {
289
+ case ".ts":
290
+ case ".tsx":
291
+ case ".js":
292
+ case ".jsx":
293
+ case ".mjs":
294
+ case ".cjs":
295
+ return parseTypeScript(filePath, source);
296
+ case ".py":
297
+ return parsePython(filePath, source);
298
+ case ".go":
299
+ return parseStructural(filePath, source, GO_PATTERNS);
300
+ case ".rs":
301
+ return parseStructural(filePath, source, RUST_PATTERNS);
302
+ case ".java":
303
+ return parseStructural(filePath, source, JAVA_PATTERNS);
304
+ case ".php":
305
+ return parseStructural(filePath, source, PHP_PATTERNS);
306
+ case ".rb":
307
+ return parseStructural(filePath, source, RUBY_PATTERNS);
308
+ case ".kt":
309
+ return parseStructural(filePath, source, KOTLIN_PATTERNS);
310
+ default:
311
+ return [];
312
+ }
313
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodingo-cli",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Kodingo CLI",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -22,6 +22,8 @@
22
22
  "dependencies": {
23
23
  "@babel/parser": "^7.29.0",
24
24
  "@babel/traverse": "^7.29.0",
25
+ "@typescript-eslint/types": "^8.61.1",
26
+ "@typescript-eslint/typescript-estree": "^8.61.1",
25
27
  "commander": "^12.1.0",
26
28
  "kodingo-core": "^0.1.0",
27
29
  "open": "^11.0.0",