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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/bin/install.cjs +306 -0
  4. package/bin/promptup-plugin +8 -0
  5. package/dist/config.d.ts +40 -0
  6. package/dist/config.js +123 -0
  7. package/dist/db.d.ts +35 -0
  8. package/dist/db.js +327 -0
  9. package/dist/decision-detector.d.ts +11 -0
  10. package/dist/decision-detector.js +47 -0
  11. package/dist/evaluator.d.ts +10 -0
  12. package/dist/evaluator.js +844 -0
  13. package/dist/git-activity-extractor.d.ts +35 -0
  14. package/dist/git-activity-extractor.js +167 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +54 -0
  17. package/dist/pr-report-generator.d.ts +20 -0
  18. package/dist/pr-report-generator.js +421 -0
  19. package/dist/shared/decision-classifier.d.ts +60 -0
  20. package/dist/shared/decision-classifier.js +385 -0
  21. package/dist/shared/decision-score.d.ts +7 -0
  22. package/dist/shared/decision-score.js +31 -0
  23. package/dist/shared/dimensions.d.ts +43 -0
  24. package/dist/shared/dimensions.js +361 -0
  25. package/dist/shared/scoring.d.ts +89 -0
  26. package/dist/shared/scoring.js +161 -0
  27. package/dist/shared/types.d.ts +108 -0
  28. package/dist/shared/types.js +9 -0
  29. package/dist/tools.d.ts +30 -0
  30. package/dist/tools.js +456 -0
  31. package/dist/transcript-parser.d.ts +36 -0
  32. package/dist/transcript-parser.js +201 -0
  33. package/hooks/auto-eval.sh +44 -0
  34. package/hooks/check-update.sh +26 -0
  35. package/hooks/debug-hook.sh +3 -0
  36. package/hooks/hooks.json +36 -0
  37. package/hooks/render-eval.sh +137 -0
  38. package/package.json +60 -0
  39. package/skills/eval/SKILL.md +12 -0
  40. package/skills/pr-report/SKILL.md +37 -0
  41. package/skills/status/SKILL.md +28 -0
  42. 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
+ }
@@ -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;