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
- // Diff-first rationale: do not trust commit message as "why"
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
- rationale: "Inferred from code changes", // MVP: change-based inference
80
- changeSummary: this.buildChangeSummary(commit, diffFiles, touchedSymbols),
81
- confidence: 0.3, // proposed baseline (matches domain contract spirit)
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
- commitMessage: commit.message, // metadata only
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])) {
@@ -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
- // Also save workspace-specific config scoped to this repo
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodingo-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Kodingo CLI",
5
5
  "license": "MIT",
6
6
  "private": false,