kodingo-cli 1.0.10 → 1.0.11
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.
|
@@ -9,6 +9,35 @@ const parser_1 = require("@babel/parser");
|
|
|
9
9
|
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
|
+
// ── Known AI agent commit signatures ─────────────────────────────────────────
|
|
13
|
+
// Agents leave identifiable co-author or author signatures in commits.
|
|
14
|
+
const AGENT_AUTHOR_PATTERNS = [
|
|
15
|
+
/co-authored-by:.*copilot/i,
|
|
16
|
+
/co-authored-by:.*github-actions/i,
|
|
17
|
+
/co-authored-by:.*cursor/i,
|
|
18
|
+
/co-authored-by:.*codeium/i,
|
|
19
|
+
/co-authored-by:.*tabnine/i,
|
|
20
|
+
/co-authored-by:.*claude/i,
|
|
21
|
+
/co-authored-by:.*codex/i,
|
|
22
|
+
/\[claude code\]/i,
|
|
23
|
+
/\[cursor\]/i,
|
|
24
|
+
/generated by copilot/i,
|
|
25
|
+
/auto-generated/i,
|
|
26
|
+
];
|
|
27
|
+
function detectAgentAuthor(commit) {
|
|
28
|
+
const haystack = [
|
|
29
|
+
commit.message,
|
|
30
|
+
commit.body ?? "",
|
|
31
|
+
commit.author_name,
|
|
32
|
+
].join("\n").toLowerCase();
|
|
33
|
+
for (const pattern of AGENT_AUTHOR_PATTERNS) {
|
|
34
|
+
if (pattern.test(haystack)) {
|
|
35
|
+
const match = haystack.match(/co-authored-by:\s*([^\n<]+)/i);
|
|
36
|
+
return match ? match[1].trim() : "ai-agent";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
12
41
|
/**
|
|
13
42
|
* Git Adapter
|
|
14
43
|
* - Polls for new commits
|
|
@@ -17,6 +46,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
17
46
|
*
|
|
18
47
|
* MVP principle: rely primarily on code changes, not commit messages.
|
|
19
48
|
* Commit messages are retained as metadata only.
|
|
49
|
+
* Agent commits are detected and tagged — diff-first inference applied.
|
|
20
50
|
*/
|
|
21
51
|
class GitListenerAdapter {
|
|
22
52
|
constructor(repoPath, eventPort, pollInterval = 5000) {
|
|
@@ -30,10 +60,6 @@ class GitListenerAdapter {
|
|
|
30
60
|
await this.processLatestCommit();
|
|
31
61
|
this.startWatching();
|
|
32
62
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Run exactly once: process the latest commit only.
|
|
35
|
-
* (Used by Phase 3.1 "run once" command.)
|
|
36
|
-
*/
|
|
37
63
|
async runOnceLatest() {
|
|
38
64
|
await this.processLatestCommit();
|
|
39
65
|
}
|
|
@@ -46,16 +72,11 @@ class GitListenerAdapter {
|
|
|
46
72
|
const log = await this.git.log({ maxCount: 50 });
|
|
47
73
|
const commits = (log.all ?? []);
|
|
48
74
|
const newCommits = this.takeCommitsSinceLast(commits, this.lastProcessedCommit);
|
|
49
|
-
// Process older -> newer (deterministic evolution)
|
|
50
75
|
for (const commit of newCommits.reverse()) {
|
|
51
76
|
await this.processCommit(commit);
|
|
52
77
|
this.lastProcessedCommit = commit.hash;
|
|
53
78
|
}
|
|
54
79
|
}
|
|
55
|
-
/**
|
|
56
|
-
* From HEAD backward, collect commits until we reach lastHash.
|
|
57
|
-
* If lastHash is null, return everything passed in.
|
|
58
|
-
*/
|
|
59
80
|
takeCommitsSinceLast(commits, lastHash) {
|
|
60
81
|
if (!lastHash)
|
|
61
82
|
return [...commits];
|
|
@@ -70,37 +91,32 @@ class GitListenerAdapter {
|
|
|
70
91
|
async processCommit(commit) {
|
|
71
92
|
const projectId = await this.getProjectId();
|
|
72
93
|
const diffFiles = await this.getCommitDiff(commit.hash);
|
|
73
|
-
// Extract JS/TS symbols (best effort) — enrich signal metadata
|
|
74
94
|
const touchedSymbols = this.extractTouchedSymbols(diffFiles);
|
|
75
|
-
//
|
|
95
|
+
// Detect if this commit was authored by an AI agent
|
|
96
|
+
const agentAuthor = detectAgentAuthor(commit);
|
|
76
97
|
const decision = {
|
|
77
98
|
id: commit.hash,
|
|
78
99
|
projectId,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
// For agent commits: explicitly note the diff is the source of truth
|
|
101
|
+
rationale: agentAuthor
|
|
102
|
+
? `Inferred from AI agent code changes (${agentAuthor})`
|
|
103
|
+
: "Inferred from code changes",
|
|
104
|
+
changeSummary: this.buildChangeSummary(commit, diffFiles, touchedSymbols, agentAuthor),
|
|
105
|
+
confidence: agentAuthor ? 0.35 : 0.3,
|
|
82
106
|
proposedAt: new Date(commit.date),
|
|
83
107
|
};
|
|
84
|
-
// Core-aligned context: requires priorMemory + source
|
|
85
|
-
// MVP: empty prior memory (DB integration can enrich later)
|
|
86
108
|
const priorMemory = {
|
|
87
109
|
projectId,
|
|
88
110
|
decisions: [],
|
|
89
111
|
updatedAt: new Date(0),
|
|
90
112
|
};
|
|
91
|
-
// kodingo-core currently restricts source to "human" | "system".
|
|
92
|
-
// Git ingestion is an automated system signal, so use "system".
|
|
93
113
|
const context = {
|
|
94
114
|
priorMemory,
|
|
95
115
|
source: "system",
|
|
96
116
|
};
|
|
97
117
|
await this.eventPort.pushDecision(decision, context);
|
|
98
118
|
}
|
|
99
|
-
|
|
100
|
-
* Stable summary string that keeps useful Git metadata.
|
|
101
|
-
* Commit message stored as metadata only (low-trust).
|
|
102
|
-
*/
|
|
103
|
-
buildChangeSummary(commit, diffFiles, symbols) {
|
|
119
|
+
buildChangeSummary(commit, diffFiles, symbols, agentAuthor) {
|
|
104
120
|
const files = diffFiles.map((f) => ({
|
|
105
121
|
file: f.file,
|
|
106
122
|
insertions: f.insertions,
|
|
@@ -110,16 +126,19 @@ class GitListenerAdapter {
|
|
|
110
126
|
const summaryObj = {
|
|
111
127
|
source: "git",
|
|
112
128
|
commitHash: commit.hash,
|
|
113
|
-
|
|
129
|
+
// For agent commits, commit message is even lower trust — flag it
|
|
130
|
+
commitMessage: agentAuthor ? `[agent: ${agentAuthor}] ${commit.message}` : commit.message,
|
|
114
131
|
author: commit.author_name,
|
|
115
132
|
filesChanged: files,
|
|
116
133
|
symbolsTouched: symbols,
|
|
117
134
|
};
|
|
135
|
+
if (agentAuthor) {
|
|
136
|
+
summaryObj.agentAuthored = true;
|
|
137
|
+
summaryObj.agentAuthor = agentAuthor;
|
|
138
|
+
summaryObj.tags = ["agent-commit"];
|
|
139
|
+
}
|
|
118
140
|
return JSON.stringify(summaryObj);
|
|
119
141
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Diff summary for a single commit.
|
|
122
|
-
*/
|
|
123
142
|
async getCommitDiff(commitHash) {
|
|
124
143
|
const diffSummary = await this.git.diffSummary([`${commitHash}^!`]);
|
|
125
144
|
return (diffSummary.files ?? []).map((f) => ({
|
|
@@ -144,7 +163,6 @@ class GitListenerAdapter {
|
|
|
144
163
|
await this.processCommit(latestCommit);
|
|
145
164
|
this.lastProcessedCommit = latestCommit.hash;
|
|
146
165
|
}
|
|
147
|
-
/**
|
|
148
166
|
/**
|
|
149
167
|
* Extract touched function/class/method names from changed files.
|
|
150
168
|
* Supports: JS, TS, JSX, TSX (AST), PHP, Python, Go, Rust, Java, Kotlin, Ruby, C/C++ (regex).
|
|
@@ -159,7 +177,6 @@ class GitListenerAdapter {
|
|
|
159
177
|
continue;
|
|
160
178
|
const src = fs_1.default.readFileSync(absFilePath, "utf-8");
|
|
161
179
|
const ext = file.file.split(".").pop()?.toLowerCase() ?? "";
|
|
162
|
-
// JS / TS / JSX / TSX — AST parser
|
|
163
180
|
if (["ts", "tsx", "js", "jsx", "mjs", "cjs"].includes(ext)) {
|
|
164
181
|
try {
|
|
165
182
|
const ast = (0, parser_1.parse)(src, {
|
|
@@ -199,7 +216,6 @@ class GitListenerAdapter {
|
|
|
199
216
|
}
|
|
200
217
|
continue;
|
|
201
218
|
}
|
|
202
|
-
// PHP
|
|
203
219
|
if (ext === "php") {
|
|
204
220
|
for (const m of src.matchAll(/(?:function\s+(\w+)\s*\(|class\s+(\w+)(?:\s+extends|\s+implements|\s*\{))/g)) {
|
|
205
221
|
const name = m[1] || m[2];
|
|
@@ -208,7 +224,6 @@ class GitListenerAdapter {
|
|
|
208
224
|
}
|
|
209
225
|
continue;
|
|
210
226
|
}
|
|
211
|
-
// Python
|
|
212
227
|
if (ext === "py") {
|
|
213
228
|
for (const m of src.matchAll(/^(?:async\s+)?def\s+(\w+)\s*\(|^class\s+(\w+)/gm)) {
|
|
214
229
|
const name = m[1] || m[2];
|
|
@@ -217,7 +232,6 @@ class GitListenerAdapter {
|
|
|
217
232
|
}
|
|
218
233
|
continue;
|
|
219
234
|
}
|
|
220
|
-
// Go
|
|
221
235
|
if (ext === "go") {
|
|
222
236
|
for (const m of src.matchAll(/^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/gm)) {
|
|
223
237
|
if (m[1])
|
|
@@ -225,7 +239,6 @@ class GitListenerAdapter {
|
|
|
225
239
|
}
|
|
226
240
|
continue;
|
|
227
241
|
}
|
|
228
|
-
// Rust
|
|
229
242
|
if (ext === "rs") {
|
|
230
243
|
for (const m of src.matchAll(/^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)|^(?:pub\s+)?struct\s+(\w+)|^(?:pub\s+)?impl\s+(\w+)/gm)) {
|
|
231
244
|
const name = m[1] || m[2] || m[3];
|
|
@@ -234,7 +247,6 @@ class GitListenerAdapter {
|
|
|
234
247
|
}
|
|
235
248
|
continue;
|
|
236
249
|
}
|
|
237
|
-
// Java
|
|
238
250
|
if (ext === "java") {
|
|
239
251
|
for (const m of src.matchAll(/(?:public|private|protected|static|\s)+[\w<>\[\]]+\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+\w+\s*)?\{|(?:public|private|protected)?\s*(?:abstract\s+)?class\s+(\w+)/g)) {
|
|
240
252
|
const name = m[1] || m[2];
|
|
@@ -243,7 +255,6 @@ class GitListenerAdapter {
|
|
|
243
255
|
}
|
|
244
256
|
continue;
|
|
245
257
|
}
|
|
246
|
-
// Kotlin
|
|
247
258
|
if (ext === "kt" || ext === "kts") {
|
|
248
259
|
for (const m of src.matchAll(/(?:fun\s+(\w+)\s*\(|class\s+(\w+)|object\s+(\w+))/g)) {
|
|
249
260
|
const name = m[1] || m[2] || m[3];
|
|
@@ -252,7 +263,6 @@ class GitListenerAdapter {
|
|
|
252
263
|
}
|
|
253
264
|
continue;
|
|
254
265
|
}
|
|
255
|
-
// Ruby
|
|
256
266
|
if (ext === "rb") {
|
|
257
267
|
for (const m of src.matchAll(/^(?:def\s+(\w+)|class\s+(\w+))/gm)) {
|
|
258
268
|
const name = m[1] || m[2];
|
|
@@ -261,7 +271,6 @@ class GitListenerAdapter {
|
|
|
261
271
|
}
|
|
262
272
|
continue;
|
|
263
273
|
}
|
|
264
|
-
// C / C++
|
|
265
274
|
if (["c", "cpp", "cc", "cxx", "h", "hpp"].includes(ext)) {
|
|
266
275
|
for (const m of src.matchAll(/^[\w:*&<>\s]+\s+(\w+)\s*\([^)]*\)\s*(?:const\s*)?\{/gm)) {
|
|
267
276
|
if (m[1] && !["if", "for", "while", "switch", "catch"].includes(m[1])) {
|
package/dist/commands/init.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Interactively configures ~/.kodingo/config.json.
|
|
6
6
|
* Supports both local (psql) and cloud (kodingo-api) modes.
|
|
7
|
+
* On successful cloud init, ensures .kortex/ is in .gitignore.
|
|
7
8
|
*/
|
|
8
9
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
10
|
if (k2 === undefined) k2 = k;
|
|
@@ -69,6 +70,20 @@ async function verifyCloudConnection(apiUrl, token) {
|
|
|
69
70
|
if (!res.ok)
|
|
70
71
|
throw new Error(`Health check failed (${res.status}) — check your API URL`);
|
|
71
72
|
}
|
|
73
|
+
// ── .gitignore helper ─────────────────────────────────────────────────────────
|
|
74
|
+
function ensureGitignoreEntry(repoRoot, entry) {
|
|
75
|
+
const gitignorePath = path_1.default.join(repoRoot, ".gitignore");
|
|
76
|
+
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()))
|
|
80
|
+
return;
|
|
81
|
+
fs_1.default.appendFileSync(gitignorePath, `\n# Kortex — auto-generated context file\n${line}`, "utf-8");
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
fs_1.default.writeFileSync(gitignorePath, `# Kortex — auto-generated context file\n${line}`, "utf-8");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
72
87
|
// ── Command ───────────────────────────────────────────────────────────────────
|
|
73
88
|
function registerInitCommand(program) {
|
|
74
89
|
program
|
|
@@ -99,6 +114,8 @@ function registerInitCommand(program) {
|
|
|
99
114
|
apiUrl: opts.apiUrl,
|
|
100
115
|
token: opts.token,
|
|
101
116
|
});
|
|
117
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
118
|
+
ensureGitignoreEntry(repoRoot, ".kortex/");
|
|
102
119
|
console.log(`✔ Cloud mode configured. Config saved to ${persistence_config_1.CONFIG_PATH}`);
|
|
103
120
|
return;
|
|
104
121
|
}
|
|
@@ -135,9 +152,11 @@ function registerInitCommand(program) {
|
|
|
135
152
|
await verifyCloudConnection(apiUrl, token);
|
|
136
153
|
const config = { mode: "cloud", apiUrl, token };
|
|
137
154
|
(0, persistence_config_1.writeConfig)(config);
|
|
138
|
-
//
|
|
155
|
+
// Save workspace-specific config scoped to this repo
|
|
139
156
|
const repoRoot = findRepoRoot(process.cwd());
|
|
140
157
|
(0, persistence_config_1.writeWorkspaceConfig)(repoRoot, { mode: "cloud", apiUrl, token });
|
|
158
|
+
// Ensure .kortex/ is gitignored — it's a generated file, not source
|
|
159
|
+
ensureGitignoreEntry(repoRoot, ".kortex/");
|
|
141
160
|
console.log(`\n✔ Cloud mode configured successfully.`);
|
|
142
161
|
console.log(` API URL : ${apiUrl}`);
|
|
143
162
|
console.log(` Config : ${persistence_config_1.CONFIG_PATH}`);
|