@devtrace-ai/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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/analyzer.js +163 -0
- package/dist/capture/claude.js +219 -0
- package/dist/capture/trailers.js +112 -0
- package/dist/card.js +142 -0
- package/dist/chat.js +266 -0
- package/dist/claude-correlate.js +150 -0
- package/dist/claude-hook-install.js +183 -0
- package/dist/claude-hooks.js +236 -0
- package/dist/cli.js +113 -0
- package/dist/config.js +174 -0
- package/dist/core/scan.js +265 -0
- package/dist/core/survival.js +400 -0
- package/dist/detector.js +92 -0
- package/dist/digest.js +183 -0
- package/dist/git.js +212 -0
- package/dist/hooks.js +166 -0
- package/dist/html_report.js +325 -0
- package/dist/ignore.js +126 -0
- package/dist/index.js +798 -0
- package/dist/metrics.js +133 -0
- package/dist/reporter.js +127 -0
- package/dist/storage.js +344 -0
- package/dist/test_runner.js +143 -0
- package/dist/types.js +2 -0
- package/package.json +53 -0
package/dist/card.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildCardSvg = buildCardSvg;
|
|
37
|
+
exports.writeCardSvg = writeCardSvg;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const config_1 = require("./config");
|
|
41
|
+
const html_report_1 = require("./html_report");
|
|
42
|
+
const CARD_WIDTH = 800;
|
|
43
|
+
const CARD_HEIGHT = 418;
|
|
44
|
+
/** Truncates a display string to a max length, appending an ellipsis if cut. */
|
|
45
|
+
function truncate(input, maxLen) {
|
|
46
|
+
if (input.length <= maxLen)
|
|
47
|
+
return input;
|
|
48
|
+
return `${input.slice(0, Math.max(0, maxLen - 1))}…`;
|
|
49
|
+
}
|
|
50
|
+
function weightedSurvivalRate(sessions) {
|
|
51
|
+
let totalInsertions = 0;
|
|
52
|
+
let totalSurviving = 0;
|
|
53
|
+
let anyKnown = false;
|
|
54
|
+
for (const s of sessions) {
|
|
55
|
+
if (!s.churn || s.diff.totalInsertions <= 0)
|
|
56
|
+
continue;
|
|
57
|
+
anyKnown = true;
|
|
58
|
+
totalInsertions += s.diff.totalInsertions;
|
|
59
|
+
totalSurviving += s.diff.totalInsertions * s.churn.survivalRate;
|
|
60
|
+
}
|
|
61
|
+
if (!anyKnown || totalInsertions === 0)
|
|
62
|
+
return null;
|
|
63
|
+
return totalSurviving / totalInsertions;
|
|
64
|
+
}
|
|
65
|
+
function weightedSurvival30d(sessions) {
|
|
66
|
+
const measured = sessions
|
|
67
|
+
.map(s => s.churn?.survival30d)
|
|
68
|
+
.filter((v) => v !== undefined && v.status === 'measured');
|
|
69
|
+
if (measured.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
const rate = measured.reduce((sum, v) => sum + v.rate, 0) / measured.length;
|
|
72
|
+
return { rate, n: measured.length };
|
|
73
|
+
}
|
|
74
|
+
/** Builds the shareable SVG scorecard content (dark theme, ~800x418 social-card ratio). */
|
|
75
|
+
function buildCardSvg(sessions, projectDir) {
|
|
76
|
+
const repoName = truncate(path.basename(projectDir), 40);
|
|
77
|
+
const aiSessions = sessions.filter((s) => s.aiTool !== 'none');
|
|
78
|
+
const total = sessions.length;
|
|
79
|
+
const aiSharePct = total > 0 ? Math.round((aiSessions.length / total) * 100) : 0;
|
|
80
|
+
const headSurvival = weightedSurvivalRate(aiSessions);
|
|
81
|
+
const w30 = weightedSurvival30d(aiSessions);
|
|
82
|
+
let periodLabel = 'no commits recorded';
|
|
83
|
+
if (total > 0) {
|
|
84
|
+
const dates = sessions.map((s) => s.timestamp.substring(0, 10)).sort();
|
|
85
|
+
const first = dates[0];
|
|
86
|
+
const last = dates[dates.length - 1];
|
|
87
|
+
periodLabel = first === last ? first : `${first} → ${last}`;
|
|
88
|
+
}
|
|
89
|
+
const survivalHeadText = headSurvival === null ? 'unknown' : `${(headSurvival * 100).toFixed(0)}%`;
|
|
90
|
+
const survival30Text = w30 ? `${(w30.rate * 100).toFixed(0)}% (n=${w30.n})` : 'window open / unknown';
|
|
91
|
+
const esc = (s) => (0, html_report_1.escapeHtml)(s);
|
|
92
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${CARD_WIDTH}" height="${CARD_HEIGHT}" viewBox="0 0 ${CARD_WIDTH} ${CARD_HEIGHT}" role="img" aria-label="DevTrace scorecard for ${esc(repoName)}">
|
|
93
|
+
<defs>
|
|
94
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
95
|
+
<stop offset="0%" stop-color="#0f172a"/>
|
|
96
|
+
<stop offset="100%" stop-color="#1e1b4b"/>
|
|
97
|
+
</linearGradient>
|
|
98
|
+
</defs>
|
|
99
|
+
<rect width="${CARD_WIDTH}" height="${CARD_HEIGHT}" fill="url(#bg)"/>
|
|
100
|
+
<rect x="1" y="1" width="${CARD_WIDTH - 2}" height="${CARD_HEIGHT - 2}" fill="none" stroke="#334155" stroke-width="2" rx="16"/>
|
|
101
|
+
|
|
102
|
+
<text x="48" y="72" fill="#e2e8f0" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="30" font-weight="700">${esc(repoName)}</text>
|
|
103
|
+
<text x="48" y="100" fill="#94a3b8" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="16">${esc(periodLabel)}</text>
|
|
104
|
+
|
|
105
|
+
<g transform="translate(48, 140)">
|
|
106
|
+
<text x="0" y="0" fill="#64748b" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="13" letter-spacing="1">AI COMMIT SHARE</text>
|
|
107
|
+
<text x="0" y="52" fill="#22d3ee" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="48" font-weight="700">${aiSharePct}%</text>
|
|
108
|
+
<text x="0" y="76" fill="#64748b" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="13">${aiSessions.length} of ${total} commit(s)</text>
|
|
109
|
+
</g>
|
|
110
|
+
|
|
111
|
+
<g transform="translate(300, 140)">
|
|
112
|
+
<text x="0" y="0" fill="#64748b" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="13" letter-spacing="1">SURVIVAL (HEAD)</text>
|
|
113
|
+
<text x="0" y="52" fill="#a78bfa" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="48" font-weight="700">${esc(survivalHeadText)}</text>
|
|
114
|
+
<text x="0" y="76" fill="#64748b" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="13">AI-authored lines still present</text>
|
|
115
|
+
</g>
|
|
116
|
+
|
|
117
|
+
<g transform="translate(552, 140)">
|
|
118
|
+
<text x="0" y="0" fill="#64748b" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="13" letter-spacing="1">SURVIVAL (30D)</text>
|
|
119
|
+
<text x="0" y="52" fill="#34d399" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="30" font-weight="700">${esc(survival30Text)}</text>
|
|
120
|
+
<text x="0" y="76" fill="#64748b" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="13">windowed at commit+30d</text>
|
|
121
|
+
</g>
|
|
122
|
+
|
|
123
|
+
<line x1="48" y1="330" x2="${CARD_WIDTH - 48}" y2="330" stroke="#334155" stroke-width="1"/>
|
|
124
|
+
<text x="48" y="360" fill="#475569" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="13">measured from git history; heuristic detections are estimates</text>
|
|
125
|
+
<text x="48" y="384" fill="#334155" font-family="-apple-system, Helvetica, Arial, sans-serif" font-size="12">DevTrace</text>
|
|
126
|
+
</svg>
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
/** Writes the SVG scorecard to disk and returns the absolute path. */
|
|
130
|
+
function writeCardSvg(sessions, projectDir, outPath) {
|
|
131
|
+
const config = config_1.ConfigManager.loadConfig(projectDir);
|
|
132
|
+
const target = outPath
|
|
133
|
+
? path.resolve(projectDir, outPath)
|
|
134
|
+
: path.join(projectDir, config.storageDir, 'card.svg');
|
|
135
|
+
const dir = path.dirname(target);
|
|
136
|
+
if (!fs.existsSync(dir)) {
|
|
137
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
const svg = buildCardSvg(sessions, projectDir);
|
|
140
|
+
fs.writeFileSync(target, svg, 'utf8');
|
|
141
|
+
return target;
|
|
142
|
+
}
|
package/dist/chat.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ChatLogParser = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const claude_1 = require("./capture/claude");
|
|
42
|
+
class ChatLogParser {
|
|
43
|
+
/**
|
|
44
|
+
* Attempts to parse chat logs from a user-supplied file path.
|
|
45
|
+
*/
|
|
46
|
+
static parseUserFile(filePath) {
|
|
47
|
+
if (!fs.existsSync(filePath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
52
|
+
// Try JSON parsing
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(content);
|
|
55
|
+
// Standard Claude Code conversation JSON format
|
|
56
|
+
if (parsed.events && Array.isArray(parsed.events)) {
|
|
57
|
+
const conversation = [];
|
|
58
|
+
let promptText = '';
|
|
59
|
+
for (const event of parsed.events) {
|
|
60
|
+
if (event.type === 'user_message') {
|
|
61
|
+
const text = event.text || '';
|
|
62
|
+
if (!promptText)
|
|
63
|
+
promptText = text;
|
|
64
|
+
conversation.push({ role: 'user', text });
|
|
65
|
+
}
|
|
66
|
+
else if (event.type === 'assistant_message') {
|
|
67
|
+
conversation.push({ role: 'assistant', text: event.text || '' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { promptText, conversation };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Fall back to treating it as plain text prompt
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
promptText: content,
|
|
78
|
+
conversation: [{ role: 'user', text: content }]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.warn(`[DevTrace] Failed to parse user chat log: ${error.message}`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Automatically attempts to parse the latest Claude Code session transcript
|
|
88
|
+
* for the current repo. Delegates to ClaudeCaptureSource (src/capture/claude.ts),
|
|
89
|
+
* which reads the real transcript location: ~/.claude/projects/<slugified-cwd>/*.jsonl,
|
|
90
|
+
* correlated to `cwd` so unrelated projects' sessions never leak in here.
|
|
91
|
+
*
|
|
92
|
+
* `cwd` defaults to process.cwd() to preserve the previous call signature
|
|
93
|
+
* (index.ts calls this with no arguments).
|
|
94
|
+
*/
|
|
95
|
+
static parseLatestClaudeCode(cwd = process.cwd()) {
|
|
96
|
+
try {
|
|
97
|
+
const source = new claude_1.ClaudeCaptureSource();
|
|
98
|
+
const result = source.getLatestPrompt(cwd);
|
|
99
|
+
if (!result)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
promptText: result.promptText,
|
|
103
|
+
conversation: [{ role: 'user', text: result.promptText }]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.warn(`[DevTrace] Failed to parse Claude Code chat logs: ${error.message}`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Attempts to query Cursor's state database for recent chat panels using native sqlite3 client.
|
|
113
|
+
* If sqlite3 is not installed, falls back to raw binary/string scanning.
|
|
114
|
+
* Cross-platform compatible (handles macOS, Windows, Linux paths).
|
|
115
|
+
*/
|
|
116
|
+
static parseLatestCursorChat(homeDir = process.env.HOME || process.env.USERPROFILE || '') {
|
|
117
|
+
if (!homeDir)
|
|
118
|
+
return null;
|
|
119
|
+
let dbPath = '';
|
|
120
|
+
if (process.platform === 'darwin') {
|
|
121
|
+
dbPath = path.join(homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
122
|
+
}
|
|
123
|
+
else if (process.platform === 'win32') {
|
|
124
|
+
dbPath = path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
dbPath = path.join(homeDir, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
128
|
+
}
|
|
129
|
+
if (!fs.existsSync(dbPath)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const uniqueId = `temp_cursor_state_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
133
|
+
const tempDbPath = path.join(os.tmpdir(), `${uniqueId}.vscdb`);
|
|
134
|
+
const tempWalPath = `${tempDbPath}-wal`;
|
|
135
|
+
const tempShmPath = `${tempDbPath}-shm`;
|
|
136
|
+
let chatState = null;
|
|
137
|
+
try {
|
|
138
|
+
// 1. Snapshot the database. Try executing sqlite3 CLI tool backup command first for atomic consistency.
|
|
139
|
+
let snapshotSuccess = false;
|
|
140
|
+
try {
|
|
141
|
+
const backupRes = (0, child_process_1.spawnSync)('sqlite3', [dbPath, `.backup '${tempDbPath}'`]);
|
|
142
|
+
if (backupRes.status === 0 && fs.existsSync(tempDbPath)) {
|
|
143
|
+
snapshotSuccess = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch { }
|
|
147
|
+
if (!snapshotSuccess) {
|
|
148
|
+
// Fallback: Copy the main database file directly (ignoring WAL/SHM sidecars to prevent lock/SHM issues)
|
|
149
|
+
fs.copyFileSync(dbPath, tempDbPath);
|
|
150
|
+
}
|
|
151
|
+
// 2. Try executing sqlite3 CLI tool first
|
|
152
|
+
const query = "SELECT value FROM ItemTable WHERE key = 'workbench.panel.chatSidebar';";
|
|
153
|
+
const result = (0, child_process_1.spawnSync)('sqlite3', [tempDbPath, query], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
154
|
+
const resultRaw = result.status === 0 ? result.stdout.trim() : '';
|
|
155
|
+
if (resultRaw) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(resultRaw);
|
|
158
|
+
if (parsed && parsed.chatState) {
|
|
159
|
+
chatState = parsed.chatState;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
}
|
|
164
|
+
// 3. Fallback: Parse binary/string if sqlite3 is missing or failed
|
|
165
|
+
if (!chatState) {
|
|
166
|
+
const binaryContent = fs.readFileSync(tempDbPath, 'latin1');
|
|
167
|
+
chatState = this.extractChatStateFromBinary(binaryContent);
|
|
168
|
+
}
|
|
169
|
+
if (chatState && Array.isArray(chatState.chats)) {
|
|
170
|
+
const chats = chatState.chats;
|
|
171
|
+
if (chats.length === 0)
|
|
172
|
+
return null;
|
|
173
|
+
const latestChat = chats[chats.length - 1];
|
|
174
|
+
if (latestChat && Array.isArray(latestChat.bubbles)) {
|
|
175
|
+
const conversation = [];
|
|
176
|
+
let promptText = '';
|
|
177
|
+
for (const bubble of latestChat.bubbles) {
|
|
178
|
+
const role = bubble.type === 'user' ? 'user' : 'assistant';
|
|
179
|
+
const text = bubble.text || '';
|
|
180
|
+
if (role === 'user' && !promptText)
|
|
181
|
+
promptText = text;
|
|
182
|
+
conversation.push({ role, text });
|
|
183
|
+
}
|
|
184
|
+
return { promptText, conversation };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
// Silent catch to prevent git hooks blocking
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
// Cleanup copied temp files
|
|
193
|
+
const toDelete = [tempDbPath, tempWalPath, tempShmPath];
|
|
194
|
+
for (const f of toDelete) {
|
|
195
|
+
if (fs.existsSync(f)) {
|
|
196
|
+
try {
|
|
197
|
+
fs.unlinkSync(f);
|
|
198
|
+
}
|
|
199
|
+
catch { }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Scans a string (loaded with 'latin1' encoding) for JSON chatState structure.
|
|
207
|
+
* Useful when sqlite3 CLI is not available on user's system.
|
|
208
|
+
*/
|
|
209
|
+
static extractChatStateFromBinary(content) {
|
|
210
|
+
const markers = ['{"chatState"', '{"chats"'];
|
|
211
|
+
for (const marker of markers) {
|
|
212
|
+
let searchStart = 0;
|
|
213
|
+
while (true) {
|
|
214
|
+
const matchIdx = content.indexOf(marker, searchStart);
|
|
215
|
+
if (matchIdx === -1)
|
|
216
|
+
break;
|
|
217
|
+
let braceCount = 0;
|
|
218
|
+
let inString = false;
|
|
219
|
+
let escaped = false;
|
|
220
|
+
let endIdx = -1;
|
|
221
|
+
for (let i = matchIdx; i < content.length; i++) {
|
|
222
|
+
const char = content[i];
|
|
223
|
+
if (escaped) {
|
|
224
|
+
escaped = false;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (char === '\\') {
|
|
228
|
+
escaped = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (char === '"') {
|
|
232
|
+
inString = !inString;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!inString) {
|
|
236
|
+
if (char === '{') {
|
|
237
|
+
braceCount++;
|
|
238
|
+
}
|
|
239
|
+
else if (char === '}') {
|
|
240
|
+
braceCount--;
|
|
241
|
+
if (braceCount === 0) {
|
|
242
|
+
endIdx = i;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (endIdx !== -1) {
|
|
249
|
+
const jsonStr = content.substring(matchIdx, endIdx + 1);
|
|
250
|
+
try {
|
|
251
|
+
const parsed = JSON.parse(jsonStr);
|
|
252
|
+
if (parsed && (parsed.chatState || parsed.chats)) {
|
|
253
|
+
return parsed.chatState || parsed;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Keep scanning if JSON parse failed
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
searchStart = matchIdx + 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
exports.ChatLogParser = ChatLogParser;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TIER2_CONFIDENCE = void 0;
|
|
37
|
+
exports.correlateCommitWithLedger = correlateCommitWithLedger;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const claude_hooks_1 = require("./claude-hooks");
|
|
41
|
+
/**
|
|
42
|
+
* Correlates a landed git commit against the Claude Code hook ledger
|
|
43
|
+
* (.devtrace/claude-ledger.jsonl) to upgrade detection to tier 2
|
|
44
|
+
* ("instrumented", tool claude-code, confidence 0.85) — but only when no
|
|
45
|
+
* tier-1 trailer already matched (trailers stay authoritative; see
|
|
46
|
+
* src/detector.ts). Ledger events are watermarked by timestamp so a given
|
|
47
|
+
* edit event is never counted toward more than one commit.
|
|
48
|
+
*
|
|
49
|
+
* See docs/plans/03b-sota-capture-research-completion.md Gap 1 / Gap 4
|
|
50
|
+
* (tier scheme) and plan 04 §2.4 (T1.4).
|
|
51
|
+
*/
|
|
52
|
+
exports.TIER2_CONFIDENCE = 0.85;
|
|
53
|
+
const WATERMARK_FILENAME = 'claude-ledger.watermark';
|
|
54
|
+
function getLedgerPath(cwd) {
|
|
55
|
+
return path.join(cwd, '.devtrace', claude_hooks_1.CLAUDE_LEDGER_FILENAME);
|
|
56
|
+
}
|
|
57
|
+
function getWatermarkPath(cwd) {
|
|
58
|
+
return path.join(cwd, '.devtrace', WATERMARK_FILENAME);
|
|
59
|
+
}
|
|
60
|
+
function readWatermark(cwd) {
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(getWatermarkPath(cwd), 'utf8').trim();
|
|
63
|
+
const ts = Date.parse(raw);
|
|
64
|
+
return Number.isNaN(ts) ? 0 : ts;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function writeWatermark(cwd, isoTs) {
|
|
71
|
+
try {
|
|
72
|
+
fs.writeFileSync(getWatermarkPath(cwd), isoTs, 'utf8');
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Best-effort — a failed watermark write only risks double-counting a future
|
|
76
|
+
// commit's correlation, not data loss or a crash.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Reads and parses all ledger lines, tolerating malformed lines. */
|
|
80
|
+
function readLedgerEvents(cwd) {
|
|
81
|
+
const ledgerPath = getLedgerPath(cwd);
|
|
82
|
+
if (!fs.existsSync(ledgerPath))
|
|
83
|
+
return [];
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = fs.readFileSync(ledgerPath, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const events = [];
|
|
92
|
+
for (const line of raw.split('\n')) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed)
|
|
95
|
+
continue;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(trimmed);
|
|
98
|
+
if (parsed && typeof parsed === 'object' && (parsed.kind === 'edit' || parsed.kind === 'session-start')) {
|
|
99
|
+
events.push(parsed);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Skip malformed lines rather than aborting the whole file.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return events;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Determines whether the given commit's changed files were touched by
|
|
110
|
+
* unconsumed Claude Code hook ledger events (i.e. events with ts newer than
|
|
111
|
+
* the last-consumed watermark). Advances the watermark to the newest event
|
|
112
|
+
* timestamp seen in this call, whether or not it matched a file, so events
|
|
113
|
+
* are never double-counted across commits.
|
|
114
|
+
*
|
|
115
|
+
* `changedFiles` must be repo-relative paths (same convention the ledger
|
|
116
|
+
* uses — see resolveRelativeFilePath in src/claude-hooks.ts).
|
|
117
|
+
*/
|
|
118
|
+
function correlateCommitWithLedger(cwd, changedFiles) {
|
|
119
|
+
const events = readLedgerEvents(cwd);
|
|
120
|
+
if (events.length === 0) {
|
|
121
|
+
return { matched: false, matchedFiles: [], sessionIds: [] };
|
|
122
|
+
}
|
|
123
|
+
const watermark = readWatermark(cwd);
|
|
124
|
+
const changedSet = new Set(changedFiles);
|
|
125
|
+
let newestTs = watermark;
|
|
126
|
+
const matchedFiles = new Set();
|
|
127
|
+
const sessionIds = new Set();
|
|
128
|
+
for (const event of events) {
|
|
129
|
+
const ts = Date.parse(event.ts);
|
|
130
|
+
if (Number.isNaN(ts) || ts <= watermark)
|
|
131
|
+
continue;
|
|
132
|
+
if (ts > newestTs)
|
|
133
|
+
newestTs = ts;
|
|
134
|
+
if (event.kind === 'edit') {
|
|
135
|
+
const edit = event;
|
|
136
|
+
if (changedSet.has(edit.filePath)) {
|
|
137
|
+
matchedFiles.add(edit.filePath);
|
|
138
|
+
sessionIds.add(edit.sessionId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (newestTs > watermark) {
|
|
143
|
+
writeWatermark(cwd, new Date(newestTs).toISOString());
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
matched: matchedFiles.size > 0,
|
|
147
|
+
matchedFiles: Array.from(matchedFiles),
|
|
148
|
+
sessionIds: Array.from(sessionIds)
|
|
149
|
+
};
|
|
150
|
+
}
|