kodingo-cli 1.0.11 → 1.0.12
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/dist/cli.js +12 -0
- package/dist/commands/scan-claude.js +179 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ const affirm_1 = require("./commands/affirm");
|
|
|
12
12
|
const ignore_1 = require("./commands/ignore");
|
|
13
13
|
const deny_1 = require("./commands/deny");
|
|
14
14
|
const scan_git_1 = require("./commands/scan-git");
|
|
15
|
+
const scan_claude_1 = require("./commands/scan-claude");
|
|
15
16
|
const init_1 = require("./commands/init");
|
|
16
17
|
const install_hook_1 = require("./commands/install-hook");
|
|
17
18
|
const update_1 = require("./commands/update");
|
|
@@ -202,4 +203,15 @@ program
|
|
|
202
203
|
payload.repo = String(options.repo);
|
|
203
204
|
await (0, install_hook_1.installHookCommand)(payload);
|
|
204
205
|
});
|
|
206
|
+
// ── scan-claude ──────────────────────────────────────────────────────────────
|
|
207
|
+
program
|
|
208
|
+
.command("scan-claude")
|
|
209
|
+
.description("Scan Claude Code session files and extract decisions into Kortex memory")
|
|
210
|
+
.option("--repo <path>", "repo path to scan")
|
|
211
|
+
.action(async (options) => {
|
|
212
|
+
const payload = {};
|
|
213
|
+
if (options.repo)
|
|
214
|
+
payload.repo = String(options.repo);
|
|
215
|
+
await (0, scan_claude_1.scanClaudeCommand)(payload);
|
|
216
|
+
});
|
|
205
217
|
program.parse(process.argv);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* kodingo scan-claude
|
|
4
|
+
*
|
|
5
|
+
* Reads Claude Code session files from .claude/sessions/ in the repo root
|
|
6
|
+
* and extracts decisions, rationale, and rejected approaches that never
|
|
7
|
+
* made it into a commit — signal that would otherwise be lost forever.
|
|
8
|
+
*
|
|
9
|
+
* Claude Code writes JSONL session files at:
|
|
10
|
+
* <repo>/.claude/sessions/<session-id>.jsonl
|
|
11
|
+
*
|
|
12
|
+
* Each line is a message in the conversation. We scan for:
|
|
13
|
+
* - Assistant messages containing architectural decisions or tradeoffs
|
|
14
|
+
* - Tool results showing code that was written then abandoned
|
|
15
|
+
* - Explicit reasoning blocks
|
|
16
|
+
*/
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.scanClaudeCommand = scanClaudeCommand;
|
|
22
|
+
const fs_1 = __importDefault(require("fs"));
|
|
23
|
+
const path_1 = __importDefault(require("path"));
|
|
24
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
25
|
+
// ── Heuristics for extracting decisions ──────────────────────────────────────
|
|
26
|
+
const DECISION_PATTERNS = [
|
|
27
|
+
/\b(?:decided|decision|chose|choosing|going with|we['']ll use|using|opted for|approach is)\b/i,
|
|
28
|
+
/\b(?:tradeoff|trade-off|because|reason|rationale|the issue is|the problem is)\b/i,
|
|
29
|
+
/\b(?:instead of|rather than|alternative|we could|we should|don['']t use|avoid)\b/i,
|
|
30
|
+
/\b(?:architecture|pattern|design|structure|strategy|approach)\b/i,
|
|
31
|
+
];
|
|
32
|
+
const REJECTION_PATTERNS = [
|
|
33
|
+
/\b(?:rejected|abandoned|won['']t work|doesn['']t work|not going to|let['']s not|avoid this)\b/i,
|
|
34
|
+
/\b(?:tried|attempted|doesn['']t fit|too complex|too slow|bad idea)\b/i,
|
|
35
|
+
];
|
|
36
|
+
function scoreText(text) {
|
|
37
|
+
let score = 0;
|
|
38
|
+
for (const pat of DECISION_PATTERNS) {
|
|
39
|
+
if (pat.test(text))
|
|
40
|
+
score += 2;
|
|
41
|
+
}
|
|
42
|
+
for (const pat of REJECTION_PATTERNS) {
|
|
43
|
+
if (pat.test(text))
|
|
44
|
+
score += 1;
|
|
45
|
+
}
|
|
46
|
+
return score;
|
|
47
|
+
}
|
|
48
|
+
function extractTextFromContent(content) {
|
|
49
|
+
if (typeof content === "string")
|
|
50
|
+
return content;
|
|
51
|
+
return content
|
|
52
|
+
.filter(b => b.type === "text" && b.text)
|
|
53
|
+
.map(b => b.text)
|
|
54
|
+
.join("\n");
|
|
55
|
+
}
|
|
56
|
+
// Split assistant message into paragraph-sized chunks
|
|
57
|
+
function splitIntoParagraphs(text) {
|
|
58
|
+
return text
|
|
59
|
+
.split(/\n{2,}/)
|
|
60
|
+
.map(p => p.trim())
|
|
61
|
+
.filter(p => p.length > 80);
|
|
62
|
+
}
|
|
63
|
+
function buildTitle(text) {
|
|
64
|
+
const first = text.split(/[.\n]/)[0].trim();
|
|
65
|
+
return first.length > 100 ? first.slice(0, 97) + "..." : first;
|
|
66
|
+
}
|
|
67
|
+
// ── Session parser ────────────────────────────────────────────────────────────
|
|
68
|
+
function parseSessionFile(filePath) {
|
|
69
|
+
const messages = [];
|
|
70
|
+
const lines = fs_1.default.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(line);
|
|
74
|
+
// Claude Code JSONL format: { role, content, ... }
|
|
75
|
+
if (parsed.role && parsed.content !== undefined) {
|
|
76
|
+
messages.push(parsed);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// skip malformed lines
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return messages;
|
|
84
|
+
}
|
|
85
|
+
function extractMemoriesFromSession(messages, sessionId, repoPath) {
|
|
86
|
+
const memories = [];
|
|
87
|
+
for (const msg of messages) {
|
|
88
|
+
if (msg.role !== "assistant")
|
|
89
|
+
continue;
|
|
90
|
+
const text = extractTextFromContent(msg.content);
|
|
91
|
+
if (!text)
|
|
92
|
+
continue;
|
|
93
|
+
const paragraphs = splitIntoParagraphs(text);
|
|
94
|
+
for (const para of paragraphs) {
|
|
95
|
+
const score = scoreText(para);
|
|
96
|
+
if (score < 2)
|
|
97
|
+
continue;
|
|
98
|
+
const isRejection = REJECTION_PATTERNS.some(p => p.test(para));
|
|
99
|
+
memories.push({
|
|
100
|
+
type: isRejection ? "note" : "decision",
|
|
101
|
+
title: buildTitle(para),
|
|
102
|
+
content: para,
|
|
103
|
+
tags: ["claude-session", isRejection ? "rejected-approach" : "ai-reasoning"],
|
|
104
|
+
repoPath,
|
|
105
|
+
status: "proposed",
|
|
106
|
+
confidence: Math.min(0.3 + score * 0.05, 0.6),
|
|
107
|
+
externalId: `claude-session:${sessionId}:${Buffer.from(para.slice(0, 40)).toString("base64").slice(0, 16)}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return memories;
|
|
112
|
+
}
|
|
113
|
+
// ── Command ───────────────────────────────────────────────────────────────────
|
|
114
|
+
async function scanClaudeCommand(options = {}) {
|
|
115
|
+
const repoRoot = resolveRepoRoot(options);
|
|
116
|
+
const sessionsDir = path_1.default.join(repoRoot, ".claude", "sessions");
|
|
117
|
+
if (!fs_1.default.existsSync(sessionsDir)) {
|
|
118
|
+
console.log(`No Claude Code sessions found at ${sessionsDir}`);
|
|
119
|
+
console.log("Claude Code sessions are created when you use Claude Code in this repo.");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const sessionFiles = fs_1.default.readdirSync(sessionsDir)
|
|
123
|
+
.filter(f => f.endsWith(".jsonl"))
|
|
124
|
+
.map(f => path_1.default.join(sessionsDir, f));
|
|
125
|
+
if (!sessionFiles.length) {
|
|
126
|
+
console.log("No session files found.");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const { initDb, saveMemory, getMemoryByExternalId } = (0, persistence_config_1.getPersistence)();
|
|
130
|
+
await initDb();
|
|
131
|
+
let totalSaved = 0;
|
|
132
|
+
let totalSkipped = 0;
|
|
133
|
+
for (const filePath of sessionFiles) {
|
|
134
|
+
const sessionId = path_1.default.basename(filePath, ".jsonl");
|
|
135
|
+
let messages;
|
|
136
|
+
try {
|
|
137
|
+
messages = parseSessionFile(filePath);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
console.warn(` Skipping unreadable session: ${sessionId}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const candidates = extractMemoriesFromSession(messages, sessionId, repoRoot);
|
|
144
|
+
for (const candidate of candidates) {
|
|
145
|
+
// Deduplicate by externalId
|
|
146
|
+
if (candidate.externalId) {
|
|
147
|
+
const existing = await getMemoryByExternalId(candidate.externalId, repoRoot);
|
|
148
|
+
if (existing) {
|
|
149
|
+
totalSkipped++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
await saveMemory(candidate);
|
|
154
|
+
totalSaved++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
console.log(`Claude session scan complete.`);
|
|
158
|
+
console.log(` Sessions scanned : ${sessionFiles.length}`);
|
|
159
|
+
console.log(` Memories saved : ${totalSaved}`);
|
|
160
|
+
console.log(` Already stored : ${totalSkipped}`);
|
|
161
|
+
}
|
|
162
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
163
|
+
function resolveRepoRoot(options) {
|
|
164
|
+
if (options.repo?.trim()) {
|
|
165
|
+
return findRepoRoot(path_1.default.resolve(options.repo.trim()));
|
|
166
|
+
}
|
|
167
|
+
return findRepoRoot(process.cwd());
|
|
168
|
+
}
|
|
169
|
+
function findRepoRoot(startPath) {
|
|
170
|
+
let current = startPath;
|
|
171
|
+
while (true) {
|
|
172
|
+
if (fs_1.default.existsSync(path_1.default.join(current, ".git")))
|
|
173
|
+
return current;
|
|
174
|
+
const parent = path_1.default.dirname(current);
|
|
175
|
+
if (parent === current)
|
|
176
|
+
return startPath;
|
|
177
|
+
current = parent;
|
|
178
|
+
}
|
|
179
|
+
}
|