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.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # kodingo-cli
2
+
3
+ ## Purpose
4
+
5
+ This repository contains the Kodingo command-line interface.
6
+
7
+ The CLI acts as a local adapter between a developer’s environment and the Kodingo core engine.
8
+
9
+ It is responsible for observing, normalizing, and forwarding signals —
10
+ not interpreting them.
11
+
12
+ ---
13
+
14
+ ## What This Repo Owns
15
+
16
+ - CLI commands and UX
17
+ - Local project initialization
18
+ - Project scoping and isolation
19
+ - Authentication and authorization flow
20
+ - Event emission to kodingo-core
21
+ - Local configuration and credentials
22
+
23
+ ---
24
+
25
+ ## What This Repo Does NOT Do
26
+
27
+ - No decision inference
28
+ - No memory interpretation
29
+ - No confidence calculation
30
+ - No domain logic
31
+
32
+ ---
33
+
34
+ ## Design Constraints
35
+
36
+ - The CLI must be safe to run locally
37
+ - It must never leak data across projects
38
+ - It must respect paid vs unpaid project boundaries
39
+ - All intelligence lives elsewhere
40
+
41
+ ---
42
+
43
+ ## Dependencies
44
+
45
+ - Depends on `kodingo-core`
46
+ - Depends on `kodingo-schema`
47
+
48
+ ---
49
+
50
+ ## Security Model
51
+
52
+ - Projects are explicitly initialized
53
+ - Access is opt-in per project
54
+ - Local identity is scoped per project directory
55
+
56
+ ---
57
+
58
+ ## Status
59
+
60
+ Interface design phase.
61
+
62
+ ---
63
+
64
+ ## Local DB Setup
65
+
66
+ ### 1) Run Postgres locally
67
+
68
+ Use Docker:
69
+
70
+ ```bash
71
+ docker run --name kodingo-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=kodingo -p 5432:5432 -d postgres:16
72
+ ```
73
+
74
+ Ensure `psql` is available in your PATH (it ships with Postgres client tools).
75
+
76
+ ### 2) Configure environment
77
+
78
+ Create a `.env` file (or export in shell):
79
+
80
+ ```bash
81
+ export DATABASE_URL=postgres://postgres:postgres@localhost:5432/kodingo
82
+ ```
83
+
84
+ ### 3) Build and run the CLI
85
+
86
+ ```bash
87
+ npm ci
88
+ npm run build
89
+ node src/cli.js capture --type decision --title "Chosen DB" --content "We chose Postgres for persistence." --tags "architecture,db"
90
+ node src/cli.js query-memory "Postgres" --limit 5
91
+ ```
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,55 @@
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.inferDecisionSummary = inferDecisionSummary;
7
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
8
+ async function inferDecisionSummary(input) {
9
+ const apiKey = process.env.ANTHROPIC_API_KEY;
10
+ if (!apiKey) {
11
+ return buildFallback(input);
12
+ }
13
+ try {
14
+ const client = new sdk_1.default({ apiKey });
15
+ const prompt = [
16
+ "You are a senior software engineer reviewing a code change.",
17
+ "Your job is to interpret what this change represents as a decision or evolution in the codebase.",
18
+ "",
19
+ "Write 2 to 4 sentences of plain prose. No markdown, no bullet points, no headers.",
20
+ "Focus on intent and reasoning, not on describing the diff mechanically.",
21
+ "Treat the commit message as a low-trust hint only, not as truth.",
22
+ "Use the symbol and file paths to understand what part of the system changed.",
23
+ "If intent cannot be confidently inferred, say so honestly in plain English.",
24
+ "",
25
+ `Repository: ${input.repoName}`,
26
+ `Symbol: ${input.symbol}`,
27
+ `Changed files: ${input.changedFiles.join(", ")}`,
28
+ `Commit message (low-trust hint): ${input.commitMessage}`,
29
+ "",
30
+ "Diff:",
31
+ input.diff.slice(0, 8000),
32
+ ].join("\n");
33
+ const response = await client.messages.create({
34
+ model: "claude-opus-4-5",
35
+ max_tokens: 300,
36
+ messages: [{ role: "user", content: prompt }],
37
+ });
38
+ const textBlock = response.content.find((b) => b.type === "text");
39
+ if (textBlock && textBlock.type === "text") {
40
+ return textBlock.text.trim();
41
+ }
42
+ return buildFallback(input);
43
+ }
44
+ catch {
45
+ return buildFallback(input);
46
+ }
47
+ }
48
+ function buildFallback(input) {
49
+ const shortHash = extractShortHash(input.diff);
50
+ return `Changes to ${input.symbol} across ${input.changedFiles.length} file(s)${shortHash ? ` in commit ${shortHash}` : ""}. Commit message: ${input.commitMessage}.`;
51
+ }
52
+ function extractShortHash(diff) {
53
+ const match = diff.match(/^commit ([a-f0-9]{7,})/m);
54
+ return match ? match[1].slice(0, 8) : "";
55
+ }
@@ -0,0 +1,290 @@
1
+ "use strict";
2
+ /**
3
+ * Cloud persistence adapter for kodingo-cli.
4
+ * Drop-in replacement for memory-persistence.ts — same function signatures,
5
+ * same return types, but talks to kodingo-api over HTTP instead of psql.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.initDb = initDb;
9
+ exports.checkDatabaseConnection = checkDatabaseConnection;
10
+ exports.saveMemory = saveMemory;
11
+ exports.getMemoryById = getMemoryById;
12
+ exports.updateMemoryLifecycle = updateMemoryLifecycle;
13
+ exports.updateMemoryConfidence = updateMemoryConfidence;
14
+ exports.applySignalToMemory = applySignalToMemory;
15
+ exports.suppressProposedSiblings = suppressProposedSiblings;
16
+ exports.getMemoryByExternalId = getMemoryByExternalId;
17
+ exports.getProposedOrIgnoredRecordBySymbol = getProposedOrIgnoredRecordBySymbol;
18
+ exports.deleteMemoryById = deleteMemoryById;
19
+ exports.queryMemory = queryMemory;
20
+ exports.queryMemoryBySymbol = queryMemoryBySymbol;
21
+ function getConfig() {
22
+ const apiUrl = process.env.KODINGO_API_URL;
23
+ const token = process.env.KODINGO_API_TOKEN;
24
+ if (!apiUrl)
25
+ throw new Error("KODINGO_API_URL is not set");
26
+ if (!token)
27
+ throw new Error("KODINGO_API_TOKEN is not set");
28
+ return { apiUrl: apiUrl.replace(/\/$/, ""), token };
29
+ }
30
+ // ── HTTP client ───────────────────────────────────────────────────────────────
31
+ async function apiFetch(path, options = {}) {
32
+ const { apiUrl, token } = getConfig();
33
+ const res = await fetch(`${apiUrl}${path}`, {
34
+ ...options,
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ "X-Kodingo-Token": token,
38
+ ...(options.headers ?? {}),
39
+ },
40
+ });
41
+ if (!res.ok) {
42
+ const body = await res.text();
43
+ throw new Error(`kodingo-api error ${res.status}: ${body}`);
44
+ }
45
+ // 204 No Content
46
+ if (res.status === 204)
47
+ return undefined;
48
+ return res.json();
49
+ }
50
+ // ── Response → AdapterMemoryRecord mapper ─────────────────────────────────────
51
+ // The API returns camelCase JSON. Dates arrive as strings and are converted.
52
+ // Adapter-layer fields (externalId, correctsId, correctedById) are passed
53
+ // through so commands can display them without reaching back to the API.
54
+ function mapApiRecord(r) {
55
+ const record = {
56
+ id: r.id,
57
+ projectId: r.projectId,
58
+ repo: r.repo ?? "",
59
+ type: r.type,
60
+ content: r.content,
61
+ tags: r.tags ?? [],
62
+ status: r.status,
63
+ confidence: r.confidence,
64
+ evidence: {
65
+ strongCodeChangeCount: r.evidence?.strongCodeChangeCount ?? 0,
66
+ structuralCount: r.evidence?.structuralCount ?? 0,
67
+ metadataCount: r.evidence?.metadataCount ?? 0,
68
+ evidenceGain: r.evidence?.evidenceGain ?? 0,
69
+ },
70
+ createdAt: new Date(r.createdAt),
71
+ updatedAt: new Date(r.updatedAt),
72
+ // Adapter-layer fields — not on core MemoryRecord but present in API responses
73
+ externalId: r.externalId ?? null,
74
+ correctsId: r.correctsId ?? null,
75
+ correctedById: r.correctedById ?? null,
76
+ };
77
+ // Optional core fields — only set if present (exactOptionalPropertyTypes)
78
+ if (r.title)
79
+ record.title = r.title;
80
+ if (r.symbol)
81
+ record.symbol = r.symbol;
82
+ if (r.fingerprint)
83
+ record.fingerprint = r.fingerprint;
84
+ if (r.deniedAt)
85
+ record.deniedAt = new Date(r.deniedAt);
86
+ return record;
87
+ }
88
+ // ── Public API (mirrors memory-persistence.ts) ────────────────────────────────
89
+ /**
90
+ * No-op for cloud mode — schema is managed by kodingo-api.
91
+ */
92
+ async function initDb() {
93
+ // Cloud: schema is managed server-side, nothing to do locally.
94
+ }
95
+ /**
96
+ * Ping the API health endpoint to verify connectivity.
97
+ */
98
+ async function checkDatabaseConnection() {
99
+ const { apiUrl } = getConfig();
100
+ const res = await fetch(`${apiUrl}/health`);
101
+ if (!res.ok)
102
+ throw new Error(`kodingo-api health check failed: ${res.status}`);
103
+ }
104
+ /**
105
+ * Save a new memory record.
106
+ */
107
+ async function saveMemory(input) {
108
+ const body = {
109
+ type: input.type,
110
+ content: input.content,
111
+ status: input.status ?? "proposed",
112
+ confidence: input.confidence ?? 0.3,
113
+ tags: input.tags ?? [],
114
+ };
115
+ if (input.title)
116
+ body.title = input.title;
117
+ if (input.repoPath)
118
+ body.repo = input.repoPath;
119
+ if (input.symbol)
120
+ body.symbol = input.symbol;
121
+ if (input.externalId)
122
+ body.externalId = input.externalId;
123
+ if (input.correctsId)
124
+ body.correctsId = input.correctsId;
125
+ if (input.correctedById)
126
+ body.correctedById = input.correctedById;
127
+ const record = await apiFetch("/memory", {
128
+ method: "POST",
129
+ body: JSON.stringify(body),
130
+ });
131
+ return mapApiRecord(record);
132
+ }
133
+ /**
134
+ * Get a memory record by ID.
135
+ */
136
+ async function getMemoryById(id) {
137
+ try {
138
+ const record = await apiFetch(`/memory/${id}`);
139
+ return mapApiRecord(record);
140
+ }
141
+ catch (err) {
142
+ if (err.message?.includes("404"))
143
+ return null;
144
+ throw err;
145
+ }
146
+ }
147
+ /**
148
+ * Update lifecycle fields (status, confidence, correctedById).
149
+ */
150
+ async function updateMemoryLifecycle(params) {
151
+ const body = {
152
+ status: params.status,
153
+ confidence: params.confidence,
154
+ };
155
+ if (params.correctedById)
156
+ body.correctedById = params.correctedById;
157
+ const record = await apiFetch(`/memory/${params.id}`, {
158
+ method: "PATCH",
159
+ body: JSON.stringify(body),
160
+ });
161
+ return mapApiRecord(record);
162
+ }
163
+ /**
164
+ * Update only the confidence of a memory record (signal accumulation).
165
+ */
166
+ async function updateMemoryConfidence(params) {
167
+ const record = await apiFetch(`/memory/${params.id}/confidence`, {
168
+ method: "PATCH",
169
+ body: JSON.stringify({ confidence: params.confidence }),
170
+ });
171
+ return mapApiRecord(record);
172
+ }
173
+ /**
174
+ * Apply a signal weight class to a memory record (uses applySignal domain rules server-side).
175
+ */
176
+ async function applySignalToMemory(params) {
177
+ const record = await apiFetch(`/memory/${params.id}/evidence`, {
178
+ method: "PATCH",
179
+ body: JSON.stringify({ weightClass: params.weightClass }),
180
+ });
181
+ return mapApiRecord(record);
182
+ }
183
+ /**
184
+ * Suppress stale proposed siblings for a symbol after affirmation.
185
+ */
186
+ async function suppressProposedSiblings(params) {
187
+ const body = {
188
+ symbol: params.symbol,
189
+ excludeId: params.excludeId,
190
+ };
191
+ if (params.repoPath)
192
+ body.repo = params.repoPath;
193
+ const res = await apiFetch("/memory/suppress-siblings", {
194
+ method: "POST",
195
+ body: JSON.stringify(body),
196
+ });
197
+ return res.suppressed;
198
+ }
199
+ /**
200
+ * Look up a memory record by externalId, scoped to a repo.
201
+ */
202
+ async function getMemoryByExternalId(externalId, repoPath) {
203
+ const params = new URLSearchParams({ externalId, repo: repoPath });
204
+ const res = await apiFetch(`/memory?${params.toString()}`);
205
+ const record = res.data?.[0];
206
+ if (!record)
207
+ return null;
208
+ return mapApiRecord(record);
209
+ }
210
+ /**
211
+ * Find the best proposed or ignored record for a symbol in a repo.
212
+ */
213
+ async function getProposedOrIgnoredRecordBySymbol(params) {
214
+ const qs = new URLSearchParams({
215
+ symbol: params.symbol,
216
+ repo: params.repoPath,
217
+ });
218
+ const [proposed, ignored] = await Promise.all([
219
+ apiFetch(`/memory?${qs}&status=proposed&limit=1`),
220
+ apiFetch(`/memory?${qs}&status=ignored&limit=1`),
221
+ ]);
222
+ const candidates = [...(proposed.data ?? []), ...(ignored.data ?? [])].map(mapApiRecord);
223
+ if (candidates.length === 0)
224
+ return null;
225
+ return candidates.sort((a, b) => {
226
+ if (a.status === "proposed" && b.status !== "proposed")
227
+ return -1;
228
+ if (b.status === "proposed" && a.status !== "proposed")
229
+ return 1;
230
+ return b.confidence - a.confidence;
231
+ })[0];
232
+ }
233
+ /**
234
+ * Delete a memory record by ID.
235
+ */
236
+ async function deleteMemoryById(id) {
237
+ await apiFetch(`/memory/${id}`, { method: "DELETE" });
238
+ }
239
+ /**
240
+ * Text query — full-text search via the API.
241
+ */
242
+ async function queryMemory(text, limit = 10, repoPath, opts = {}) {
243
+ const qs = new URLSearchParams({ limit: String(limit) });
244
+ if (repoPath)
245
+ qs.set("repo", repoPath);
246
+ if (!opts.includeNonCanonical)
247
+ qs.set("status", "proposed");
248
+ // Use server-side FTS when text is provided
249
+ const term = text.trim();
250
+ if (term)
251
+ qs.set("q", term);
252
+ const res = await apiFetch(`/memory?${qs.toString()}`);
253
+ let records = (res.data ?? []).map(mapApiRecord);
254
+ if (opts.dedupeBySymbol) {
255
+ const seen = new Set();
256
+ records = records.filter((r) => {
257
+ const key = r.symbol ?? r.id;
258
+ if (seen.has(key))
259
+ return false;
260
+ seen.add(key);
261
+ return true;
262
+ });
263
+ }
264
+ return records.slice(0, limit);
265
+ }
266
+ /**
267
+ * Symbol query with canonical preference.
268
+ */
269
+ async function queryMemoryBySymbol(params) {
270
+ const qs = new URLSearchParams({
271
+ symbol: params.symbol,
272
+ limit: String(params.limit ?? 10),
273
+ });
274
+ if (params.repoPath)
275
+ qs.set("repo", params.repoPath);
276
+ if (params.onlyCanonical)
277
+ qs.set("status", "affirmed");
278
+ const res = await apiFetch(`/memory?${qs.toString()}`);
279
+ let records = (res.data ?? []).map(mapApiRecord);
280
+ if (!params.includeDenied && !params.onlyCanonical) {
281
+ records = records.filter((r) => r.status !== "denied");
282
+ }
283
+ return records.sort((a, b) => {
284
+ if (a.status === "affirmed" && b.status !== "affirmed")
285
+ return -1;
286
+ if (b.status === "affirmed" && a.status !== "affirmed")
287
+ return 1;
288
+ return b.confidence - a.confidence;
289
+ });
290
+ }