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,201 @@
|
|
|
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 { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
16
|
+
import { join, basename } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { ulid } from 'ulid';
|
|
19
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
20
|
+
function isTextBlock(block) {
|
|
21
|
+
return block.type === 'text' && typeof block.text === 'string';
|
|
22
|
+
}
|
|
23
|
+
function isToolUseBlock(block) {
|
|
24
|
+
return block.type === 'tool_use' && typeof block.name === 'string';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extract human-readable text from a message's content field.
|
|
28
|
+
*/
|
|
29
|
+
function extractText(content) {
|
|
30
|
+
if (typeof content === 'string')
|
|
31
|
+
return content;
|
|
32
|
+
if (!Array.isArray(content))
|
|
33
|
+
return '';
|
|
34
|
+
const parts = [];
|
|
35
|
+
for (const block of content) {
|
|
36
|
+
if (isTextBlock(block)) {
|
|
37
|
+
parts.push(block.text);
|
|
38
|
+
}
|
|
39
|
+
else if (block.type === 'tool_result') {
|
|
40
|
+
const tr = block;
|
|
41
|
+
if (typeof tr.content === 'string') {
|
|
42
|
+
parts.push(`[tool_result] ${tr.content}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return parts.join('\n');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extract tool_use blocks from content, returning a JSON-serializable array.
|
|
50
|
+
*/
|
|
51
|
+
function extractToolUses(content) {
|
|
52
|
+
if (!Array.isArray(content))
|
|
53
|
+
return null;
|
|
54
|
+
const tools = [];
|
|
55
|
+
for (const block of content) {
|
|
56
|
+
if (isToolUseBlock(block)) {
|
|
57
|
+
tools.push({ name: block.name, input: block.input });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return tools.length > 0 ? JSON.stringify(tools) : null;
|
|
61
|
+
}
|
|
62
|
+
// ─── Main parser ─────────────────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Parse a Claude Code JSONL transcript file into MessageRow[].
|
|
65
|
+
*
|
|
66
|
+
* Reads the file, splits by newlines, parses each JSON line, filters to
|
|
67
|
+
* 'user' and 'assistant' types, extracts text content, tool uses, token
|
|
68
|
+
* usage, and model name. Assigns 0-indexed sequence numbers and generates
|
|
69
|
+
* ULID-based IDs.
|
|
70
|
+
*
|
|
71
|
+
* Malformed lines are silently skipped.
|
|
72
|
+
*/
|
|
73
|
+
export function parseTranscript(filePath) {
|
|
74
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
75
|
+
const lines = raw.split('\n');
|
|
76
|
+
const messages = [];
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (!trimmed)
|
|
80
|
+
continue;
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(trimmed);
|
|
83
|
+
const type = parsed.type;
|
|
84
|
+
if (type !== 'user' && type !== 'assistant')
|
|
85
|
+
continue;
|
|
86
|
+
if (!parsed.message || typeof parsed.message !== 'object')
|
|
87
|
+
continue;
|
|
88
|
+
messages.push(parsed);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Skip malformed lines
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Derive a session ID from the filename (strip .jsonl extension)
|
|
95
|
+
const sessionId = basename(filePath, '.jsonl');
|
|
96
|
+
const rows = [];
|
|
97
|
+
for (let i = 0; i < messages.length; i++) {
|
|
98
|
+
const msg = messages[i];
|
|
99
|
+
const id = ulid();
|
|
100
|
+
let role = msg.type;
|
|
101
|
+
const timestamp = msg.timestamp ?? new Date().toISOString();
|
|
102
|
+
let content;
|
|
103
|
+
let toolUses = null;
|
|
104
|
+
let tokensIn = 0;
|
|
105
|
+
let tokensOut = 0;
|
|
106
|
+
let model = null;
|
|
107
|
+
if (msg.type === 'user') {
|
|
108
|
+
content = extractText(msg.message.content);
|
|
109
|
+
// Tool result messages have content blocks that are all tool_result type.
|
|
110
|
+
// Keep them (they're useful for decision detection) but mark role as 'tool_result'
|
|
111
|
+
// so the report can distinguish developer prompts from tool outputs.
|
|
112
|
+
const msgContent = msg.message.content;
|
|
113
|
+
if (Array.isArray(msgContent) && msgContent.length > 0 && msgContent.every(b => b.type === 'tool_result')) {
|
|
114
|
+
role = 'tool_result';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// assistant
|
|
119
|
+
const assistantContent = msg.message.content;
|
|
120
|
+
content = extractText(assistantContent);
|
|
121
|
+
toolUses = extractToolUses(assistantContent);
|
|
122
|
+
if (msg.message.usage) {
|
|
123
|
+
tokensIn = msg.message.usage.input_tokens ?? 0;
|
|
124
|
+
tokensOut = msg.message.usage.output_tokens ?? 0;
|
|
125
|
+
}
|
|
126
|
+
model = msg.message.model ?? null;
|
|
127
|
+
}
|
|
128
|
+
rows.push({
|
|
129
|
+
id,
|
|
130
|
+
session_id: sessionId,
|
|
131
|
+
role,
|
|
132
|
+
content,
|
|
133
|
+
tool_uses: toolUses,
|
|
134
|
+
sequence_number: i,
|
|
135
|
+
tokens_in: tokensIn,
|
|
136
|
+
tokens_out: tokensOut,
|
|
137
|
+
model,
|
|
138
|
+
created_at: timestamp,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return rows;
|
|
142
|
+
}
|
|
143
|
+
// ─── Find latest transcript ──────────────────────────────────────────────────
|
|
144
|
+
/**
|
|
145
|
+
* Find the most recent JSONL transcript file from Claude Code's project directory.
|
|
146
|
+
* Looks in ~/.claude/projects/ for the most recently modified .jsonl file.
|
|
147
|
+
*
|
|
148
|
+
* Walks the directory tree up to 3 levels deep:
|
|
149
|
+
* ~/.claude/projects/<project-hash>/sessions/<session-id>.jsonl
|
|
150
|
+
*
|
|
151
|
+
* Returns the absolute path to the most recently modified file, or null if none found.
|
|
152
|
+
*/
|
|
153
|
+
export function findLatestTranscript() {
|
|
154
|
+
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
155
|
+
if (!existsSync(projectsDir))
|
|
156
|
+
return null;
|
|
157
|
+
let latestPath = null;
|
|
158
|
+
let latestMtime = 0;
|
|
159
|
+
try {
|
|
160
|
+
const projectDirs = readdirSync(projectsDir, { withFileTypes: true });
|
|
161
|
+
for (const projectEntry of projectDirs) {
|
|
162
|
+
if (!projectEntry.isDirectory())
|
|
163
|
+
continue;
|
|
164
|
+
const projectPath = join(projectsDir, projectEntry.name);
|
|
165
|
+
// Look for .jsonl files directly in the project dir
|
|
166
|
+
scanForJsonl(projectPath);
|
|
167
|
+
// Look in a sessions/ subdirectory
|
|
168
|
+
const sessionsDir = join(projectPath, 'sessions');
|
|
169
|
+
if (existsSync(sessionsDir)) {
|
|
170
|
+
scanForJsonl(sessionsDir);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Permissions error or similar — return what we have
|
|
176
|
+
}
|
|
177
|
+
function scanForJsonl(dir) {
|
|
178
|
+
try {
|
|
179
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
|
|
182
|
+
continue;
|
|
183
|
+
const fullPath = join(dir, entry.name);
|
|
184
|
+
try {
|
|
185
|
+
const stat = statSync(fullPath);
|
|
186
|
+
if (stat.mtimeMs > latestMtime) {
|
|
187
|
+
latestMtime = stat.mtimeMs;
|
|
188
|
+
latestPath = fullPath;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Skip files we can't stat
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Skip directories we can't read
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return latestPath;
|
|
201
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# UserPromptSubmit hook: counts prompts, triggers background eval per config
|
|
3
|
+
PLUGIN_DIR="${CLAUDE_PLUGIN_ROOT:-$HOME/.promptup/plugin}"
|
|
4
|
+
DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}"
|
|
5
|
+
COUNT_FILE="$DATA_DIR/prompt-count"
|
|
6
|
+
CONFIG_FILE="$DATA_DIR/config.json"
|
|
7
|
+
|
|
8
|
+
mkdir -p "$DATA_DIR"
|
|
9
|
+
|
|
10
|
+
# Read config — check if auto_trigger is enabled
|
|
11
|
+
TRIGGER=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('evaluation',{}).get('auto_trigger','off'))" 2>/dev/null || echo "off")
|
|
12
|
+
if [ "$TRIGGER" = "off" ]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
INTERVAL=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('evaluation',{}).get('interval',10))" 2>/dev/null || echo "10")
|
|
17
|
+
|
|
18
|
+
COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0)
|
|
19
|
+
COUNT=$((COUNT + 1))
|
|
20
|
+
echo $COUNT > "$COUNT_FILE"
|
|
21
|
+
|
|
22
|
+
if [ "$TRIGGER" = "prompt_count" ] && [ "$COUNT" -ge "$INTERVAL" ]; then
|
|
23
|
+
echo 0 > "$COUNT_FILE"
|
|
24
|
+
nohup node -e "
|
|
25
|
+
import { initDatabase, insertSession, insertMessages, getSession } from '$PLUGIN_DIR/dist/db.js';
|
|
26
|
+
import { parseTranscript, findLatestTranscript } from '$PLUGIN_DIR/dist/transcript-parser.js';
|
|
27
|
+
import { evaluateSession } from '$PLUGIN_DIR/dist/evaluator.js';
|
|
28
|
+
import { ulid } from 'ulid';
|
|
29
|
+
initDatabase();
|
|
30
|
+
const tp = findLatestTranscript();
|
|
31
|
+
if (!tp) process.exit(0);
|
|
32
|
+
const msgs = parseTranscript(tp);
|
|
33
|
+
if (msgs.length < 3) process.exit(0);
|
|
34
|
+
const sid = msgs[0]?.session_id || ulid();
|
|
35
|
+
if (!getSession(sid)) {
|
|
36
|
+
insertSession({ id: sid, project_path: process.cwd(), transcript_path: tp, status: 'active', message_count: msgs.length, started_at: msgs[0].created_at, ended_at: msgs[msgs.length-1].created_at, created_at: new Date().toISOString() });
|
|
37
|
+
}
|
|
38
|
+
for (const m of msgs) m.session_id = sid;
|
|
39
|
+
insertMessages(msgs);
|
|
40
|
+
await evaluateSession(sid, msgs, 'prompt_count');
|
|
41
|
+
" > /dev/null 2>&1 &
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SessionStart hook: checks for plugin update in background
|
|
3
|
+
PLUGIN_DIR="${CLAUDE_PLUGIN_ROOT:-$HOME/.promptup/plugin}"
|
|
4
|
+
DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}"
|
|
5
|
+
UPDATE_FILE="$DATA_DIR/update-available"
|
|
6
|
+
|
|
7
|
+
mkdir -p "$DATA_DIR"
|
|
8
|
+
|
|
9
|
+
# Get local version
|
|
10
|
+
LOCAL_VER=$(node -e "console.log(require('$PLUGIN_DIR/package.json').version)" 2>/dev/null || echo "0.0.0")
|
|
11
|
+
|
|
12
|
+
# Check remote version (npm registry)
|
|
13
|
+
REMOTE_VER=$(npm view @promptup/plugin version 2>/dev/null || echo "")
|
|
14
|
+
|
|
15
|
+
if [ -z "$REMOTE_VER" ]; then
|
|
16
|
+
# Not published yet or no network — try GitHub
|
|
17
|
+
REMOTE_VER=$(curl -sf "https://raw.githubusercontent.com/promptup/claude-plugin/main/package.json" 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(d).version)}catch{}})" 2>/dev/null || echo "")
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
if [ -n "$REMOTE_VER" ] && [ "$REMOTE_VER" != "$LOCAL_VER" ]; then
|
|
21
|
+
echo "$REMOTE_VER" > "$UPDATE_FILE"
|
|
22
|
+
else
|
|
23
|
+
rm -f "$UPDATE_FILE"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
exit 0
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "cat >> \"${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}/tool-events.jsonl\"",
|
|
9
|
+
"async": true
|
|
10
|
+
}]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"matcher": "mcp__promptup__evaluate_session",
|
|
14
|
+
"hooks": [{
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT:-$HOME/.promptup/plugin}/hooks/render-eval.sh\""
|
|
17
|
+
}]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"matcher": "mcp__promptup__generate_pr_report",
|
|
21
|
+
"hooks": [{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT:-$HOME/.promptup/plugin}/hooks/render-eval.sh\""
|
|
24
|
+
}]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"Stop": [{
|
|
28
|
+
"matcher": "",
|
|
29
|
+
"hooks": [{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "cat > \"${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}/session-end.json\"",
|
|
32
|
+
"async": true
|
|
33
|
+
}]
|
|
34
|
+
}]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PostToolUse hook: renders eval results as a formatted table directly to stderr
|
|
3
|
+
# Triggered after mcp__promptup__evaluate_session completes
|
|
4
|
+
# Reads hook JSON from stdin, extracts the tool output text, renders table
|
|
5
|
+
|
|
6
|
+
INPUT=$(cat)
|
|
7
|
+
|
|
8
|
+
# Extract the tool output text from the hook JSON
|
|
9
|
+
# The hook input has: { tool_name, tool_input, tool_output: { content: [{ text: "..." }] } }
|
|
10
|
+
TEXT=$(echo "$INPUT" | jq -r '.tool_output.content[0].text // empty' 2>/dev/null)
|
|
11
|
+
|
|
12
|
+
if [ -z "$TEXT" ]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Parse the markdown table from the eval output and render it with box drawing
|
|
17
|
+
# Extract lines between the table header and the next blank line
|
|
18
|
+
echo "$TEXT" | awk '
|
|
19
|
+
BEGIN {
|
|
20
|
+
# Colors
|
|
21
|
+
RED="\033[31m"
|
|
22
|
+
YEL="\033[33m"
|
|
23
|
+
GRN="\033[32m"
|
|
24
|
+
BLD="\033[1m"
|
|
25
|
+
DIM="\033[2m"
|
|
26
|
+
RST="\033[0m"
|
|
27
|
+
found_header=0
|
|
28
|
+
found_table=0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Hero composite score line
|
|
32
|
+
/^### Composite Score:/ {
|
|
33
|
+
gsub(/^### /, "")
|
|
34
|
+
gsub(/\*\*/, "")
|
|
35
|
+
printf "\n" > "/dev/stderr"
|
|
36
|
+
printf " %s%s%s\n", BLD, $0, RST > "/dev/stderr"
|
|
37
|
+
next
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Progress bar line (emoji squares)
|
|
41
|
+
/^🟩|^🟨|^🟥/ {
|
|
42
|
+
printf " %s\n", $0 > "/dev/stderr"
|
|
43
|
+
printf "\n" > "/dev/stderr"
|
|
44
|
+
next
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Table header
|
|
48
|
+
/^\| Dimension \| Score \| Why \|/ {
|
|
49
|
+
printf " %s┌───────────────────────────┬──────────────────────────┬────────────────────────────────────────────────────────────────┐%s\n", DIM, RST > "/dev/stderr"
|
|
50
|
+
printf " %s│ %-25s │ %-24s │ %-62s │%s\n", BLD, "Dimension", "Score", "Why", RST > "/dev/stderr"
|
|
51
|
+
printf " %s├───────────────────────────┼──────────────────────────┼────────────────────────────────────────────────────────────────┤%s\n", DIM, RST > "/dev/stderr"
|
|
52
|
+
found_table=1
|
|
53
|
+
next
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Table separator (skip)
|
|
57
|
+
/^\|---/ { next }
|
|
58
|
+
|
|
59
|
+
# Table rows
|
|
60
|
+
found_table && /^\|/ {
|
|
61
|
+
# Split by |
|
|
62
|
+
n = split($0, cols, "|")
|
|
63
|
+
if (n >= 4) {
|
|
64
|
+
dim = cols[2]
|
|
65
|
+
score_col = cols[3]
|
|
66
|
+
why = cols[4]
|
|
67
|
+
|
|
68
|
+
# Trim whitespace
|
|
69
|
+
gsub(/^[ \t]+|[ \t]+$/, "", dim)
|
|
70
|
+
gsub(/^[ \t]+|[ \t]+$/, "", score_col)
|
|
71
|
+
gsub(/^[ \t]+|[ \t]+$/, "", why)
|
|
72
|
+
|
|
73
|
+
# Extract numeric score from score column
|
|
74
|
+
match(score_col, /[0-9]+/)
|
|
75
|
+
score = substr(score_col, RSTART, RLENGTH) + 0
|
|
76
|
+
|
|
77
|
+
# Color based on score
|
|
78
|
+
if (score >= 70) color = GRN
|
|
79
|
+
else if (score >= 40) color = YEL
|
|
80
|
+
else color = RED
|
|
81
|
+
|
|
82
|
+
# Truncate why to fit
|
|
83
|
+
if (length(why) > 62) why = substr(why, 1, 59) "..."
|
|
84
|
+
|
|
85
|
+
printf " │ %-25s │ %s%-24s%s │ %s%-62s%s │\n", dim, color, score_col, RST, DIM, why, RST > "/dev/stderr"
|
|
86
|
+
}
|
|
87
|
+
next
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# End of table (blank line after table)
|
|
91
|
+
found_table && /^$/ {
|
|
92
|
+
printf " %s└───────────────────────────┴──────────────────────────┴────────────────────────────────────────────────────────────────┘%s\n", DIM, RST > "/dev/stderr"
|
|
93
|
+
found_table=0
|
|
94
|
+
next
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Developer prompts line
|
|
98
|
+
/^Developer prompts:/ {
|
|
99
|
+
gsub(/\*\*/, "")
|
|
100
|
+
printf "\n %s%s%s\n", DIM, $0, RST > "/dev/stderr"
|
|
101
|
+
next
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Decisions header
|
|
105
|
+
/^### Decisions/ {
|
|
106
|
+
printf "\n %s%sDecisions%s\n", BLD, "", RST > "/dev/stderr"
|
|
107
|
+
next
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Decision lines (emoji + text)
|
|
111
|
+
/^🔀|^🚫|^✅|^✏️|^📐|^👍/ {
|
|
112
|
+
gsub(/\*\*/, "\033[1m")
|
|
113
|
+
printf " %s\n", $0 > "/dev/stderr"
|
|
114
|
+
next
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Routine decisions note
|
|
118
|
+
/^\*\+/ {
|
|
119
|
+
gsub(/\*/, "")
|
|
120
|
+
printf " %s%s%s\n", DIM, $0, RST > "/dev/stderr"
|
|
121
|
+
next
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Recommendations header
|
|
125
|
+
/^### Recommendations/ {
|
|
126
|
+
printf "\n %s%sRecommendations%s\n", BLD, "", RST > "/dev/stderr"
|
|
127
|
+
next
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Recommendation lines
|
|
131
|
+
/^🔴|^🟡|^🟢/ {
|
|
132
|
+
printf " %s\n", $0 > "/dev/stderr"
|
|
133
|
+
next
|
|
134
|
+
}
|
|
135
|
+
' 2>&1
|
|
136
|
+
|
|
137
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "promptup-plugin",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "AI coding skill evaluator for Claude Code — 11-dimension scoring, decision intelligence, PR reports",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"promptup-plugin": "bin/promptup-plugin"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"dist",
|
|
13
|
+
"hooks",
|
|
14
|
+
"skills",
|
|
15
|
+
"statusline.sh"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20.0.0"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"ai",
|
|
24
|
+
"evaluation",
|
|
25
|
+
"developer-tools",
|
|
26
|
+
"coding-skill",
|
|
27
|
+
"mcp",
|
|
28
|
+
"mcp-server",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"promptup",
|
|
31
|
+
"session-analysis",
|
|
32
|
+
"decision-quality",
|
|
33
|
+
"pr-report"
|
|
34
|
+
],
|
|
35
|
+
"author": "gabikreal1",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/gabikreal1/promptup-plugin.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/gabikreal1/promptup-plugin",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/gabikreal1/promptup-plugin/issues"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"dev": "tsc --watch",
|
|
48
|
+
"prepublishOnly": "echo 'Ensure dist/ is built before publishing'"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
52
|
+
"better-sqlite3": "^11.0.0",
|
|
53
|
+
"ulid": "^2.3.0",
|
|
54
|
+
"zod": "^3.24.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
58
|
+
"typescript": "^5.9.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: eval
|
|
3
|
+
description: Evaluate the current coding session across 11 dimensions. Spawns an independent Claude analysis of your session transcript for unbiased scoring.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: [session-id]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Call `mcp__promptup__evaluate_session` with the session ID from `$ARGUMENTS` if provided, otherwise no arguments.
|
|
9
|
+
|
|
10
|
+
**CRITICAL: The tool returns a fully formatted markdown report. Output the ENTIRE tool response text verbatim in your reply — do NOT summarize, rephrase, or truncate any part of it. Include the full table, all progress bars, all decisions, and all recommendations exactly as returned. The user needs to see every line.**
|
|
11
|
+
|
|
12
|
+
If the evaluation fails, explain what happened and suggest running `/status`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-report
|
|
3
|
+
description: Generate a Decision Quality Score (DQS) report for the current branch. Shows developer decisions made during AI-assisted coding, optionally posts to GitHub PR.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: [--post] [--branch name]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /pr-report — PR Decision Report
|
|
9
|
+
|
|
10
|
+
Generate a decision quality report for the current git branch using PromptUp MCP tools.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Parse the user's arguments (`$ARGUMENTS`):
|
|
15
|
+
- If `--post` is present, set `post: true`
|
|
16
|
+
- If `--branch <name>` is present, set `branch` to that value
|
|
17
|
+
- Otherwise use defaults (current branch, no posting)
|
|
18
|
+
|
|
19
|
+
2. Call the `mcp__promptup__generate_pr_report` tool with the parsed options.
|
|
20
|
+
|
|
21
|
+
3. Display the returned **markdown report** directly — it contains:
|
|
22
|
+
- DQS score (Decision Quality Score, 0-100)
|
|
23
|
+
- Key decisions grouped by category (Architecture & Direction, Quality Gates, Scope Adjustments, Overrides)
|
|
24
|
+
- Summary table of decisions by type and signal level
|
|
25
|
+
- Commits list
|
|
26
|
+
|
|
27
|
+
4. If `--post` was used and the report was posted to GitHub:
|
|
28
|
+
- Confirm: "Posted to PR #N"
|
|
29
|
+
- Show the PR URL
|
|
30
|
+
|
|
31
|
+
5. If `--post` was used but posting failed:
|
|
32
|
+
- Explain why (no PR found, gh CLI not available, etc.)
|
|
33
|
+
- Suggest: install `gh` CLI, push branch first, create PR first
|
|
34
|
+
|
|
35
|
+
6. If no sessions or decisions were found for the branch:
|
|
36
|
+
- Explain that PromptUp needs active session data
|
|
37
|
+
- Suggest running `/status` to check what's been tracked
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: status
|
|
3
|
+
description: Show PromptUp tracking status — recent sessions, evaluations, and decision counts.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /status — PromptUp Status
|
|
8
|
+
|
|
9
|
+
Show the current state of PromptUp tracking.
|
|
10
|
+
|
|
11
|
+
## Instructions
|
|
12
|
+
|
|
13
|
+
1. Call the `mcp__promptup__get_status` tool.
|
|
14
|
+
|
|
15
|
+
2. Present the results as a clean summary:
|
|
16
|
+
|
|
17
|
+
**PromptUp Status**
|
|
18
|
+
- Sessions tracked: N
|
|
19
|
+
- Evaluations completed: N
|
|
20
|
+
- Decisions captured: N
|
|
21
|
+
- Latest session: [id] (started X ago)
|
|
22
|
+
- Latest eval score: X/100
|
|
23
|
+
- Tool events logged: N
|
|
24
|
+
|
|
25
|
+
3. If no data exists yet, explain:
|
|
26
|
+
- PromptUp tracks sessions passively via hooks
|
|
27
|
+
- Run `/eval` after a coding session to generate scores
|
|
28
|
+
- Run `/pr-report` on a branch with commits to see decision analysis
|
package/statusline.sh
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
DB="${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}/promptup.db"
|
|
3
|
+
DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.promptup}"
|
|
4
|
+
CONFIG_FILE="$DATA_DIR/config.json"
|
|
5
|
+
UPDATE_FILE="$DATA_DIR/update-available"
|
|
6
|
+
|
|
7
|
+
if [ ! -f "$DB" ]; then
|
|
8
|
+
echo "pupmeter: --"
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
SCORE=$(sqlite3 "$DB" "SELECT composite_score FROM evaluations ORDER BY created_at DESC LIMIT 1" 2>/dev/null)
|
|
13
|
+
SCORE_INT=$(printf "%.0f" "${SCORE:-0}")
|
|
14
|
+
|
|
15
|
+
FILLED=$((SCORE_INT / 10))
|
|
16
|
+
EMPTY=$((10 - FILLED))
|
|
17
|
+
BAR=""
|
|
18
|
+
for i in $(seq 1 $FILLED 2>/dev/null); do BAR="${BAR}█"; done
|
|
19
|
+
for i in $(seq 1 $EMPTY 2>/dev/null); do BAR="${BAR}░"; done
|
|
20
|
+
|
|
21
|
+
# Check for update
|
|
22
|
+
UPDATE=""
|
|
23
|
+
if [ -f "$UPDATE_FILE" ]; then
|
|
24
|
+
NEW_VER=$(cat "$UPDATE_FILE")
|
|
25
|
+
UPDATE=" │ ⬆ ${NEW_VER}"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
echo "pupmeter ${BAR} ${SCORE_INT}%${UPDATE}"
|
|
29
|
+
|
|
30
|
+
# Show recommendation if enabled
|
|
31
|
+
SHOW_REC=$(python3 -c "import json; c=json.load(open('$CONFIG_FILE')); print(c.get('statusline',{}).get('show_recommendation',True))" 2>/dev/null || echo "True")
|
|
32
|
+
if [ "$SHOW_REC" = "True" ]; then
|
|
33
|
+
REC=$(sqlite3 "$DB" "SELECT recommendations FROM evaluations WHERE recommendations IS NOT NULL ORDER BY created_at DESC LIMIT 1" 2>/dev/null)
|
|
34
|
+
if [ -n "$REC" ]; then
|
|
35
|
+
TIP=$(echo "$REC" | python3 -c "
|
|
36
|
+
import sys,json
|
|
37
|
+
try:
|
|
38
|
+
r=json.load(sys.stdin)
|
|
39
|
+
if r: print(r[0].get('recommendation','')[:60])
|
|
40
|
+
except: pass
|
|
41
|
+
" 2>/dev/null)
|
|
42
|
+
if [ -n "$TIP" ]; then
|
|
43
|
+
echo "💡 ${TIP}"
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
fi
|