promptup-plugin 0.1.1
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 +78 -0
- package/bin/install.cjs +306 -0
- package/bin/promptup-plugin +8 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +123 -0
- package/dist/db.d.ts +35 -0
- package/dist/db.js +327 -0
- package/dist/decision-detector.d.ts +11 -0
- package/dist/decision-detector.js +47 -0
- package/dist/evaluator.d.ts +10 -0
- package/dist/evaluator.js +844 -0
- package/dist/git-activity-extractor.d.ts +35 -0
- package/dist/git-activity-extractor.js +167 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +54 -0
- package/dist/pr-report-generator.d.ts +20 -0
- package/dist/pr-report-generator.js +421 -0
- package/dist/shared/decision-classifier.d.ts +60 -0
- package/dist/shared/decision-classifier.js +385 -0
- package/dist/shared/decision-score.d.ts +7 -0
- package/dist/shared/decision-score.js +31 -0
- package/dist/shared/dimensions.d.ts +43 -0
- package/dist/shared/dimensions.js +361 -0
- package/dist/shared/scoring.d.ts +89 -0
- package/dist/shared/scoring.js +161 -0
- package/dist/shared/types.d.ts +108 -0
- package/dist/shared/types.js +9 -0
- package/dist/tools.d.ts +30 -0
- package/dist/tools.js +456 -0
- package/dist/transcript-parser.d.ts +36 -0
- package/dist/transcript-parser.js +201 -0
- package/hooks/auto-eval.sh +44 -0
- package/hooks/check-update.sh +26 -0
- package/hooks/debug-hook.sh +3 -0
- package/hooks/hooks.json +36 -0
- package/hooks/render-eval.sh +137 -0
- package/package.json +60 -0
- package/skills/eval/SKILL.md +12 -0
- package/skills/pr-report/SKILL.md +37 -0
- package/skills/status/SKILL.md +28 -0
- package/statusline.sh +46 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export interface SessionRow {
|
|
2
|
+
id: string;
|
|
3
|
+
project_path: string | null;
|
|
4
|
+
transcript_path: string | null;
|
|
5
|
+
status: string;
|
|
6
|
+
message_count: number;
|
|
7
|
+
started_at: string;
|
|
8
|
+
ended_at: string | null;
|
|
9
|
+
created_at: string;
|
|
10
|
+
}
|
|
11
|
+
export interface EvaluationRow {
|
|
12
|
+
id: string;
|
|
13
|
+
session_id: string;
|
|
14
|
+
trigger_type: string;
|
|
15
|
+
report_type: string;
|
|
16
|
+
composite_score: number;
|
|
17
|
+
dimension_scores: string;
|
|
18
|
+
recommendations: string | null;
|
|
19
|
+
trends: string | null;
|
|
20
|
+
risk_flags: string | null;
|
|
21
|
+
raw_evaluation: string | null;
|
|
22
|
+
message_count: number;
|
|
23
|
+
message_range_from: number;
|
|
24
|
+
message_range_to: number;
|
|
25
|
+
weight_profile: string;
|
|
26
|
+
created_at: string;
|
|
27
|
+
}
|
|
28
|
+
export interface MessageRow {
|
|
29
|
+
id: string;
|
|
30
|
+
session_id: string;
|
|
31
|
+
role: 'user' | 'assistant' | 'system' | 'tool_result';
|
|
32
|
+
content: string;
|
|
33
|
+
tool_uses: string | null;
|
|
34
|
+
sequence_number: number;
|
|
35
|
+
tokens_in: number;
|
|
36
|
+
tokens_out: number;
|
|
37
|
+
model: string | null;
|
|
38
|
+
created_at: string;
|
|
39
|
+
}
|
|
40
|
+
export type DecisionType = 'steer' | 'accept' | 'reject' | 'modify' | 'validate' | 'scope';
|
|
41
|
+
export type DecisionDepth = 'surface' | 'tactical' | 'architectural';
|
|
42
|
+
export type DecisionOpinionation = 'low' | 'medium' | 'high';
|
|
43
|
+
export type DecisionSignal = 'high' | 'medium' | 'low';
|
|
44
|
+
export interface DecisionRow {
|
|
45
|
+
id: string;
|
|
46
|
+
session_id: string;
|
|
47
|
+
type: DecisionType;
|
|
48
|
+
message_index: number;
|
|
49
|
+
context: string;
|
|
50
|
+
files_affected: string;
|
|
51
|
+
source: 'plugin' | 'daemon';
|
|
52
|
+
matched_rule: string | null;
|
|
53
|
+
depth: DecisionDepth | null;
|
|
54
|
+
opinionation: DecisionOpinionation | null;
|
|
55
|
+
ai_action: string | null;
|
|
56
|
+
signal: DecisionSignal | null;
|
|
57
|
+
created_at: string;
|
|
58
|
+
}
|
|
59
|
+
export type GitOpType = 'checkout' | 'commit' | 'push' | 'branch_create' | 'merge';
|
|
60
|
+
export interface GitActivityRow {
|
|
61
|
+
id: string;
|
|
62
|
+
session_id: string;
|
|
63
|
+
type: GitOpType;
|
|
64
|
+
branch: string | null;
|
|
65
|
+
commit_hash: string | null;
|
|
66
|
+
commit_message: string | null;
|
|
67
|
+
remote: string | null;
|
|
68
|
+
raw_command: string;
|
|
69
|
+
message_index: number;
|
|
70
|
+
created_at: string;
|
|
71
|
+
}
|
|
72
|
+
export interface PRReportRow {
|
|
73
|
+
id: string;
|
|
74
|
+
branch: string;
|
|
75
|
+
repo: string;
|
|
76
|
+
pr_number: number | null;
|
|
77
|
+
pr_url: string | null;
|
|
78
|
+
commits: string;
|
|
79
|
+
session_ids: string;
|
|
80
|
+
total_decisions: number;
|
|
81
|
+
decision_breakdown: string;
|
|
82
|
+
dqs: number | null;
|
|
83
|
+
markdown: string;
|
|
84
|
+
posted_at: string | null;
|
|
85
|
+
created_at: string;
|
|
86
|
+
}
|
|
87
|
+
export interface EvalDimensionScore {
|
|
88
|
+
key: string;
|
|
89
|
+
score: number;
|
|
90
|
+
weight: number;
|
|
91
|
+
reasoning?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface EvalRecommendation {
|
|
94
|
+
dimension_key: string;
|
|
95
|
+
priority: 'low' | 'medium' | 'high';
|
|
96
|
+
recommendation: string;
|
|
97
|
+
suggestions?: string[];
|
|
98
|
+
}
|
|
99
|
+
export interface EvalTrend {
|
|
100
|
+
dimension_key: string;
|
|
101
|
+
direction: 'improving' | 'declining' | 'stable';
|
|
102
|
+
delta: number;
|
|
103
|
+
previous_score: number;
|
|
104
|
+
current_score: number;
|
|
105
|
+
}
|
|
106
|
+
export type EvalTriggerType = 'manual' | 'prompt_count' | 'session_end';
|
|
107
|
+
export type WeightProfileKey = 'balanced' | 'greenfield' | 'bugfix' | 'refactor' | 'security_review';
|
|
108
|
+
export declare function classify(score: number): 'junior' | 'middle' | 'senior';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// ─── Session tracking ─────────────────────────────────────────────────────────
|
|
2
|
+
// ─── Classification ──────────────────────────────────────────────────────────
|
|
3
|
+
export function classify(score) {
|
|
4
|
+
if (score <= 40)
|
|
5
|
+
return 'junior';
|
|
6
|
+
if (score <= 70)
|
|
7
|
+
return 'middle';
|
|
8
|
+
return 'senior';
|
|
9
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool handlers for the standalone PromptUp plugin.
|
|
3
|
+
*
|
|
4
|
+
* Three tools:
|
|
5
|
+
* - evaluate_session — evaluate a coding session across 11 skill dimensions
|
|
6
|
+
* - generate_pr_report — generate a DQS report for a git branch
|
|
7
|
+
* - get_status — show tracking status and recent activity
|
|
8
|
+
*
|
|
9
|
+
* STANDALONE — no imports from @promptup/shared or workspace packages.
|
|
10
|
+
*/
|
|
11
|
+
interface ToolResponse {
|
|
12
|
+
content: Array<{
|
|
13
|
+
type: 'text';
|
|
14
|
+
text: string;
|
|
15
|
+
}>;
|
|
16
|
+
isError?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function handleEvaluateSession(args: {
|
|
19
|
+
session_id?: string;
|
|
20
|
+
}): Promise<ToolResponse>;
|
|
21
|
+
export declare function handleGeneratePRReport(args: {
|
|
22
|
+
branch?: string;
|
|
23
|
+
post?: boolean;
|
|
24
|
+
}): Promise<ToolResponse>;
|
|
25
|
+
export declare function handleGetStatus(_args: {}): Promise<ToolResponse>;
|
|
26
|
+
export declare function handleConfigure(args: {
|
|
27
|
+
get?: string;
|
|
28
|
+
set?: Record<string, unknown>;
|
|
29
|
+
}): Promise<ToolResponse>;
|
|
30
|
+
export {};
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool handlers for the standalone PromptUp plugin.
|
|
3
|
+
*
|
|
4
|
+
* Three tools:
|
|
5
|
+
* - evaluate_session — evaluate a coding session across 11 skill dimensions
|
|
6
|
+
* - generate_pr_report — generate a DQS report for a git branch
|
|
7
|
+
* - get_status — show tracking status and recent activity
|
|
8
|
+
*
|
|
9
|
+
* STANDALONE — no imports from @promptup/shared or workspace packages.
|
|
10
|
+
*/
|
|
11
|
+
import { evaluateSession } from './evaluator.js';
|
|
12
|
+
import { generatePRReport } from './pr-report-generator.js';
|
|
13
|
+
import { parseTranscript, findLatestTranscript } from './transcript-parser.js';
|
|
14
|
+
import { extractAndStoreGitActivity } from './git-activity-extractor.js';
|
|
15
|
+
import * as db from './db.js';
|
|
16
|
+
import { ulid } from 'ulid';
|
|
17
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { loadConfig, updateConfig } from './config.js';
|
|
21
|
+
function textResponse(text, isError = false) {
|
|
22
|
+
return { content: [{ type: 'text', text }], ...(isError ? { isError } : {}) };
|
|
23
|
+
}
|
|
24
|
+
function progressBar(score, width = 20) {
|
|
25
|
+
const filled = Math.round((score / 100) * width);
|
|
26
|
+
const block = score >= 70 ? '🟩' : score >= 40 ? '🟨' : '🟥';
|
|
27
|
+
return block.repeat(filled) + '⬜'.repeat(width - filled);
|
|
28
|
+
}
|
|
29
|
+
/** Truncate reasoning to fit in a table cell */
|
|
30
|
+
function truncReasoning(reasoning, max = 60) {
|
|
31
|
+
if (!reasoning)
|
|
32
|
+
return '';
|
|
33
|
+
// Take first sentence or truncate
|
|
34
|
+
const firstSentence = reasoning.split(/\.\s/)[0];
|
|
35
|
+
const text = firstSentence.length <= max ? firstSentence : firstSentence.slice(0, max - 1) + '…';
|
|
36
|
+
return text.replace(/\|/g, '/'); // escape pipe for markdown tables
|
|
37
|
+
}
|
|
38
|
+
const DECISION_ICONS = {
|
|
39
|
+
steer: '🔀', reject: '🚫', validate: '✅',
|
|
40
|
+
modify: '✏️', scope: '📐', accept: '👍',
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Try to locate the session-end.json file that Claude Code writes when a
|
|
44
|
+
* session completes. Contains transcript_path and session metadata.
|
|
45
|
+
*/
|
|
46
|
+
function readSessionEndJson() {
|
|
47
|
+
try {
|
|
48
|
+
const candidates = [
|
|
49
|
+
join(homedir(), '.claude', 'session-end.json'),
|
|
50
|
+
join(process.cwd(), '.claude', 'session-end.json'),
|
|
51
|
+
];
|
|
52
|
+
for (const p of candidates) {
|
|
53
|
+
if (existsSync(p)) {
|
|
54
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Ignore read/parse errors
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse tool-events.jsonl to extract Bash tool events containing git commands.
|
|
65
|
+
* This captures git activity from the current MCP session context.
|
|
66
|
+
*/
|
|
67
|
+
function parseToolEventsForGit(sessionId) {
|
|
68
|
+
const candidates = [
|
|
69
|
+
join(homedir(), '.claude', 'tool-events.jsonl'),
|
|
70
|
+
join(process.cwd(), '.claude', 'tool-events.jsonl'),
|
|
71
|
+
];
|
|
72
|
+
for (const eventsPath of candidates) {
|
|
73
|
+
if (!existsSync(eventsPath))
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const raw = readFileSync(eventsPath, 'utf-8');
|
|
77
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
78
|
+
const messages = [];
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
try {
|
|
81
|
+
const event = JSON.parse(lines[i]);
|
|
82
|
+
if (event.type !== 'tool_use' && event.type !== 'assistant')
|
|
83
|
+
continue;
|
|
84
|
+
// Build a minimal MessageRow with tool_uses to feed into the git extractor
|
|
85
|
+
const toolUses = [];
|
|
86
|
+
if (event.name === 'Bash' && event.input && typeof event.input === 'object') {
|
|
87
|
+
toolUses.push({ name: 'Bash', input: event.input });
|
|
88
|
+
}
|
|
89
|
+
else if (event.content && Array.isArray(event.content)) {
|
|
90
|
+
for (const block of event.content) {
|
|
91
|
+
if (block.type === 'tool_use' && block.name === 'Bash' && block.input) {
|
|
92
|
+
toolUses.push({ name: 'Bash', input: block.input });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (toolUses.length > 0) {
|
|
97
|
+
messages.push({
|
|
98
|
+
id: ulid(),
|
|
99
|
+
session_id: sessionId,
|
|
100
|
+
role: 'assistant',
|
|
101
|
+
content: '',
|
|
102
|
+
tool_uses: JSON.stringify(toolUses),
|
|
103
|
+
sequence_number: i,
|
|
104
|
+
tokens_in: 0,
|
|
105
|
+
tokens_out: 0,
|
|
106
|
+
model: null,
|
|
107
|
+
created_at: event.timestamp ?? new Date().toISOString(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Skip malformed lines
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (messages.length > 0) {
|
|
116
|
+
extractAndStoreGitActivity(messages, sessionId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Ignore file-level errors
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ─── evaluate_session ────────────────────────────────────────────────────────
|
|
125
|
+
export async function handleEvaluateSession(args) {
|
|
126
|
+
try {
|
|
127
|
+
let transcriptPath = null;
|
|
128
|
+
let sessionId = args.session_id ?? null;
|
|
129
|
+
let messages = null;
|
|
130
|
+
// Strategy 1: Check session-end.json for transcript_path
|
|
131
|
+
const sessionEnd = readSessionEndJson();
|
|
132
|
+
if (sessionEnd?.transcript_path && typeof sessionEnd.transcript_path === 'string') {
|
|
133
|
+
transcriptPath = sessionEnd.transcript_path;
|
|
134
|
+
}
|
|
135
|
+
// Strategy 2: Find the latest transcript file
|
|
136
|
+
if (!transcriptPath) {
|
|
137
|
+
transcriptPath = findLatestTranscript();
|
|
138
|
+
}
|
|
139
|
+
// Strategy 3: If we have a session_id, check if messages are already in DB
|
|
140
|
+
if (!transcriptPath && sessionId) {
|
|
141
|
+
const session = db.getSession(sessionId);
|
|
142
|
+
if (session?.transcript_path && existsSync(session.transcript_path)) {
|
|
143
|
+
transcriptPath = session.transcript_path;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!transcriptPath) {
|
|
147
|
+
return textResponse('No transcript found. Ensure Claude Code is running and has an active session, ' +
|
|
148
|
+
'or provide a session_id for a previously tracked session.\n\n' +
|
|
149
|
+
'Transcripts are expected at ~/.claude/projects/<hash>/<session>.jsonl', true);
|
|
150
|
+
}
|
|
151
|
+
// Parse transcript
|
|
152
|
+
messages = parseTranscript(transcriptPath);
|
|
153
|
+
if (messages.length === 0) {
|
|
154
|
+
return textResponse(`Transcript at ${transcriptPath} contains no user/assistant messages.`, true);
|
|
155
|
+
}
|
|
156
|
+
// Derive session ID from transcript if not provided
|
|
157
|
+
if (!sessionId) {
|
|
158
|
+
sessionId = messages[0].session_id;
|
|
159
|
+
}
|
|
160
|
+
// Create or find session in DB
|
|
161
|
+
let session = db.getSession(sessionId);
|
|
162
|
+
if (!session) {
|
|
163
|
+
const now = new Date().toISOString();
|
|
164
|
+
const firstMsg = messages[0];
|
|
165
|
+
const lastMsg = messages[messages.length - 1];
|
|
166
|
+
db.insertSession({
|
|
167
|
+
id: sessionId,
|
|
168
|
+
project_path: process.cwd(),
|
|
169
|
+
transcript_path: transcriptPath,
|
|
170
|
+
status: 'completed',
|
|
171
|
+
message_count: messages.length,
|
|
172
|
+
started_at: firstMsg.created_at,
|
|
173
|
+
ended_at: lastMsg.created_at,
|
|
174
|
+
created_at: now,
|
|
175
|
+
});
|
|
176
|
+
session = db.getSession(sessionId);
|
|
177
|
+
}
|
|
178
|
+
// Persist messages so decision detection + PR reports can access them
|
|
179
|
+
db.insertMessages(messages);
|
|
180
|
+
// Run evaluation
|
|
181
|
+
const evaluation = await evaluateSession(sessionId, messages, 'manual');
|
|
182
|
+
if (!evaluation) {
|
|
183
|
+
return textResponse('Evaluation failed. Claude Code CLI may not be available, or the session is too short ' +
|
|
184
|
+
'(minimum 3 messages required).', true);
|
|
185
|
+
}
|
|
186
|
+
// Parse eval data
|
|
187
|
+
const dimScores = JSON.parse(evaluation.dimension_scores);
|
|
188
|
+
const recommendations = evaluation.recommendations
|
|
189
|
+
? JSON.parse(evaluation.recommendations)
|
|
190
|
+
: [];
|
|
191
|
+
const trends = evaluation.trends
|
|
192
|
+
? JSON.parse(evaluation.trends)
|
|
193
|
+
: [];
|
|
194
|
+
const decisions = db.getDecisionsBySession(sessionId);
|
|
195
|
+
// Count real developer prompts vs Claude responses
|
|
196
|
+
const userCount = messages.filter(m => m.role === 'user').length;
|
|
197
|
+
const assistantCount = messages.filter(m => m.role === 'assistant').length;
|
|
198
|
+
const cls = evaluation.composite_score <= 40 ? 'Junior' : evaluation.composite_score <= 70 ? 'Middle' : 'Senior';
|
|
199
|
+
// Build trend map for dimension arrows
|
|
200
|
+
const trendMap = new Map();
|
|
201
|
+
for (const t of trends) {
|
|
202
|
+
if (t.direction === 'improving')
|
|
203
|
+
trendMap.set(t.dimension_key, ` ▲+${Math.abs(t.delta)}`);
|
|
204
|
+
else if (t.direction === 'declining')
|
|
205
|
+
trendMap.set(t.dimension_key, ` ▼${t.delta}`);
|
|
206
|
+
}
|
|
207
|
+
// ── Build markdown ──
|
|
208
|
+
const lines = [
|
|
209
|
+
'## Session Evaluation',
|
|
210
|
+
'',
|
|
211
|
+
`### Composite Score: ${evaluation.composite_score}/100 — **${cls}**`,
|
|
212
|
+
'',
|
|
213
|
+
progressBar(evaluation.composite_score, 20),
|
|
214
|
+
'',
|
|
215
|
+
'| Dimension | Score | Why |',
|
|
216
|
+
'|-----------|-------|-----|',
|
|
217
|
+
];
|
|
218
|
+
for (const dim of dimScores) {
|
|
219
|
+
const label = dim.key.replace(/_/g, ' ');
|
|
220
|
+
const bar = progressBar(dim.score, 8);
|
|
221
|
+
const trend = trendMap.get(dim.key) ?? '';
|
|
222
|
+
const why = truncReasoning(dim.reasoning);
|
|
223
|
+
lines.push(`| ${label} | ${bar} ${dim.score}${trend} | ${why} |`);
|
|
224
|
+
}
|
|
225
|
+
lines.push('', `Developer prompts: **${userCount}** | Claude responses: **${assistantCount}**`);
|
|
226
|
+
// Decisions
|
|
227
|
+
if (decisions.length > 0) {
|
|
228
|
+
const high = decisions.filter(d => d.signal === 'high');
|
|
229
|
+
const medium = decisions.filter(d => d.signal === 'medium');
|
|
230
|
+
const low = decisions.filter(d => d.signal === 'low');
|
|
231
|
+
lines.push('', '### Decisions', '');
|
|
232
|
+
for (const d of high) {
|
|
233
|
+
lines.push(`${DECISION_ICONS[d.type] ?? '•'} **${d.context}**`);
|
|
234
|
+
}
|
|
235
|
+
for (const d of medium) {
|
|
236
|
+
lines.push(`${DECISION_ICONS[d.type] ?? '•'} ${d.context}`);
|
|
237
|
+
}
|
|
238
|
+
if (low.length > 0) {
|
|
239
|
+
lines.push('', `*+ ${low.length} routine decision${low.length > 1 ? 's' : ''} not shown*`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Recommendations
|
|
243
|
+
if (recommendations.length > 0) {
|
|
244
|
+
lines.push('', '### Recommendations', '');
|
|
245
|
+
for (const rec of recommendations) {
|
|
246
|
+
const badge = rec.priority === 'high' ? '🔴' : rec.priority === 'medium' ? '🟡' : '🟢';
|
|
247
|
+
lines.push(`${badge} **${rec.recommendation}**`);
|
|
248
|
+
if (rec.suggestions && rec.suggestions.length > 0) {
|
|
249
|
+
for (const s of rec.suggestions) {
|
|
250
|
+
lines.push(` - ${s}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return textResponse(lines.join('\n'));
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
259
|
+
return textResponse(`Evaluation error: ${msg}`, true);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// ─── generate_pr_report ──────────────────────────────────────────────────────
|
|
263
|
+
export async function handleGeneratePRReport(args) {
|
|
264
|
+
try {
|
|
265
|
+
// Extract git activity from tool-events.jsonl if available
|
|
266
|
+
const tempSessionId = ulid();
|
|
267
|
+
parseToolEventsForGit(tempSessionId);
|
|
268
|
+
// Backfill messages for sessions that have transcript_path but no stored messages
|
|
269
|
+
// (e.g. sessions created by eval before this fix, or daemon-created sessions)
|
|
270
|
+
const recentSessions = db.getRecentSessions(20);
|
|
271
|
+
for (const s of recentSessions) {
|
|
272
|
+
if (s.transcript_path) {
|
|
273
|
+
const existing = db.getMessagesBySession(s.id, 1, 0);
|
|
274
|
+
if (existing.length === 0) {
|
|
275
|
+
try {
|
|
276
|
+
const msgs = parseTranscript(s.transcript_path);
|
|
277
|
+
if (msgs.length > 0) {
|
|
278
|
+
// Rewrite session_id to match the DB session
|
|
279
|
+
for (const m of msgs)
|
|
280
|
+
m.session_id = s.id;
|
|
281
|
+
db.insertMessages(msgs);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch { /* transcript may no longer exist */ }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Generate the report
|
|
289
|
+
const result = await generatePRReport({
|
|
290
|
+
branch: args.branch,
|
|
291
|
+
post: args.post,
|
|
292
|
+
projectPath: process.cwd(),
|
|
293
|
+
});
|
|
294
|
+
const { report, isNew } = result;
|
|
295
|
+
let text = '';
|
|
296
|
+
if (!isNew) {
|
|
297
|
+
text += `*Cached report found (generated ${report.created_at})*\n\n`;
|
|
298
|
+
}
|
|
299
|
+
text += report.markdown;
|
|
300
|
+
text += '\n\n---\n';
|
|
301
|
+
text += `**Report ID:** ${report.id}\n`;
|
|
302
|
+
text += `**Branch:** ${report.branch}\n`;
|
|
303
|
+
if (report.repo)
|
|
304
|
+
text += `**Repo:** ${report.repo}\n`;
|
|
305
|
+
if (report.pr_url)
|
|
306
|
+
text += `**PR:** ${report.pr_url}\n`;
|
|
307
|
+
text += `**DQS:** ${report.dqs !== null ? `${report.dqs}/100` : 'N/A'}\n`;
|
|
308
|
+
text += `**Decisions:** ${report.total_decisions}\n`;
|
|
309
|
+
if (report.posted_at)
|
|
310
|
+
text += `**Posted to PR:** ${report.posted_at}\n`;
|
|
311
|
+
return textResponse(text);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
315
|
+
return textResponse(`PR report error: ${msg}`, true);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// ─── get_status ──────────────────────────────────────────────────────────────
|
|
319
|
+
export async function handleGetStatus(_args) {
|
|
320
|
+
try {
|
|
321
|
+
const stats = db.getStats();
|
|
322
|
+
const sessionEnd = readSessionEndJson();
|
|
323
|
+
const latestEval = db.getLatestEvaluation();
|
|
324
|
+
const recentSessions = db.getRecentSessions(5);
|
|
325
|
+
let text = `## PromptUp Status\n\n`;
|
|
326
|
+
// Overall stats
|
|
327
|
+
text += `### Database\n`;
|
|
328
|
+
text += `- **Sessions tracked:** ${stats.sessions}\n`;
|
|
329
|
+
text += `- **Evaluations:** ${stats.evaluations}\n`;
|
|
330
|
+
text += `- **Decisions captured:** ${stats.decisions}\n\n`;
|
|
331
|
+
// Current session info from session-end.json
|
|
332
|
+
if (sessionEnd) {
|
|
333
|
+
text += `### Current Session\n`;
|
|
334
|
+
if (sessionEnd.session_id)
|
|
335
|
+
text += `- **Session ID:** ${sessionEnd.session_id}\n`;
|
|
336
|
+
if (sessionEnd.transcript_path)
|
|
337
|
+
text += `- **Transcript:** ${sessionEnd.transcript_path}\n`;
|
|
338
|
+
if (sessionEnd.project_path)
|
|
339
|
+
text += `- **Project:** ${sessionEnd.project_path}\n`;
|
|
340
|
+
text += '\n';
|
|
341
|
+
}
|
|
342
|
+
// Latest evaluation
|
|
343
|
+
if (latestEval) {
|
|
344
|
+
const dimScores = JSON.parse(latestEval.dimension_scores);
|
|
345
|
+
text += `### Latest Evaluation\n`;
|
|
346
|
+
text += `- **Session:** ${latestEval.session_id}\n`;
|
|
347
|
+
text += `- **Composite Score:** ${latestEval.composite_score}/100\n`;
|
|
348
|
+
text += `- **Type:** ${latestEval.trigger_type} (${latestEval.report_type})\n`;
|
|
349
|
+
text += `- **Date:** ${latestEval.created_at}\n`;
|
|
350
|
+
text += `- **Dimensions:** ${dimScores.map(d => `${d.key}=${d.score}`).join(', ')}\n\n`;
|
|
351
|
+
}
|
|
352
|
+
// Recent sessions
|
|
353
|
+
if (recentSessions.length > 0) {
|
|
354
|
+
text += `### Recent Sessions\n`;
|
|
355
|
+
for (const s of recentSessions) {
|
|
356
|
+
const status = s.status === 'active' ? '[ACTIVE]' : '[DONE]';
|
|
357
|
+
text += `- ${status} ${s.id} | ${s.message_count} msgs | ${s.started_at}\n`;
|
|
358
|
+
}
|
|
359
|
+
text += '\n';
|
|
360
|
+
}
|
|
361
|
+
if (stats.sessions === 0) {
|
|
362
|
+
text += `*No sessions tracked yet. Use \`evaluate_session\` after completing a coding session, `;
|
|
363
|
+
text += `or \`generate_pr_report\` to analyze decisions on a branch.*\n`;
|
|
364
|
+
}
|
|
365
|
+
return textResponse(text);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
369
|
+
return textResponse(`Status error: ${msg}`, true);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// ─── configure ──────────────────────────────────────────────────────────────
|
|
373
|
+
export async function handleConfigure(args) {
|
|
374
|
+
try {
|
|
375
|
+
// If setting values, apply them first
|
|
376
|
+
if (args.set && Object.keys(args.set).length > 0) {
|
|
377
|
+
const updated = updateConfig(args.set);
|
|
378
|
+
const lines = ['## PromptUp Config Updated', ''];
|
|
379
|
+
for (const [key, value] of Object.entries(args.set)) {
|
|
380
|
+
lines.push(`**${key}** → \`${JSON.stringify(value)}\``);
|
|
381
|
+
}
|
|
382
|
+
lines.push('', '---', '');
|
|
383
|
+
lines.push(formatConfig(updated));
|
|
384
|
+
return textResponse(lines.join('\n'));
|
|
385
|
+
}
|
|
386
|
+
// If getting a specific value
|
|
387
|
+
if (args.get) {
|
|
388
|
+
const config = loadConfig();
|
|
389
|
+
const value = getNestedFromConfig(config, args.get);
|
|
390
|
+
return textResponse(`**${args.get}** = \`${JSON.stringify(value, null, 2)}\``);
|
|
391
|
+
}
|
|
392
|
+
// Default: show full config
|
|
393
|
+
const config = loadConfig();
|
|
394
|
+
return textResponse(formatConfig(config));
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
398
|
+
return textResponse(`Config error: ${msg}`, true);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function formatConfig(config) {
|
|
402
|
+
const lines = [
|
|
403
|
+
'## PromptUp Configuration',
|
|
404
|
+
'',
|
|
405
|
+
'### Evaluation',
|
|
406
|
+
`| Setting | Value | Options |`,
|
|
407
|
+
`|---------|-------|---------|`,
|
|
408
|
+
`| auto_trigger | \`${config.evaluation.auto_trigger}\` | off, prompt_count, session_end |`,
|
|
409
|
+
`| interval | \`${config.evaluation.interval}\` | prompts between auto-evals |`,
|
|
410
|
+
`| weight_profile | \`${config.evaluation.weight_profile}\` | balanced, greenfield, bugfix, refactor, security_review |`,
|
|
411
|
+
`| timeout_seconds | \`${config.evaluation.timeout_seconds}\` | seconds |`,
|
|
412
|
+
`| feedback_detail | \`${config.evaluation.feedback_detail}\` | brief, standard, detailed |`,
|
|
413
|
+
'',
|
|
414
|
+
'### Dimensions',
|
|
415
|
+
`| Setting | Value |`,
|
|
416
|
+
`|---------|-------|`,
|
|
417
|
+
`| enabled | \`${JSON.stringify(config.dimensions.enabled)}\` |`,
|
|
418
|
+
`| custom_weights | \`${config.dimensions.custom_weights ? 'set' : 'null (using profile)'}\` |`,
|
|
419
|
+
'',
|
|
420
|
+
'### Decisions',
|
|
421
|
+
`| Setting | Value | Options |`,
|
|
422
|
+
`|---------|-------|---------|`,
|
|
423
|
+
`| signal_filter | \`${config.decisions.signal_filter}\` | high, high+medium, all |`,
|
|
424
|
+
`| show_routine_count | \`${config.decisions.show_routine_count}\` | true/false |`,
|
|
425
|
+
'',
|
|
426
|
+
'### PR Report',
|
|
427
|
+
`| Setting | Value |`,
|
|
428
|
+
`|---------|-------|`,
|
|
429
|
+
`| auto_post | \`${config.pr_report.auto_post}\` |`,
|
|
430
|
+
`| base_branch | \`${config.pr_report.base_branch}\` |`,
|
|
431
|
+
'',
|
|
432
|
+
'### Classification',
|
|
433
|
+
`| Band | Range |`,
|
|
434
|
+
`|------|-------|`,
|
|
435
|
+
...Object.entries(config.classification.bands).map(([name, [min, max]]) => `| ${name} | ${min}-${max} |`),
|
|
436
|
+
'',
|
|
437
|
+
'### Status Line',
|
|
438
|
+
`| Setting | Value |`,
|
|
439
|
+
`|---------|-------|`,
|
|
440
|
+
`| enabled | \`${config.statusline.enabled}\` |`,
|
|
441
|
+
`| show_recommendation | \`${config.statusline.show_recommendation}\` |`,
|
|
442
|
+
'',
|
|
443
|
+
`*Config file: ~/.promptup/config.json*`,
|
|
444
|
+
];
|
|
445
|
+
return lines.join('\n');
|
|
446
|
+
}
|
|
447
|
+
function getNestedFromConfig(obj, path) {
|
|
448
|
+
const parts = path.split('.');
|
|
449
|
+
let current = obj;
|
|
450
|
+
for (const part of parts) {
|
|
451
|
+
if (current === undefined || current === null)
|
|
452
|
+
return undefined;
|
|
453
|
+
current = current[part];
|
|
454
|
+
}
|
|
455
|
+
return current;
|
|
456
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for Claude Code JSONL transcript files.
|
|
3
|
+
*
|
|
4
|
+
* Fully self-contained — no imports from @promptup/shared or any workspace package.
|
|
5
|
+
* Reads Claude Code JSONL files and produces MessageRow[] arrays.
|
|
6
|
+
*
|
|
7
|
+
* Claude Code JSONL format: each line is a JSON object with a `type` field:
|
|
8
|
+
* - "user" — user prompt (message.content: string | ContentBlock[])
|
|
9
|
+
* - "assistant" — model reply (message.content: ContentBlock[], message.usage, message.model)
|
|
10
|
+
* - "progress" — tool progress (filtered out)
|
|
11
|
+
* - "system" — system event (filtered out)
|
|
12
|
+
* - "result" — final result (filtered out)
|
|
13
|
+
* - "file_history_snapshot" — file state (filtered out)
|
|
14
|
+
*/
|
|
15
|
+
import type { MessageRow } from './shared/types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Parse a Claude Code JSONL transcript file into MessageRow[].
|
|
18
|
+
*
|
|
19
|
+
* Reads the file, splits by newlines, parses each JSON line, filters to
|
|
20
|
+
* 'user' and 'assistant' types, extracts text content, tool uses, token
|
|
21
|
+
* usage, and model name. Assigns 0-indexed sequence numbers and generates
|
|
22
|
+
* ULID-based IDs.
|
|
23
|
+
*
|
|
24
|
+
* Malformed lines are silently skipped.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseTranscript(filePath: string): MessageRow[];
|
|
27
|
+
/**
|
|
28
|
+
* Find the most recent JSONL transcript file from Claude Code's project directory.
|
|
29
|
+
* Looks in ~/.claude/projects/ for the most recently modified .jsonl file.
|
|
30
|
+
*
|
|
31
|
+
* Walks the directory tree up to 3 levels deep:
|
|
32
|
+
* ~/.claude/projects/<project-hash>/sessions/<session-id>.jsonl
|
|
33
|
+
*
|
|
34
|
+
* Returns the absolute path to the most recently modified file, or null if none found.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findLatestTranscript(): string | null;
|