kodingo-cli 1.0.0

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.
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CloudEventPort = void 0;
7
+ const kodingo_core_1 = require("kodingo-core");
8
+ const persistence_config_1 = require("../utils/persistence-config");
9
+ const inference_1 = require("../adapters/ai/inference");
10
+ const path_1 = __importDefault(require("path"));
11
+ /**
12
+ * EventPort implementation that persists via getPersistence().
13
+ * Used in cloud mode — drop-in replacement for DbEventPort.
14
+ * GitListenerAdapter only depends on EventPort, so it works with either.
15
+ */
16
+ class CloudEventPort {
17
+ constructor(repoPath) {
18
+ this.repoPath = repoPath;
19
+ this.persistence = (0, persistence_config_1.getPersistence)();
20
+ }
21
+ async pushDecision(decision, _context) {
22
+ await this.persistence.initDb();
23
+ const { title, content, symbol, externalId } = await this.mapDecisionToMemory(decision);
24
+ // Confidence accumulation: if symbol is present, look for existing record
25
+ if (symbol) {
26
+ const existing = await this.getProposedOrIgnoredBySymbol(symbol);
27
+ if (existing) {
28
+ const domainRecord = {
29
+ id: existing.id,
30
+ projectId: existing.projectId,
31
+ repo: this.repoPath,
32
+ type: "decision",
33
+ title: existing.title ?? "",
34
+ content: existing.content,
35
+ symbol: existing.symbol ?? "",
36
+ tags: existing.tags ?? [],
37
+ status: existing.status,
38
+ confidence: existing.confidence,
39
+ evidence: {
40
+ strongCodeChangeCount: 0,
41
+ structuralCount: 0,
42
+ metadataCount: 0,
43
+ evidenceGain: 0,
44
+ },
45
+ createdAt: existing.createdAt instanceof Date
46
+ ? existing.createdAt
47
+ : new Date(existing.createdAt),
48
+ updatedAt: existing.updatedAt instanceof Date
49
+ ? existing.updatedAt
50
+ : new Date(existing.updatedAt),
51
+ };
52
+ const updated = (0, kodingo_core_1.applySignal)(domainRecord, {
53
+ weightClass: "code_change",
54
+ });
55
+ await this.persistence.updateMemoryConfidence({
56
+ id: existing.id,
57
+ confidence: updated.confidence,
58
+ });
59
+ console.log(`Signal applied to existing memory: ${existing.id} (symbol=${symbol}, confidence=${updated.confidence.toFixed(2)})`);
60
+ return;
61
+ }
62
+ }
63
+ // No existing proposed/ignored record — insert new
64
+ const input = {
65
+ type: "decision",
66
+ content,
67
+ repoPath: this.repoPath,
68
+ status: "proposed",
69
+ confidence: 0.3,
70
+ externalId,
71
+ tags: ["signal:git", "kind:decision"],
72
+ };
73
+ if (title)
74
+ input.title = title;
75
+ if (symbol)
76
+ input.symbol = symbol;
77
+ await this.persistence.saveMemory(input);
78
+ }
79
+ /**
80
+ * Query for a proposed or ignored record by symbol, scoped to this repo.
81
+ * The cloud adapter exposes this via queryMemoryBySymbol — we replicate
82
+ * the same selection logic as getProposedOrIgnoredRecordBySymbol in db-adapter.
83
+ */
84
+ async getProposedOrIgnoredBySymbol(symbol) {
85
+ const results = await this.persistence.queryMemoryBySymbol({
86
+ symbol,
87
+ limit: 10,
88
+ repoPath: this.repoPath,
89
+ includeDenied: false,
90
+ onlyCanonical: false,
91
+ });
92
+ // Prefer proposed over ignored, then highest confidence
93
+ const candidates = results.filter((r) => r.status === "proposed" || r.status === "ignored");
94
+ if (candidates.length === 0)
95
+ return null;
96
+ return candidates.sort((a, b) => {
97
+ if (a.status === "proposed" && b.status !== "proposed")
98
+ return -1;
99
+ if (b.status === "proposed" && a.status !== "proposed")
100
+ return 1;
101
+ return b.confidence - a.confidence;
102
+ })[0];
103
+ }
104
+ async mapDecisionToMemory(decision) {
105
+ const summary = safeParseJson(decision.changeSummary);
106
+ const commitHash = typeof summary?.commitHash === "string"
107
+ ? summary.commitHash
108
+ : decision.id;
109
+ const filesChanged = Array.isArray(summary?.filesChanged)
110
+ ? summary.filesChanged
111
+ : [];
112
+ const symbolsTouched = Array.isArray(summary?.symbolsTouched)
113
+ ? summary.symbolsTouched
114
+ : [];
115
+ const symbol = this.pickBestSymbol(symbolsTouched, filesChanged);
116
+ const filePaths = extractFilePaths(filesChanged);
117
+ const repoName = path_1.default.basename(this.repoPath);
118
+ const shortHash = String(commitHash).slice(0, 8);
119
+ const content = await (0, inference_1.inferDecisionSummary)({
120
+ diff: typeof summary?.rawDiff === "string"
121
+ ? summary.rawDiff
122
+ : (decision.changeSummary ?? ""),
123
+ changedFiles: filePaths,
124
+ symbol: symbol ?? "unknown",
125
+ commitMessage: decision.rationale ?? "",
126
+ repoName,
127
+ });
128
+ const title = symbol
129
+ ? `Decision: changes to ${symbol}`
130
+ : `Decision: changes in commit ${shortHash}`;
131
+ const externalId = `git:${String(commitHash)}`;
132
+ return { title, content, symbol, externalId };
133
+ }
134
+ pickBestSymbol(symbolsTouched, filesChanged) {
135
+ const firstSymbol = typeof symbolsTouched?.[0] === "string"
136
+ ? symbolsTouched[0]
137
+ : null;
138
+ if (firstSymbol)
139
+ return firstSymbol;
140
+ const filePaths = extractFilePaths(filesChanged);
141
+ for (const p of filePaths) {
142
+ const feature = deriveFeatureSymbol(p);
143
+ if (feature)
144
+ return feature;
145
+ }
146
+ for (const p of filePaths) {
147
+ const folder = deriveTopFolderSymbol(p);
148
+ if (folder)
149
+ return folder;
150
+ }
151
+ return "repo";
152
+ }
153
+ }
154
+ exports.CloudEventPort = CloudEventPort;
155
+ // ── Shared helpers (mirrored from DbEventPort) ────────────────────────────────
156
+ function extractFilePaths(filesChanged) {
157
+ const out = [];
158
+ for (const item of filesChanged) {
159
+ if (item && typeof item === "object") {
160
+ const anyItem = item;
161
+ if (typeof anyItem.file === "string" && anyItem.file.trim()) {
162
+ out.push(normalizePath(anyItem.file.trim()));
163
+ }
164
+ }
165
+ }
166
+ return out;
167
+ }
168
+ function normalizePath(p) {
169
+ return p.replace(/\\/g, "/");
170
+ }
171
+ function deriveFeatureSymbol(filePath) {
172
+ const parts = filePath.split("/").filter(Boolean);
173
+ if (parts.length < 3)
174
+ return null;
175
+ if (parts[0] !== "lib")
176
+ return null;
177
+ if (parts[1] !== "features")
178
+ return null;
179
+ const featureName = parts[2];
180
+ if (!featureName)
181
+ return null;
182
+ return `feature:lib/features/${featureName}`;
183
+ }
184
+ function deriveTopFolderSymbol(filePath) {
185
+ const parts = filePath.split("/").filter(Boolean);
186
+ if (parts.length === 0)
187
+ return null;
188
+ if (parts[0] === "lib") {
189
+ if (parts.length >= 2) {
190
+ if (parts.length === 2 && parts[1].includes("."))
191
+ return "module:lib";
192
+ return `module:lib/${parts[1]}`;
193
+ }
194
+ return "module:lib";
195
+ }
196
+ return `path:${parts[0]}`;
197
+ }
198
+ function safeParseJson(input) {
199
+ if (typeof input !== "string")
200
+ return null;
201
+ try {
202
+ return JSON.parse(input);
203
+ }
204
+ catch {
205
+ return null;
206
+ }
207
+ }
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DbEventPort = void 0;
7
+ const kodingo_core_1 = require("kodingo-core");
8
+ const memory_persistence_1 = require("../adapters/db-adapter/memory-persistence");
9
+ const inference_1 = require("../adapters/ai/inference");
10
+ const path_1 = __importDefault(require("path"));
11
+ /**
12
+ * Minimal EventPort implementation for the CLI.
13
+ * Persists incoming Decisions as memory records using the DB adapter.
14
+ *
15
+ * MVP wiring:
16
+ * - status: proposed
17
+ * - confidence: 0.30 baseline (domain contract)
18
+ * - repoPath: provided at construction time
19
+ * - externalId: stable dedupe key (git:<commitHash>)
20
+ * - symbol:
21
+ * 1) first touched symbol if present (ts/js)
22
+ * 2) else derive folder symbol from changed files (flutter/dart etc.)
23
+ *
24
+ * Confidence accumulation:
25
+ * - When a symbol already has a proposed/ignored record, apply a code_change
26
+ * signal to raise its confidence instead of inserting a duplicate.
27
+ * - Affirmed/denied records are never modified — insert a new proposed record.
28
+ */
29
+ class DbEventPort {
30
+ constructor(repoPath) {
31
+ this.repoPath = repoPath;
32
+ }
33
+ async pushDecision(decision, _context) {
34
+ await (0, memory_persistence_1.initDb)();
35
+ const { title, content, symbol, externalId } = await this.mapDecisionToMemory(decision);
36
+ // Confidence accumulation: if symbol is present, look for existing record
37
+ if (symbol) {
38
+ const existing = await (0, memory_persistence_1.getProposedOrIgnoredRecordBySymbol)({
39
+ symbol,
40
+ repoPath: this.repoPath,
41
+ });
42
+ if (existing) {
43
+ // Build a minimal domain MemoryRecord for applySignal.
44
+ // Evidence counters are zeroed intentionally — full evidence replay
45
+ // is out of scope for MVP. Only confidence is persisted.
46
+ const domainRecord = {
47
+ id: existing.id,
48
+ projectId: "",
49
+ repo: this.repoPath,
50
+ type: "decision",
51
+ title: existing.title ?? "",
52
+ content: existing.content,
53
+ symbol: existing.symbol ?? "",
54
+ tags: existing.tags ?? [],
55
+ status: existing.status,
56
+ confidence: existing.confidence,
57
+ evidence: {
58
+ strongCodeChangeCount: 0,
59
+ structuralCount: 0,
60
+ metadataCount: 0,
61
+ evidenceGain: 0,
62
+ },
63
+ createdAt: existing.createdAt,
64
+ updatedAt: existing.createdAt,
65
+ };
66
+ const updated = (0, kodingo_core_1.applySignal)(domainRecord, {
67
+ weightClass: "code_change",
68
+ });
69
+ await (0, memory_persistence_1.updateMemoryConfidence)({
70
+ id: existing.id,
71
+ confidence: updated.confidence,
72
+ });
73
+ console.log(`Signal applied to existing memory: ${existing.id} (symbol=${symbol}, confidence=${updated.confidence.toFixed(2)})`);
74
+ return;
75
+ }
76
+ }
77
+ // No existing proposed/ignored record — insert new record as usual
78
+ const input = {
79
+ type: "decision",
80
+ content,
81
+ repoPath: this.repoPath,
82
+ status: "proposed",
83
+ confidence: 0.3,
84
+ externalId,
85
+ };
86
+ if (title)
87
+ input.title = title;
88
+ if (symbol)
89
+ input.symbol = symbol;
90
+ input.tags = ["signal:git", "kind:decision"];
91
+ await (0, memory_persistence_1.saveMemory)(input);
92
+ }
93
+ async mapDecisionToMemory(decision) {
94
+ const summary = safeParseJson(decision.changeSummary);
95
+ const commitHash = typeof summary?.commitHash === "string"
96
+ ? summary.commitHash
97
+ : decision.id;
98
+ const filesChanged = Array.isArray(summary?.filesChanged)
99
+ ? summary.filesChanged
100
+ : [];
101
+ const symbolsTouched = Array.isArray(summary?.symbolsTouched)
102
+ ? summary.symbolsTouched
103
+ : [];
104
+ const symbol = this.pickBestSymbol(symbolsTouched, filesChanged);
105
+ const filePaths = extractFilePaths(filesChanged);
106
+ const repoName = path_1.default.basename(this.repoPath);
107
+ const shortHash = String(commitHash).slice(0, 8);
108
+ const content = await (0, inference_1.inferDecisionSummary)({
109
+ diff: typeof summary?.rawDiff === "string"
110
+ ? summary.rawDiff
111
+ : (decision.changeSummary ?? ""),
112
+ changedFiles: filePaths,
113
+ symbol: symbol ?? "unknown",
114
+ commitMessage: decision.rationale ?? "",
115
+ repoName,
116
+ });
117
+ const title = symbol
118
+ ? `Decision: changes to ${symbol}`
119
+ : `Decision: changes in commit ${shortHash}`;
120
+ const externalId = `git:${String(commitHash)}`;
121
+ return { title, content, symbol, externalId };
122
+ }
123
+ pickBestSymbol(symbolsTouched, filesChanged) {
124
+ const firstSymbol = typeof symbolsTouched?.[0] === "string"
125
+ ? symbolsTouched[0]
126
+ : null;
127
+ if (firstSymbol)
128
+ return firstSymbol;
129
+ const filePaths = extractFilePaths(filesChanged);
130
+ for (const p of filePaths) {
131
+ const feature = deriveFeatureSymbol(p);
132
+ if (feature)
133
+ return feature;
134
+ }
135
+ for (const p of filePaths) {
136
+ const folder = deriveTopFolderSymbol(p);
137
+ if (folder)
138
+ return folder;
139
+ }
140
+ return "repo";
141
+ }
142
+ }
143
+ exports.DbEventPort = DbEventPort;
144
+ function extractFilePaths(filesChanged) {
145
+ const out = [];
146
+ for (const item of filesChanged) {
147
+ if (item && typeof item === "object") {
148
+ const anyItem = item;
149
+ if (typeof anyItem.file === "string" && anyItem.file.trim()) {
150
+ out.push(normalizePath(anyItem.file.trim()));
151
+ }
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+ function normalizePath(p) {
157
+ return p.replace(/\\/g, "/");
158
+ }
159
+ function deriveFeatureSymbol(filePath) {
160
+ const parts = filePath.split("/").filter(Boolean);
161
+ if (parts.length < 3)
162
+ return null;
163
+ if (parts[0] !== "lib")
164
+ return null;
165
+ if (parts[1] !== "features")
166
+ return null;
167
+ const featureName = parts[2];
168
+ if (!featureName)
169
+ return null;
170
+ return `feature:lib/features/${featureName}`;
171
+ }
172
+ function deriveTopFolderSymbol(filePath) {
173
+ const parts = filePath.split("/").filter(Boolean);
174
+ if (parts.length === 0)
175
+ return null;
176
+ if (parts[0] === "lib") {
177
+ if (parts.length >= 2) {
178
+ if (parts.length === 2 && parts[1].includes("."))
179
+ return "module:lib";
180
+ return `module:lib/${parts[1]}`;
181
+ }
182
+ return "module:lib";
183
+ }
184
+ return `path:${parts[0]}`;
185
+ }
186
+ function safeParseJson(input) {
187
+ if (typeof input !== "string")
188
+ return null;
189
+ try {
190
+ return JSON.parse(input);
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CONFIG_PATH = void 0;
7
+ exports.readConfig = readConfig;
8
+ exports.writeConfig = writeConfig;
9
+ exports.isCloudMode = isCloudMode;
10
+ exports.getPersistence = getPersistence;
11
+ /**
12
+ * Persistence config switcher.
13
+ * Reads ~/.kodingo/config.json and returns the correct adapter.
14
+ *
15
+ * Config file shape:
16
+ * {
17
+ * "mode": "cloud", // "local" | "cloud"
18
+ * "apiUrl": "https://...", // required when mode=cloud
19
+ * "token": "uuid" // required when mode=cloud
20
+ * }
21
+ *
22
+ * Falls back to "local" if config file is missing or mode=local.
23
+ */
24
+ const node_fs_1 = __importDefault(require("node:fs"));
25
+ const node_path_1 = __importDefault(require("node:path"));
26
+ const node_os_1 = __importDefault(require("node:os"));
27
+ exports.CONFIG_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".kodingo", "config.json");
28
+ function readConfig() {
29
+ if (!node_fs_1.default.existsSync(exports.CONFIG_PATH)) {
30
+ return { mode: "local" };
31
+ }
32
+ try {
33
+ const raw = node_fs_1.default.readFileSync(exports.CONFIG_PATH, "utf-8");
34
+ const parsed = JSON.parse(raw);
35
+ return parsed;
36
+ }
37
+ catch {
38
+ console.warn("Warning: could not parse ~/.kodingo/config.json — using local mode");
39
+ return { mode: "local" };
40
+ }
41
+ }
42
+ function writeConfig(config) {
43
+ const dir = node_path_1.default.dirname(exports.CONFIG_PATH);
44
+ if (!node_fs_1.default.existsSync(dir)) {
45
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
46
+ }
47
+ node_fs_1.default.writeFileSync(exports.CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
48
+ }
49
+ function isCloudMode() {
50
+ return readConfig().mode === "cloud";
51
+ }
52
+ /**
53
+ * Returns the correct persistence adapter based on config.
54
+ * Import this in commands instead of importing the adapters directly.
55
+ */
56
+ function getPersistence() {
57
+ const config = readConfig();
58
+ if (config.mode === "cloud") {
59
+ if (!config.apiUrl)
60
+ throw new Error("Cloud mode requires apiUrl in ~/.kodingo/config.json");
61
+ if (!config.token)
62
+ throw new Error("Cloud mode requires token in ~/.kodingo/config.json");
63
+ // Inject into env so cloud-persistence picks them up
64
+ process.env.KODINGO_API_URL = config.apiUrl;
65
+ process.env.KODINGO_API_TOKEN = config.token;
66
+ return require("../adapters/cloud-adapter/cloud-persistence");
67
+ }
68
+ return require("../adapters/db-adapter/memory-persistence");
69
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveRepoScope = resolveRepoScope;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function resolveRepoScope(options = {}) {
10
+ if (options.global) {
11
+ return null;
12
+ }
13
+ const basePath = options.repo
14
+ ? path_1.default.resolve(options.repo)
15
+ : path_1.default.resolve(options.cwd ?? process.cwd());
16
+ const repoRoot = findRepoRoot(basePath);
17
+ if (options.repo) {
18
+ return repoRoot ?? basePath;
19
+ }
20
+ return repoRoot ?? basePath;
21
+ }
22
+ function findRepoRoot(startPath) {
23
+ let current = normalizePath(startPath);
24
+ while (true) {
25
+ const gitPath = path_1.default.join(current, ".git");
26
+ if (fs_1.default.existsSync(gitPath)) {
27
+ return current;
28
+ }
29
+ const parent = path_1.default.dirname(current);
30
+ if (parent === current) {
31
+ return null;
32
+ }
33
+ current = parent;
34
+ }
35
+ }
36
+ function normalizePath(startPath) {
37
+ let current = path_1.default.resolve(startPath);
38
+ try {
39
+ const stats = fs_1.default.statSync(current);
40
+ if (stats.isFile()) {
41
+ current = path_1.default.dirname(current);
42
+ }
43
+ }
44
+ catch {
45
+ // If the path doesn't exist, keep the resolved path as-is.
46
+ }
47
+ return current;
48
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "kodingo-cli",
3
+ "version": "1.0.0",
4
+ "description": "Kodingo CLI",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "type": "commonjs",
8
+ "main": "dist/cli.js",
9
+ "bin": {
10
+ "kodingo": "dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "dev": "tsc -p tsconfig.json --watch",
18
+ "start": "node dist/cli.js",
19
+ "test": "echo \"Error: no test specified\" && exit 1",
20
+ "prepare": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@anthropic-ai/sdk": "^0.78.0",
24
+ "@babel/parser": "^7.29.0",
25
+ "@babel/traverse": "^7.29.0",
26
+ "commander": "^12.1.0",
27
+ "kodingo-core": "file:../kodingo-core",
28
+ "simple-git": "^3.30.0",
29
+ "uuid": "^10.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/babel__traverse": "^7.28.0",
33
+ "@types/node": "^22.0.0",
34
+ "@types/uuid": "^10.0.0",
35
+ "typescript": "^5.6.0"
36
+ }
37
+ }