@ulpi/cli 0.1.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 +200 -0
- package/dist/auth-PN7TMQHV-2W4ICG64.js +15 -0
- package/dist/chunk-247GVVKK.js +2259 -0
- package/dist/chunk-2CLNOKPA.js +793 -0
- package/dist/chunk-2HEE5OKX.js +79 -0
- package/dist/chunk-2MZER6ND.js +415 -0
- package/dist/chunk-3SBPZRB5.js +772 -0
- package/dist/chunk-4VNS5WPM.js +42 -0
- package/dist/chunk-6JCMYYBT.js +1546 -0
- package/dist/chunk-6OCEY7JY.js +422 -0
- package/dist/chunk-74WVVWJ4.js +375 -0
- package/dist/chunk-7AL4DOEJ.js +131 -0
- package/dist/chunk-7LXY5UVC.js +330 -0
- package/dist/chunk-DBMUNBNB.js +3048 -0
- package/dist/chunk-JWUUVXIV.js +13694 -0
- package/dist/chunk-KIKPIH6N.js +4048 -0
- package/dist/chunk-KLEASXUR.js +70 -0
- package/dist/chunk-MIAQVCFW.js +39 -0
- package/dist/chunk-NNUWU6CV.js +1610 -0
- package/dist/chunk-PKD4ASEM.js +115 -0
- package/dist/chunk-Q4HIY43N.js +4230 -0
- package/dist/chunk-QJ5GSMEC.js +146 -0
- package/dist/chunk-SIAQVRKG.js +2163 -0
- package/dist/chunk-SPOI23SB.js +197 -0
- package/dist/chunk-YM2HV4IA.js +505 -0
- package/dist/codemap-RRJIDBQ5.js +636 -0
- package/dist/config-EGAXXCGL.js +127 -0
- package/dist/dist-6G7JC2RA.js +90 -0
- package/dist/dist-7LHZ65GC.js +418 -0
- package/dist/dist-LZKZFPVX.js +140 -0
- package/dist/dist-R5F4MX3I.js +107 -0
- package/dist/dist-R5ZJ4LX5.js +56 -0
- package/dist/dist-RJGCUS3L.js +87 -0
- package/dist/dist-RKOGLK7R.js +151 -0
- package/dist/dist-W7K4WPAF.js +597 -0
- package/dist/export-import-4A5MWLIA.js +53 -0
- package/dist/history-ATTUKOHO.js +934 -0
- package/dist/index.js +2120 -0
- package/dist/init-AY5C2ZAS.js +393 -0
- package/dist/launchd-LF2QMSKZ.js +148 -0
- package/dist/log-TVTUXAYD.js +75 -0
- package/dist/mcp-installer-NQCGKQ23.js +124 -0
- package/dist/memory-J3G24QHS.js +406 -0
- package/dist/ollama-3XCUZMZT-FYKHW4TZ.js +7 -0
- package/dist/openai-E7G2YAHU-UYY4ZWON.js +8 -0
- package/dist/projects-ATHDD3D6.js +271 -0
- package/dist/review-ADUPV3PN.js +152 -0
- package/dist/rules-E427DKYJ.js +134 -0
- package/dist/server-MOYPE4SM-N7SE2AN7.js +18 -0
- package/dist/server-X5P6WH2M-7K2RY34N.js +11 -0
- package/dist/skills/ulpi-generate-guardian/SKILL.md +511 -0
- package/dist/skills/ulpi-generate-guardian/references/framework-rules.md +692 -0
- package/dist/skills/ulpi-generate-guardian/references/language-rules.md +596 -0
- package/dist/skills-CX73O3IV.js +76 -0
- package/dist/status-4DFHDJMN.js +66 -0
- package/dist/templates/biome.yml +24 -0
- package/dist/templates/conventional-commits.yml +18 -0
- package/dist/templates/django.yml +30 -0
- package/dist/templates/docker.yml +30 -0
- package/dist/templates/eslint.yml +13 -0
- package/dist/templates/express.yml +20 -0
- package/dist/templates/fastapi.yml +23 -0
- package/dist/templates/git-flow.yml +26 -0
- package/dist/templates/github-flow.yml +27 -0
- package/dist/templates/go.yml +33 -0
- package/dist/templates/jest.yml +24 -0
- package/dist/templates/laravel.yml +30 -0
- package/dist/templates/monorepo.yml +26 -0
- package/dist/templates/nestjs.yml +21 -0
- package/dist/templates/nextjs.yml +31 -0
- package/dist/templates/nodejs.yml +33 -0
- package/dist/templates/npm.yml +15 -0
- package/dist/templates/php.yml +25 -0
- package/dist/templates/pnpm.yml +15 -0
- package/dist/templates/prettier.yml +23 -0
- package/dist/templates/prisma.yml +21 -0
- package/dist/templates/python.yml +33 -0
- package/dist/templates/quality-of-life.yml +111 -0
- package/dist/templates/ruby.yml +25 -0
- package/dist/templates/rust.yml +34 -0
- package/dist/templates/typescript.yml +14 -0
- package/dist/templates/vitest.yml +24 -0
- package/dist/templates/yarn.yml +15 -0
- package/dist/templates-U7T6MARD.js +156 -0
- package/dist/ui-L7UAWXDY.js +167 -0
- package/dist/ui.html +698 -0
- package/dist/ulpi-RMMCUAGP-JCJ273T6.js +161 -0
- package/dist/uninstall-6SW35IK4.js +25 -0
- package/dist/update-M2B4RLGH.js +61 -0
- package/dist/version-checker-ANCS3IHR.js +10 -0
- package/package.json +92 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_HISTORY_CONFIG,
|
|
3
|
+
buildSessionSummary,
|
|
4
|
+
entryExists,
|
|
5
|
+
findReviewPlansForCommit,
|
|
6
|
+
findSessionForCommit,
|
|
7
|
+
getCommitDiffStats,
|
|
8
|
+
getCommitMetadata,
|
|
9
|
+
getCommitRawDiff,
|
|
10
|
+
getCurrentHead,
|
|
11
|
+
historyBranchExists,
|
|
12
|
+
initHistoryBranch,
|
|
13
|
+
listRecentCommits,
|
|
14
|
+
loadActiveGuards,
|
|
15
|
+
readBranchMeta,
|
|
16
|
+
readEntryRawData,
|
|
17
|
+
readHistoryEntry,
|
|
18
|
+
readTimeline,
|
|
19
|
+
updateEntryEnrichment,
|
|
20
|
+
writeHistoryEntry
|
|
21
|
+
} from "./chunk-NNUWU6CV.js";
|
|
22
|
+
import "./chunk-YM2HV4IA.js";
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_AI_MODEL,
|
|
25
|
+
REVIEWS_DIR,
|
|
26
|
+
getHistoryBranch
|
|
27
|
+
} from "./chunk-7LXY5UVC.js";
|
|
28
|
+
import "./chunk-4VNS5WPM.js";
|
|
29
|
+
|
|
30
|
+
// src/commands/history.ts
|
|
31
|
+
import chalk from "chalk";
|
|
32
|
+
import * as path2 from "path";
|
|
33
|
+
import * as fs2 from "fs";
|
|
34
|
+
import * as readline from "readline";
|
|
35
|
+
|
|
36
|
+
// src/history/enricher.ts
|
|
37
|
+
import { execFileSync, spawn } from "child_process";
|
|
38
|
+
import * as fs from "fs";
|
|
39
|
+
import * as os from "os";
|
|
40
|
+
import * as path from "path";
|
|
41
|
+
function resolveClaudePath() {
|
|
42
|
+
try {
|
|
43
|
+
const result = execFileSync("which", ["claude"], { stdio: "pipe", timeout: 3e3 }).toString().trim();
|
|
44
|
+
if (result) return result;
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
const home = os.homedir();
|
|
48
|
+
const candidates = [
|
|
49
|
+
path.join(home, ".local", "bin", "claude"),
|
|
50
|
+
path.join(home, ".claude", "bin", "claude"),
|
|
51
|
+
"/usr/local/bin/claude"
|
|
52
|
+
];
|
|
53
|
+
for (const p of candidates) {
|
|
54
|
+
try {
|
|
55
|
+
if (fs.existsSync(p)) return p;
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
var cachedClaudePath;
|
|
62
|
+
function getClaudePath() {
|
|
63
|
+
if (cachedClaudePath === void 0) {
|
|
64
|
+
cachedClaudePath = resolveClaudePath();
|
|
65
|
+
}
|
|
66
|
+
return cachedClaudePath;
|
|
67
|
+
}
|
|
68
|
+
function isClaudeCliAvailable() {
|
|
69
|
+
const p = getClaudePath();
|
|
70
|
+
if (!p) return false;
|
|
71
|
+
try {
|
|
72
|
+
execFileSync(p, ["--version"], { stdio: "pipe", timeout: 5e3 });
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function computeAntiPatternMetrics(events) {
|
|
79
|
+
const rawFileEdits = {};
|
|
80
|
+
for (const ev of events) {
|
|
81
|
+
if ((ev.toolName === "Write" || ev.toolName === "Edit") && ev.filePath) {
|
|
82
|
+
rawFileEdits[ev.filePath] = (rawFileEdits[ev.filePath] ?? 0) + 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const fileEditCounts = {};
|
|
86
|
+
for (const [file, count] of Object.entries(rawFileEdits)) {
|
|
87
|
+
if (count >= 3) fileEditCounts[file] = count;
|
|
88
|
+
}
|
|
89
|
+
const postcondFailRates = {};
|
|
90
|
+
for (const ev of events) {
|
|
91
|
+
if (ev.event === "postcondition_run" && ev.command) {
|
|
92
|
+
if (!postcondFailRates[ev.command]) {
|
|
93
|
+
postcondFailRates[ev.command] = { success: 0, fail: 0 };
|
|
94
|
+
}
|
|
95
|
+
postcondFailRates[ev.command].success++;
|
|
96
|
+
}
|
|
97
|
+
if (ev.event === "postcondition_failed" && ev.command) {
|
|
98
|
+
if (!postcondFailRates[ev.command]) {
|
|
99
|
+
postcondFailRates[ev.command] = { success: 0, fail: 0 };
|
|
100
|
+
}
|
|
101
|
+
postcondFailRates[ev.command].fail++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const blockRetrySequences = [];
|
|
105
|
+
let currentBlock = null;
|
|
106
|
+
for (const ev of events) {
|
|
107
|
+
if (ev.event === "tool_blocked" && ev.toolName) {
|
|
108
|
+
const key = `${ev.toolName}|${ev.filePath ?? ""}|${ev.ruleName ?? ""}`;
|
|
109
|
+
if (currentBlock && `${currentBlock.tool}|${currentBlock.file}|${currentBlock.rule}` === key) {
|
|
110
|
+
currentBlock.count++;
|
|
111
|
+
} else {
|
|
112
|
+
if (currentBlock && currentBlock.count >= 2) {
|
|
113
|
+
blockRetrySequences.push({ ...currentBlock });
|
|
114
|
+
}
|
|
115
|
+
currentBlock = {
|
|
116
|
+
tool: ev.toolName,
|
|
117
|
+
file: ev.filePath ?? "",
|
|
118
|
+
rule: ev.ruleName ?? "",
|
|
119
|
+
count: 1
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
if (currentBlock && currentBlock.count >= 2) {
|
|
124
|
+
blockRetrySequences.push({ ...currentBlock });
|
|
125
|
+
}
|
|
126
|
+
currentBlock = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (currentBlock && currentBlock.count >= 2) {
|
|
130
|
+
blockRetrySequences.push({ ...currentBlock });
|
|
131
|
+
}
|
|
132
|
+
const rawCmdCounts = {};
|
|
133
|
+
for (const ev of events) {
|
|
134
|
+
if (ev.toolName === "Bash" && ev.command) {
|
|
135
|
+
rawCmdCounts[ev.command] = (rawCmdCounts[ev.command] ?? 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const commandRepeatCounts = {};
|
|
139
|
+
for (const [cmd, count] of Object.entries(rawCmdCounts)) {
|
|
140
|
+
if (count >= 5) commandRepeatCounts[cmd] = count;
|
|
141
|
+
}
|
|
142
|
+
return { fileEditCounts, postcondFailRates, blockRetrySequences, commandRepeatCounts };
|
|
143
|
+
}
|
|
144
|
+
function buildEnrichmentPrompt(entry, rawEvents) {
|
|
145
|
+
const sections = [];
|
|
146
|
+
sections.push("You are analyzing a commit captured by ULPI, a rules engine for AI coding agents.");
|
|
147
|
+
sections.push("Fill in the structured output fields:");
|
|
148
|
+
sections.push("- summary: A concise 1-2 sentence description of what this commit accomplishes.");
|
|
149
|
+
sections.push("- intent: The high-level goal or motivation behind the changes.");
|
|
150
|
+
sections.push("- challenges: Specific difficulties encountered during this session (e.g. test failures, blocked tools, repeated edits). Be concrete, not vague.");
|
|
151
|
+
sections.push("- ruleInsights: Observations about which guardian rules helped, which were too strict, or what rules could be added. Reference specific rule names when possible.");
|
|
152
|
+
sections.push("");
|
|
153
|
+
sections.push("## Commit");
|
|
154
|
+
sections.push(`SHA: ${entry.commit.shortSha}`);
|
|
155
|
+
sections.push(`Subject: ${entry.commit.subject}`);
|
|
156
|
+
sections.push(`Author: ${entry.commit.authorName}`);
|
|
157
|
+
sections.push(`Date: ${entry.commit.authorDate}`);
|
|
158
|
+
sections.push(`Branch: ${entry.commit.branch}`);
|
|
159
|
+
if (entry.commit.message !== entry.commit.subject) {
|
|
160
|
+
sections.push(`Message:
|
|
161
|
+
${entry.commit.message}`);
|
|
162
|
+
}
|
|
163
|
+
sections.push("");
|
|
164
|
+
sections.push("## Diff Stats");
|
|
165
|
+
sections.push(`Files changed: ${entry.diff.filesChanged}`);
|
|
166
|
+
sections.push(`Insertions: +${entry.diff.insertions}`);
|
|
167
|
+
sections.push(`Deletions: -${entry.diff.deletions}`);
|
|
168
|
+
if (entry.diff.files.length > 0) {
|
|
169
|
+
sections.push("Files:");
|
|
170
|
+
for (const f of entry.diff.files.slice(0, 30)) {
|
|
171
|
+
sections.push(` ${f.status} ${f.path} (+${f.additions}/-${f.deletions})`);
|
|
172
|
+
}
|
|
173
|
+
if (entry.diff.files.length > 30) {
|
|
174
|
+
sections.push(` ... and ${entry.diff.files.length - 30} more files`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
sections.push("");
|
|
178
|
+
if (entry.session) {
|
|
179
|
+
const s = entry.session;
|
|
180
|
+
sections.push("## Session Summary");
|
|
181
|
+
sections.push(`Session: ${s.sessionId}${s.sessionName ? ` (${s.sessionName})` : ""}`);
|
|
182
|
+
sections.push(`Total events: ${s.totalEvents}`);
|
|
183
|
+
sections.push(`Rules enforced: ${s.stats.rulesEnforced}`);
|
|
184
|
+
sections.push(`Actions blocked: ${s.stats.actionsBlocked}`);
|
|
185
|
+
sections.push(`Auto-approved: ${s.stats.autoActionsRun}`);
|
|
186
|
+
sections.push(`Files read: ${s.stats.filesRead}, written: ${s.stats.filesWritten}, deleted: ${s.stats.filesDeleted}`);
|
|
187
|
+
sections.push(`Commands run: ${s.stats.commandsRun}`);
|
|
188
|
+
sections.push(`Tests: ${s.stats.testsRun ? s.stats.testsPassed ? "passed" : "failed/unknown" : "not run"}`);
|
|
189
|
+
sections.push(`Lint: ${s.stats.lintRun ? "run" : "not run"}, Build: ${s.stats.buildRun ? "run" : "not run"}`);
|
|
190
|
+
if (s.blockedTools.length > 0) {
|
|
191
|
+
sections.push(`Blocked tools: ${s.blockedTools.join(", ")}`);
|
|
192
|
+
}
|
|
193
|
+
if (s.preconditionsFired.length > 0) {
|
|
194
|
+
sections.push(`Preconditions fired: ${s.preconditionsFired.join(", ")}`);
|
|
195
|
+
}
|
|
196
|
+
if (s.postconditionsRun.length > 0) {
|
|
197
|
+
sections.push(`Postconditions run: ${s.postconditionsRun.join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
if (s.skillsInjected.length > 0) {
|
|
200
|
+
sections.push(`Skills injected: ${s.skillsInjected.join(", ")}`);
|
|
201
|
+
}
|
|
202
|
+
sections.push("");
|
|
203
|
+
}
|
|
204
|
+
if (rawEvents && rawEvents.length > 0) {
|
|
205
|
+
const metrics = computeAntiPatternMetrics(rawEvents);
|
|
206
|
+
const hasMetrics = Object.keys(metrics.fileEditCounts).length > 0 || Object.keys(metrics.postcondFailRates).length > 0 || metrics.blockRetrySequences.length > 0 || Object.keys(metrics.commandRepeatCounts).length > 0;
|
|
207
|
+
if (hasMetrics) {
|
|
208
|
+
sections.push("## Anti-Pattern Metrics");
|
|
209
|
+
if (Object.keys(metrics.fileEditCounts).length > 0) {
|
|
210
|
+
sections.push("Files edited 3+ times (possible churn):");
|
|
211
|
+
for (const [file, count] of Object.entries(metrics.fileEditCounts)) {
|
|
212
|
+
sections.push(` ${file}: ${count} edits`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (metrics.blockRetrySequences.length > 0) {
|
|
216
|
+
sections.push("Block-retry sequences (tool blocked then retried):");
|
|
217
|
+
for (const seq of metrics.blockRetrySequences) {
|
|
218
|
+
sections.push(` ${seq.tool}${seq.file ? ` on ${seq.file}` : ""}${seq.rule ? ` by ${seq.rule}` : ""}: ${seq.count}x`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const failedPostconds = Object.entries(metrics.postcondFailRates).filter(
|
|
222
|
+
([, v]) => v.fail > 0
|
|
223
|
+
);
|
|
224
|
+
if (failedPostconds.length > 0) {
|
|
225
|
+
sections.push("Postcondition failure rates:");
|
|
226
|
+
for (const [cmd, rates] of failedPostconds) {
|
|
227
|
+
sections.push(` ${cmd}: ${rates.success} pass, ${rates.fail} fail`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (Object.keys(metrics.commandRepeatCounts).length > 0) {
|
|
231
|
+
sections.push("Commands run 5+ times:");
|
|
232
|
+
for (const [cmd, count] of Object.entries(metrics.commandRepeatCounts)) {
|
|
233
|
+
sections.push(` ${cmd}: ${count}x`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
sections.push("");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (entry.rawDiff) {
|
|
240
|
+
const maxAiDiff = 1e4;
|
|
241
|
+
const diffExcerpt = entry.rawDiff.length > maxAiDiff ? entry.rawDiff.slice(0, maxAiDiff) + "\n... (truncated)" : entry.rawDiff;
|
|
242
|
+
sections.push("## Raw Diff (excerpt)");
|
|
243
|
+
sections.push(diffExcerpt);
|
|
244
|
+
sections.push("");
|
|
245
|
+
}
|
|
246
|
+
sections.push("Fill in all fields based on the data above. Be specific and concrete.");
|
|
247
|
+
return sections.join("\n");
|
|
248
|
+
}
|
|
249
|
+
function toAiEnrichment(parsed, model, generatedAt) {
|
|
250
|
+
return {
|
|
251
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
252
|
+
intent: typeof parsed.intent === "string" ? parsed.intent : void 0,
|
|
253
|
+
challenges: Array.isArray(parsed.challenges) ? parsed.challenges : void 0,
|
|
254
|
+
ruleInsights: Array.isArray(parsed.ruleInsights) ? parsed.ruleInsights : void 0,
|
|
255
|
+
model,
|
|
256
|
+
generatedAt
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function parseEnrichmentResponse(response, model) {
|
|
260
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
261
|
+
try {
|
|
262
|
+
const envelope = JSON.parse(response.trim());
|
|
263
|
+
if (envelope.structured_output && typeof envelope.structured_output === "object") {
|
|
264
|
+
return toAiEnrichment(envelope.structured_output, model, generatedAt);
|
|
265
|
+
}
|
|
266
|
+
if (typeof envelope.result === "string" && envelope.result.trim().startsWith("{")) {
|
|
267
|
+
const parsed = JSON.parse(envelope.result);
|
|
268
|
+
return toAiEnrichment(parsed, model, generatedAt);
|
|
269
|
+
}
|
|
270
|
+
if (typeof envelope.summary === "string") {
|
|
271
|
+
return toAiEnrichment(envelope, model, generatedAt);
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
const jsonMatch = response.match(/```json\s*\n?([\s\S]*?)\n?\s*```/);
|
|
276
|
+
if (jsonMatch) {
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
279
|
+
return toAiEnrichment(parsed, model, generatedAt);
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(response.trim());
|
|
285
|
+
return toAiEnrichment(parsed, model, generatedAt);
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
summary: response.trim().slice(0, 500),
|
|
290
|
+
model,
|
|
291
|
+
generatedAt
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
var ENRICHMENT_SCHEMA = JSON.stringify({
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
summary: { type: "string", description: "Concise 1-2 sentence description of what this commit accomplishes" },
|
|
298
|
+
intent: { type: "string", description: "High-level goal or motivation behind the changes" },
|
|
299
|
+
challenges: {
|
|
300
|
+
type: "array",
|
|
301
|
+
items: { type: "string" },
|
|
302
|
+
description: "Specific difficulties encountered (test failures, blocked tools, repeated edits)"
|
|
303
|
+
},
|
|
304
|
+
ruleInsights: {
|
|
305
|
+
type: "array",
|
|
306
|
+
items: { type: "string" },
|
|
307
|
+
description: "Observations about which guardian rules helped, which were too strict, or what rules could be added"
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
required: ["summary"]
|
|
311
|
+
});
|
|
312
|
+
function enrichEntry(entry, rawEvents, model = DEFAULT_AI_MODEL) {
|
|
313
|
+
const prompt = buildEnrichmentPrompt(entry, rawEvents);
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const timeout = 2 * 60 * 1e3;
|
|
316
|
+
const claudePath = getClaudePath();
|
|
317
|
+
if (!claudePath) {
|
|
318
|
+
reject(new Error("Claude CLI not found"));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const proc = spawn(claudePath, [
|
|
322
|
+
"--print",
|
|
323
|
+
"--model",
|
|
324
|
+
model,
|
|
325
|
+
"--output-format",
|
|
326
|
+
"json",
|
|
327
|
+
"--json-schema",
|
|
328
|
+
ENRICHMENT_SCHEMA,
|
|
329
|
+
"--permission-mode",
|
|
330
|
+
"bypassPermissions"
|
|
331
|
+
], {
|
|
332
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
333
|
+
});
|
|
334
|
+
let stdout = "";
|
|
335
|
+
let stderr = "";
|
|
336
|
+
proc.stdout.on("data", (data) => {
|
|
337
|
+
stdout += data.toString();
|
|
338
|
+
});
|
|
339
|
+
proc.stderr.on("data", (data) => {
|
|
340
|
+
stderr += data.toString();
|
|
341
|
+
});
|
|
342
|
+
proc.stdin.write(prompt);
|
|
343
|
+
proc.stdin.end();
|
|
344
|
+
const timer = setTimeout(() => {
|
|
345
|
+
proc.kill("SIGTERM");
|
|
346
|
+
reject(new Error("AI enrichment timed out after 2 minutes"));
|
|
347
|
+
}, timeout);
|
|
348
|
+
proc.on("close", (code) => {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
if (code !== 0) {
|
|
351
|
+
reject(new Error(`claude CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
352
|
+
} else {
|
|
353
|
+
resolve(parseEnrichmentResponse(stdout, model));
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
proc.on("error", (err) => {
|
|
357
|
+
clearTimeout(timer);
|
|
358
|
+
reject(new Error(`Failed to run claude CLI: ${err.message}`));
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/commands/history.ts
|
|
364
|
+
async function runHistory(args, projectDir) {
|
|
365
|
+
const branch = getHistoryBranch();
|
|
366
|
+
const subcommand = args[0];
|
|
367
|
+
switch (subcommand) {
|
|
368
|
+
case "init":
|
|
369
|
+
return await initSubcommand(projectDir);
|
|
370
|
+
case "capture":
|
|
371
|
+
return await captureSubcommand(args.slice(1), projectDir);
|
|
372
|
+
case "backfill":
|
|
373
|
+
return await backfillSubcommand(args.slice(1), projectDir);
|
|
374
|
+
case "list":
|
|
375
|
+
return listSubcommand(args.slice(1), projectDir);
|
|
376
|
+
case "show":
|
|
377
|
+
return showSubcommand(args.slice(1), projectDir);
|
|
378
|
+
case "enrich":
|
|
379
|
+
return await enrichSubcommand(args.slice(1), projectDir);
|
|
380
|
+
case "rewind":
|
|
381
|
+
return await rewindSubcommand(args.slice(1), projectDir);
|
|
382
|
+
default:
|
|
383
|
+
console.log(`
|
|
384
|
+
Usage: ulpi history <subcommand>
|
|
385
|
+
|
|
386
|
+
Subcommands:
|
|
387
|
+
init Initialize the ${branch} branch
|
|
388
|
+
capture [sha...] Capture entries for specific commits (default: HEAD)
|
|
389
|
+
backfill [--limit N] Backfill recent commits (default: 20)
|
|
390
|
+
list [--limit N] List timeline entries
|
|
391
|
+
show <sha> Show entry details
|
|
392
|
+
enrich [sha...] [--model <id>] AI-enrich entries
|
|
393
|
+
rewind <sha> Rewind working tree to a captured commit
|
|
394
|
+
`.trim());
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function initSubcommand(projectDir) {
|
|
398
|
+
const branch = getHistoryBranch();
|
|
399
|
+
console.log(chalk.bold("\nULPI History \u2014 Initialize\n"));
|
|
400
|
+
if (historyBranchExists(projectDir)) {
|
|
401
|
+
console.log(chalk.red(`Error: ${branch} branch already exists.`));
|
|
402
|
+
console.log(chalk.dim("Use 'history capture' or 'history backfill' to add entries."));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const reviewPlansDir = REVIEWS_DIR;
|
|
406
|
+
let collectReviewPlans = false;
|
|
407
|
+
if (fs2.existsSync(reviewPlansDir)) {
|
|
408
|
+
const answer = await askQuestion(
|
|
409
|
+
"ULPI Review plans detected. Include in history? (Y/n) "
|
|
410
|
+
);
|
|
411
|
+
collectReviewPlans = answer.trim().toLowerCase() !== "n";
|
|
412
|
+
}
|
|
413
|
+
const projectName = path2.basename(projectDir);
|
|
414
|
+
try {
|
|
415
|
+
initHistoryBranch(projectDir, projectName, "0.1.0");
|
|
416
|
+
} catch (err) {
|
|
417
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
418
|
+
console.log(chalk.red(`Error: ${message}`));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (collectReviewPlans) {
|
|
422
|
+
try {
|
|
423
|
+
const { withWorktree, writeAndStage, commitInWorktree } = await import("./dist-RJGCUS3L.js");
|
|
424
|
+
const meta = readBranchMeta(projectDir);
|
|
425
|
+
if (meta) {
|
|
426
|
+
meta.config.collectReviewPlans = true;
|
|
427
|
+
await withWorktree(projectDir, branch, (worktreeDir) => {
|
|
428
|
+
writeAndStage(worktreeDir, "meta.json", JSON.stringify(meta, null, 2) + "\n");
|
|
429
|
+
commitInWorktree(worktreeDir, "Enable review plan collection");
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
console.log(chalk.green(`\u2713 Initialized ${branch} branch`));
|
|
436
|
+
console.log(chalk.dim(` Branch: ${branch}`));
|
|
437
|
+
console.log(chalk.dim(` Project: ${projectName}`));
|
|
438
|
+
if (collectReviewPlans) {
|
|
439
|
+
console.log(chalk.dim(" Review plans: enabled"));
|
|
440
|
+
}
|
|
441
|
+
const installHooksAnswer = await askQuestion(
|
|
442
|
+
"Install git hooks (prepare-commit-msg, post-commit, pre-push)? (Y/n) "
|
|
443
|
+
);
|
|
444
|
+
if (installHooksAnswer.trim().toLowerCase() !== "n") {
|
|
445
|
+
try {
|
|
446
|
+
const { installGitHooks } = await import("./dist-RJGCUS3L.js");
|
|
447
|
+
const { getBinaryPath } = await import("./dist-RKOGLK7R.js");
|
|
448
|
+
const binaryPath = getBinaryPath();
|
|
449
|
+
const result = installGitHooks(projectDir, binaryPath);
|
|
450
|
+
if (result.installed.length > 0) {
|
|
451
|
+
console.log(chalk.green(` \u2713 Installed hooks: ${result.installed.join(", ")}`));
|
|
452
|
+
}
|
|
453
|
+
if (result.skipped.length > 0) {
|
|
454
|
+
console.log(chalk.dim(` Skipped (already installed): ${result.skipped.join(", ")}`));
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
458
|
+
console.log(chalk.yellow(` Warning: Could not install git hooks: ${message}`));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
console.log(chalk.dim("\nNext: run 'ulpi history backfill' to capture recent commits."));
|
|
462
|
+
}
|
|
463
|
+
async function captureSubcommand(args, projectDir) {
|
|
464
|
+
const branch = getHistoryBranch();
|
|
465
|
+
if (!historyBranchExists(projectDir)) {
|
|
466
|
+
console.log(chalk.red(`Error: ${branch} branch not found. Run 'history init' first.`));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
let shas = args.filter((a) => !a.startsWith("--"));
|
|
470
|
+
if (shas.length === 0) {
|
|
471
|
+
const head = getCurrentHead(projectDir);
|
|
472
|
+
if (!head) {
|
|
473
|
+
console.log(chalk.red("Error: Could not determine HEAD. Is this a git repository?"));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
shas = [head];
|
|
477
|
+
}
|
|
478
|
+
const maxDiffSize = DEFAULT_HISTORY_CONFIG.maxDiffSize;
|
|
479
|
+
for (const sha of shas) {
|
|
480
|
+
try {
|
|
481
|
+
if (entryExists(projectDir, sha)) {
|
|
482
|
+
console.log(chalk.yellow(` Skipped ${sha.slice(0, 7)} \u2014 already captured`));
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const metadata = getCommitMetadata(projectDir, sha);
|
|
486
|
+
const diffStats = getCommitDiffStats(projectDir, sha);
|
|
487
|
+
const rawDiffResult = getCommitRawDiff(projectDir, sha, maxDiffSize);
|
|
488
|
+
const guardsYaml = loadActiveGuards(projectDir);
|
|
489
|
+
const entry = {
|
|
490
|
+
version: 1,
|
|
491
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
492
|
+
commit: metadata,
|
|
493
|
+
diff: diffStats,
|
|
494
|
+
rawDiff: rawDiffResult.diff || void 0,
|
|
495
|
+
diffTruncated: rawDiffResult.truncated || void 0,
|
|
496
|
+
session: null,
|
|
497
|
+
enrichment: null,
|
|
498
|
+
reviewPlans: null
|
|
499
|
+
};
|
|
500
|
+
await writeHistoryEntry(projectDir, entry, {
|
|
501
|
+
state: null,
|
|
502
|
+
events: [],
|
|
503
|
+
guardsYaml
|
|
504
|
+
});
|
|
505
|
+
console.log(chalk.green(` \u2713 ${metadata.shortSha}`) + chalk.dim(` ${truncate(metadata.subject, 50)}`));
|
|
506
|
+
} catch (err) {
|
|
507
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
508
|
+
console.log(chalk.red(` \u2717 ${sha.slice(0, 7)}: ${message}`));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async function backfillSubcommand(args, projectDir) {
|
|
513
|
+
const branch = getHistoryBranch();
|
|
514
|
+
if (!historyBranchExists(projectDir)) {
|
|
515
|
+
console.log(chalk.red(`Error: ${branch} branch not found. Run 'history init' first.`));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const limitFlag = args.find((a) => a.startsWith("--limit"));
|
|
519
|
+
let limit = 20;
|
|
520
|
+
if (limitFlag) {
|
|
521
|
+
const idx = args.indexOf(limitFlag);
|
|
522
|
+
if (limitFlag.includes("=")) {
|
|
523
|
+
limit = parseInt(limitFlag.split("=")[1], 10) || 20;
|
|
524
|
+
} else if (args[idx + 1]) {
|
|
525
|
+
limit = parseInt(args[idx + 1], 10) || 20;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (args[0] && !args[0].startsWith("--") && /^\d+$/.test(args[0])) {
|
|
529
|
+
limit = parseInt(args[0], 10);
|
|
530
|
+
}
|
|
531
|
+
console.log(chalk.bold(`
|
|
532
|
+
ULPI History \u2014 Backfill (last ${limit} commits)
|
|
533
|
+
`));
|
|
534
|
+
const commits = listRecentCommits(projectDir, limit);
|
|
535
|
+
if (commits.length === 0) {
|
|
536
|
+
console.log(chalk.yellow("No commits found."));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const meta = readBranchMeta(projectDir);
|
|
540
|
+
const config = meta?.config ?? DEFAULT_HISTORY_CONFIG;
|
|
541
|
+
const maxDiffSize = config.maxDiffSize;
|
|
542
|
+
let captured = 0;
|
|
543
|
+
let skipped = 0;
|
|
544
|
+
let withSession = 0;
|
|
545
|
+
let withReview = 0;
|
|
546
|
+
for (const sha of commits) {
|
|
547
|
+
if (entryExists(projectDir, sha)) {
|
|
548
|
+
skipped++;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const metadata = getCommitMetadata(projectDir, sha);
|
|
553
|
+
const diffStats = getCommitDiffStats(projectDir, sha);
|
|
554
|
+
const rawDiffResult = getCommitRawDiff(projectDir, sha, maxDiffSize);
|
|
555
|
+
const guardsYaml = loadActiveGuards(projectDir);
|
|
556
|
+
let sessionSummary = null;
|
|
557
|
+
let sessionState = null;
|
|
558
|
+
let sessionEvents = [];
|
|
559
|
+
const sessionMatch = findSessionForCommit(projectDir, metadata.authorDate);
|
|
560
|
+
if (sessionMatch) {
|
|
561
|
+
sessionSummary = buildSessionSummary(sessionMatch.state, sessionMatch.events);
|
|
562
|
+
sessionState = sessionMatch.state;
|
|
563
|
+
sessionEvents = sessionMatch.events;
|
|
564
|
+
withSession++;
|
|
565
|
+
}
|
|
566
|
+
let reviewSnapshots = null;
|
|
567
|
+
let reviewRawData;
|
|
568
|
+
if (config.collectReviewPlans) {
|
|
569
|
+
const sessionStart = sessionState?.startedAt;
|
|
570
|
+
const reviewResult = findReviewPlansForCommit(metadata.authorDate, sessionStart, projectDir);
|
|
571
|
+
if (reviewResult) {
|
|
572
|
+
reviewSnapshots = reviewResult.snapshots;
|
|
573
|
+
reviewRawData = reviewResult.rawData;
|
|
574
|
+
withReview++;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const entry = {
|
|
578
|
+
version: 1,
|
|
579
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
580
|
+
commit: metadata,
|
|
581
|
+
diff: diffStats,
|
|
582
|
+
rawDiff: rawDiffResult.diff || void 0,
|
|
583
|
+
diffTruncated: rawDiffResult.truncated || void 0,
|
|
584
|
+
session: sessionSummary,
|
|
585
|
+
enrichment: null,
|
|
586
|
+
reviewPlans: reviewSnapshots
|
|
587
|
+
};
|
|
588
|
+
await writeHistoryEntry(projectDir, entry, {
|
|
589
|
+
state: sessionState,
|
|
590
|
+
events: sessionEvents,
|
|
591
|
+
guardsYaml,
|
|
592
|
+
reviewPlans: reviewRawData
|
|
593
|
+
});
|
|
594
|
+
const tags = [];
|
|
595
|
+
if (sessionSummary) tags.push(chalk.cyan("session"));
|
|
596
|
+
if (reviewSnapshots) tags.push(chalk.magenta("review"));
|
|
597
|
+
const tagStr = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
|
|
598
|
+
console.log(
|
|
599
|
+
chalk.green(` \u2713 ${metadata.shortSha}`) + chalk.dim(` ${truncate(metadata.subject, 40)}`) + tagStr
|
|
600
|
+
);
|
|
601
|
+
captured++;
|
|
602
|
+
} catch (err) {
|
|
603
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
604
|
+
console.log(chalk.red(` \u2717 ${sha.slice(0, 7)}: ${message}`));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
console.log("");
|
|
608
|
+
console.log(chalk.bold("Summary:"));
|
|
609
|
+
console.log(` Captured: ${chalk.green(String(captured))}`);
|
|
610
|
+
if (skipped > 0) console.log(` Skipped (already captured): ${chalk.dim(String(skipped))}`);
|
|
611
|
+
if (withSession > 0) console.log(` With session data: ${chalk.cyan(String(withSession))}`);
|
|
612
|
+
if (withReview > 0) console.log(` With review plans: ${chalk.magenta(String(withReview))}`);
|
|
613
|
+
console.log(` Git-only (no session): ${chalk.dim(String(captured - withSession))}`);
|
|
614
|
+
}
|
|
615
|
+
function listSubcommand(args, projectDir) {
|
|
616
|
+
const branch = getHistoryBranch();
|
|
617
|
+
if (!historyBranchExists(projectDir)) {
|
|
618
|
+
console.log(chalk.red(`Error: ${branch} branch not found. Run 'history init' first.`));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const limitFlag = args.find((a) => a.startsWith("--limit"));
|
|
622
|
+
let limit = 30;
|
|
623
|
+
if (limitFlag) {
|
|
624
|
+
const idx = args.indexOf(limitFlag);
|
|
625
|
+
if (limitFlag.includes("=")) {
|
|
626
|
+
limit = parseInt(limitFlag.split("=")[1], 10) || 30;
|
|
627
|
+
} else if (args[idx + 1]) {
|
|
628
|
+
limit = parseInt(args[idx + 1], 10) || 30;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const timeline = readTimeline(projectDir);
|
|
632
|
+
if (!timeline || timeline.entries.length === 0) {
|
|
633
|
+
console.log(chalk.yellow("No history entries found. Run 'history capture' or 'history backfill'."));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const entries = timeline.entries.slice(0, limit);
|
|
637
|
+
console.log(chalk.bold("\nULPI History Timeline\n"));
|
|
638
|
+
console.log(
|
|
639
|
+
chalk.dim(" Date ") + chalk.dim("SHA ") + chalk.dim("Branch ") + chalk.dim("Subject ") + chalk.dim("Ses ") + chalk.dim("Rev ") + chalk.dim("AI")
|
|
640
|
+
);
|
|
641
|
+
console.log(chalk.dim(" " + "-".repeat(95)));
|
|
642
|
+
for (const e of entries) {
|
|
643
|
+
const date = formatShortDate(e.date);
|
|
644
|
+
const sha = chalk.yellow(e.shortSha);
|
|
645
|
+
const branch2 = padRight(e.branch === "unknown" ? "" : e.branch, 16).slice(0, 16);
|
|
646
|
+
const subject = padRight(truncate(e.subject, 40), 40);
|
|
647
|
+
const session = e.hasSession ? chalk.green(" Y ") : chalk.dim(" - ");
|
|
648
|
+
const review = e.hasReviewPlans ? chalk.green(" Y ") : chalk.dim(" - ");
|
|
649
|
+
const ai = e.hasEnrichment ? chalk.green(" Y") : chalk.dim(" -");
|
|
650
|
+
console.log(` ${date} ${sha} ${chalk.dim(branch2)} ${subject} ${session}${review}${ai}`);
|
|
651
|
+
}
|
|
652
|
+
if (timeline.entries.length > limit) {
|
|
653
|
+
console.log(chalk.dim(`
|
|
654
|
+
Showing ${limit} of ${timeline.entries.length} entries. Use --limit to see more.`));
|
|
655
|
+
}
|
|
656
|
+
console.log("");
|
|
657
|
+
}
|
|
658
|
+
function showSubcommand(args, projectDir) {
|
|
659
|
+
const branch = getHistoryBranch();
|
|
660
|
+
const sha = args[0];
|
|
661
|
+
if (!sha) {
|
|
662
|
+
console.log(chalk.red("Usage: ulpi history show <sha>"));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (!historyBranchExists(projectDir)) {
|
|
666
|
+
console.log(chalk.red(`Error: ${branch} branch not found. Run 'history init' first.`));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const entry = readHistoryEntry(projectDir, sha);
|
|
670
|
+
if (!entry) {
|
|
671
|
+
console.log(chalk.red(`Entry not found for SHA: ${sha}`));
|
|
672
|
+
console.log(chalk.dim("Use 'history list' to see available entries."));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
console.log(chalk.bold("\nCommit"));
|
|
676
|
+
console.log(` SHA: ${chalk.yellow(entry.commit.sha)}`);
|
|
677
|
+
console.log(` Subject: ${entry.commit.subject}`);
|
|
678
|
+
if (entry.commit.message !== entry.commit.subject) {
|
|
679
|
+
console.log(` Message: ${entry.commit.message}`);
|
|
680
|
+
}
|
|
681
|
+
console.log(` Author: ${entry.commit.authorName} <${entry.commit.authorEmail}>`);
|
|
682
|
+
console.log(` Date: ${entry.commit.authorDate}`);
|
|
683
|
+
console.log(` Branch: ${entry.commit.branch}`);
|
|
684
|
+
console.log(chalk.bold("\nDiff Stats"));
|
|
685
|
+
console.log(` Files changed: ${entry.diff.filesChanged}`);
|
|
686
|
+
console.log(` Insertions: ${chalk.green("+" + entry.diff.insertions)}`);
|
|
687
|
+
console.log(` Deletions: ${chalk.red("-" + entry.diff.deletions)}`);
|
|
688
|
+
if (entry.diff.files.length > 0) {
|
|
689
|
+
console.log(chalk.dim(" Files:"));
|
|
690
|
+
for (const f of entry.diff.files.slice(0, 15)) {
|
|
691
|
+
const statusColor = f.status === "added" ? chalk.green : f.status === "deleted" ? chalk.red : f.status === "renamed" ? chalk.blue : chalk.dim;
|
|
692
|
+
console.log(` ${statusColor(f.status.padEnd(8))} ${f.path} (+${f.additions}/-${f.deletions})`);
|
|
693
|
+
}
|
|
694
|
+
if (entry.diff.files.length > 15) {
|
|
695
|
+
console.log(chalk.dim(` ... and ${entry.diff.files.length - 15} more files`));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (entry.session) {
|
|
699
|
+
const s = entry.session;
|
|
700
|
+
console.log(chalk.bold("\nSession"));
|
|
701
|
+
console.log(` ID: ${s.sessionId}${s.sessionName ? ` (${s.sessionName})` : ""}`);
|
|
702
|
+
console.log(` Total events: ${s.totalEvents}`);
|
|
703
|
+
console.log(` Rules enforced: ${s.stats.rulesEnforced}`);
|
|
704
|
+
console.log(` Blocked: ${s.stats.actionsBlocked}`);
|
|
705
|
+
console.log(` Auto-approved: ${s.stats.autoActionsRun}`);
|
|
706
|
+
console.log(` Files: ${s.stats.filesRead} read, ${s.stats.filesWritten} written, ${s.stats.filesDeleted} deleted`);
|
|
707
|
+
console.log(` Commands: ${s.stats.commandsRun}`);
|
|
708
|
+
console.log(` Tests: ${s.stats.testsRun ? s.stats.testsPassed ? chalk.green("passed") : chalk.red("failed") : chalk.dim("not run")}`);
|
|
709
|
+
console.log(` Lint: ${s.stats.lintRun ? chalk.green("run") : chalk.dim("not run")}`);
|
|
710
|
+
console.log(` Build: ${s.stats.buildRun ? chalk.green("run") : chalk.dim("not run")}`);
|
|
711
|
+
if (s.blockedTools.length > 0) {
|
|
712
|
+
console.log(` Blocked tools: ${chalk.red(s.blockedTools.join(", "))}`);
|
|
713
|
+
}
|
|
714
|
+
if (s.preconditionsFired.length > 0) {
|
|
715
|
+
console.log(` Preconditions: ${chalk.yellow(s.preconditionsFired.join(", "))}`);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
console.log(chalk.dim("\nSession: none (git-only capture)"));
|
|
719
|
+
}
|
|
720
|
+
if (entry.reviewPlans && entry.reviewPlans.length > 0) {
|
|
721
|
+
console.log(chalk.bold("\nReview Plans"));
|
|
722
|
+
for (const plan of entry.reviewPlans) {
|
|
723
|
+
const decision = plan.version.decision ? plan.version.decision.behavior === "allow" ? chalk.green("approved") : chalk.red("denied") : chalk.dim("pending");
|
|
724
|
+
console.log(` ${chalk.magenta(plan.title)} \u2014 ${decision}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (entry.enrichment) {
|
|
728
|
+
const e = entry.enrichment;
|
|
729
|
+
console.log(chalk.bold("\nAI Enrichment") + chalk.dim(` (${e.model}, ${e.generatedAt})`));
|
|
730
|
+
console.log(` Summary: ${e.summary}`);
|
|
731
|
+
if (e.intent) {
|
|
732
|
+
console.log(` Intent: ${e.intent}`);
|
|
733
|
+
}
|
|
734
|
+
if (e.challenges && e.challenges.length > 0) {
|
|
735
|
+
console.log(chalk.yellow(" Challenges:"));
|
|
736
|
+
for (const c of e.challenges) {
|
|
737
|
+
console.log(chalk.yellow(` - ${c}`));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (e.ruleInsights && e.ruleInsights.length > 0) {
|
|
741
|
+
console.log(chalk.cyan(" Rule Insights:"));
|
|
742
|
+
for (const r of e.ruleInsights) {
|
|
743
|
+
console.log(chalk.cyan(` - ${r}`));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
console.log("");
|
|
748
|
+
}
|
|
749
|
+
async function enrichSubcommand(args, projectDir) {
|
|
750
|
+
const branch = getHistoryBranch();
|
|
751
|
+
if (!historyBranchExists(projectDir)) {
|
|
752
|
+
console.log(chalk.red(`Error: ${branch} branch not found. Run 'history init' first.`));
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (!isClaudeCliAvailable()) {
|
|
756
|
+
console.log(chalk.red("Error: Claude CLI not found on PATH."));
|
|
757
|
+
console.log(chalk.dim("Install it from: https://docs.anthropic.com/en/docs/claude-cli"));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const modelIdx = args.findIndex((a) => a === "--model");
|
|
761
|
+
let model = DEFAULT_AI_MODEL;
|
|
762
|
+
if (modelIdx !== -1 && args[modelIdx + 1]) {
|
|
763
|
+
model = args[modelIdx + 1];
|
|
764
|
+
}
|
|
765
|
+
const modelEqFlag = args.find((a) => a.startsWith("--model="));
|
|
766
|
+
if (modelEqFlag) {
|
|
767
|
+
model = modelEqFlag.split("=")[1];
|
|
768
|
+
}
|
|
769
|
+
let shas = args.filter((a) => !a.startsWith("--") && (modelIdx === -1 || args.indexOf(a) !== modelIdx + 1));
|
|
770
|
+
if (shas.length === 0) {
|
|
771
|
+
const timeline = readTimeline(projectDir);
|
|
772
|
+
if (!timeline || timeline.entries.length === 0) {
|
|
773
|
+
console.log(chalk.yellow("No history entries found."));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
shas = timeline.entries.filter((e) => !e.hasEnrichment).map((e) => e.sha);
|
|
777
|
+
if (shas.length === 0) {
|
|
778
|
+
console.log(chalk.green("All entries are already enriched."));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
console.log(chalk.bold(`
|
|
782
|
+
ULPI History \u2014 AI Enrichment (${shas.length} entries, model: ${model})
|
|
783
|
+
`));
|
|
784
|
+
} else {
|
|
785
|
+
console.log(chalk.bold(`
|
|
786
|
+
ULPI History \u2014 AI Enrichment (model: ${model})
|
|
787
|
+
`));
|
|
788
|
+
}
|
|
789
|
+
let enriched = 0;
|
|
790
|
+
let failed = 0;
|
|
791
|
+
for (const sha of shas) {
|
|
792
|
+
const entry = readHistoryEntry(projectDir, sha);
|
|
793
|
+
if (!entry) {
|
|
794
|
+
console.log(chalk.yellow(` Skipped ${sha.slice(0, 7)} \u2014 entry not found`));
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
let rawEvents = [];
|
|
798
|
+
const rawData = readEntryRawData(projectDir, sha);
|
|
799
|
+
if (rawData.events) {
|
|
800
|
+
try {
|
|
801
|
+
rawEvents = rawData.events.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
process.stdout.write(chalk.dim(` Enriching ${entry.commit.shortSha}...`));
|
|
807
|
+
const enrichment = await enrichEntry(entry, rawEvents, model);
|
|
808
|
+
await updateEntryEnrichment(projectDir, sha, enrichment);
|
|
809
|
+
console.log(chalk.green(" done"));
|
|
810
|
+
enriched++;
|
|
811
|
+
} catch (err) {
|
|
812
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
813
|
+
console.log(chalk.red(` failed: ${message}`));
|
|
814
|
+
failed++;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
console.log("");
|
|
818
|
+
console.log(chalk.bold("Summary:"));
|
|
819
|
+
console.log(` Enriched: ${chalk.green(String(enriched))}`);
|
|
820
|
+
if (failed > 0) console.log(` Failed: ${chalk.red(String(failed))}`);
|
|
821
|
+
}
|
|
822
|
+
async function rewindSubcommand(args, projectDir) {
|
|
823
|
+
const branch = getHistoryBranch();
|
|
824
|
+
const sha = args[0];
|
|
825
|
+
if (!sha) {
|
|
826
|
+
console.log(chalk.red("Usage: ulpi history rewind <sha>"));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (!historyBranchExists(projectDir)) {
|
|
830
|
+
console.log(chalk.red(`Error: ${branch} branch not found. Run 'history init' first.`));
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const { execFileSync: execFileSync2 } = await import("child_process");
|
|
834
|
+
try {
|
|
835
|
+
execFileSync2("git", ["diff", "--quiet"], {
|
|
836
|
+
cwd: projectDir,
|
|
837
|
+
encoding: "utf-8",
|
|
838
|
+
timeout: 5e3
|
|
839
|
+
});
|
|
840
|
+
execFileSync2("git", ["diff", "--cached", "--quiet"], {
|
|
841
|
+
cwd: projectDir,
|
|
842
|
+
encoding: "utf-8",
|
|
843
|
+
timeout: 5e3
|
|
844
|
+
});
|
|
845
|
+
} catch {
|
|
846
|
+
console.log(chalk.red("Error: You have uncommitted changes."));
|
|
847
|
+
console.log(chalk.dim("Commit or stash your changes before rewinding."));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const timeline = readTimeline(projectDir);
|
|
851
|
+
if (!timeline) {
|
|
852
|
+
console.log(chalk.red("No timeline found. Run 'ulpi history backfill' first."));
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const match = timeline.entries.find(
|
|
856
|
+
(e) => e.sha === sha || e.shortSha === sha || e.sha.startsWith(sha)
|
|
857
|
+
);
|
|
858
|
+
if (!match) {
|
|
859
|
+
console.log(chalk.red(`No history entry found for SHA: ${sha}`));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
console.log(chalk.bold(`
|
|
863
|
+
Rewind to: ${match.shortSha} -- ${match.subject}`));
|
|
864
|
+
console.log(chalk.dim(`Date: ${match.date}`));
|
|
865
|
+
console.log(chalk.dim(`Branch: ${match.branch}`));
|
|
866
|
+
console.log(chalk.dim(`Files changed: ${match.filesChanged}`));
|
|
867
|
+
console.log("");
|
|
868
|
+
try {
|
|
869
|
+
const diffStat = execFileSync2("git", ["diff", "--stat", `HEAD...${match.sha}`], {
|
|
870
|
+
cwd: projectDir,
|
|
871
|
+
encoding: "utf-8",
|
|
872
|
+
timeout: 1e4
|
|
873
|
+
}).trim();
|
|
874
|
+
if (diffStat) {
|
|
875
|
+
console.log(chalk.dim("Changes to apply:"));
|
|
876
|
+
console.log(diffStat);
|
|
877
|
+
console.log("");
|
|
878
|
+
}
|
|
879
|
+
} catch {
|
|
880
|
+
}
|
|
881
|
+
const entry = readHistoryEntry(projectDir, match.sha);
|
|
882
|
+
if (entry?.transcriptSize && entry.transcriptSize > 0) {
|
|
883
|
+
console.log(chalk.dim(`Transcript available (${(entry.transcriptSize / 1024).toFixed(1)} KB)`));
|
|
884
|
+
}
|
|
885
|
+
const answer = await askQuestion(chalk.yellow("This will reset your working tree. Continue? (y/N) "));
|
|
886
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
887
|
+
console.log(chalk.dim("Rewind cancelled."));
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
execFileSync2("git", ["reset", "--hard", match.sha], {
|
|
892
|
+
cwd: projectDir,
|
|
893
|
+
encoding: "utf-8",
|
|
894
|
+
timeout: 1e4
|
|
895
|
+
});
|
|
896
|
+
console.log(chalk.green(`
|
|
897
|
+
\u2713 Rewound to ${match.shortSha}`));
|
|
898
|
+
} catch (err) {
|
|
899
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
900
|
+
console.log(chalk.red(`Rewind failed: ${message}`));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function askQuestion(question) {
|
|
904
|
+
const rl = readline.createInterface({
|
|
905
|
+
input: process.stdin,
|
|
906
|
+
output: process.stdout
|
|
907
|
+
});
|
|
908
|
+
return new Promise((resolve) => {
|
|
909
|
+
rl.question(question, (answer) => {
|
|
910
|
+
rl.close();
|
|
911
|
+
resolve(answer);
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function truncate(str, maxLen) {
|
|
916
|
+
if (str.length <= maxLen) return str;
|
|
917
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
918
|
+
}
|
|
919
|
+
function padRight(str, len) {
|
|
920
|
+
return str.length >= len ? str : str + " ".repeat(len - str.length);
|
|
921
|
+
}
|
|
922
|
+
function formatShortDate(iso) {
|
|
923
|
+
try {
|
|
924
|
+
const d = new Date(iso);
|
|
925
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
926
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
927
|
+
return `${d.getFullYear()}-${month}-${day}`;
|
|
928
|
+
} catch {
|
|
929
|
+
return iso.slice(0, 10);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
export {
|
|
933
|
+
runHistory
|
|
934
|
+
};
|