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
package/dist/db.js ADDED
@@ -0,0 +1,327 @@
1
+ /**
2
+ * SQLite database layer for the standalone PromptUp plugin.
3
+ *
4
+ * Fully self-contained — no imports from @promptup/shared or any workspace package.
5
+ * Database lives at ${CLAUDE_PLUGIN_DATA}/promptup.db or ~/.promptup/promptup.db.
6
+ * WAL mode enabled for concurrent reads.
7
+ */
8
+ import Database from 'better-sqlite3';
9
+ import { mkdirSync, existsSync } from 'node:fs';
10
+ import { join, dirname } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+ // ─── Database Path ───────────────────────────────────────────────────────────
13
+ function resolveDbPath() {
14
+ const envDir = process.env.CLAUDE_PLUGIN_DATA;
15
+ const baseDir = envDir ?? join(homedir(), '.promptup');
16
+ return join(baseDir, 'promptup.db');
17
+ }
18
+ // ─── Singleton ───────────────────────────────────────────────────────────────
19
+ let db = null;
20
+ export function getDb() {
21
+ if (!db) {
22
+ throw new Error('Database not initialized. Call initDatabase() first.');
23
+ }
24
+ return db;
25
+ }
26
+ // ─── Initialization ──────────────────────────────────────────────────────────
27
+ export function initDatabase() {
28
+ if (db)
29
+ return;
30
+ const dbPath = resolveDbPath();
31
+ const dir = dirname(dbPath);
32
+ if (!existsSync(dir)) {
33
+ mkdirSync(dir, { recursive: true });
34
+ }
35
+ const instance = new Database(dbPath);
36
+ instance.pragma('journal_mode = WAL');
37
+ instance.pragma('foreign_keys = ON');
38
+ instance.exec(`
39
+ CREATE TABLE IF NOT EXISTS sessions (
40
+ id TEXT PRIMARY KEY,
41
+ project_path TEXT,
42
+ transcript_path TEXT,
43
+ status TEXT DEFAULT 'active',
44
+ message_count INTEGER DEFAULT 0,
45
+ started_at TEXT NOT NULL,
46
+ ended_at TEXT,
47
+ created_at TEXT NOT NULL
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS evaluations (
51
+ id TEXT PRIMARY KEY,
52
+ session_id TEXT NOT NULL,
53
+ trigger_type TEXT NOT NULL,
54
+ report_type TEXT DEFAULT 'checkpoint',
55
+ composite_score REAL NOT NULL,
56
+ dimension_scores TEXT NOT NULL,
57
+ recommendations TEXT,
58
+ trends TEXT,
59
+ risk_flags TEXT,
60
+ raw_evaluation TEXT,
61
+ message_count INTEGER DEFAULT 0,
62
+ message_range_from INTEGER DEFAULT 0,
63
+ message_range_to INTEGER DEFAULT 0,
64
+ weight_profile TEXT DEFAULT 'balanced',
65
+ created_at TEXT NOT NULL,
66
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
67
+ );
68
+
69
+ CREATE TABLE IF NOT EXISTS decisions (
70
+ id TEXT PRIMARY KEY,
71
+ session_id TEXT NOT NULL,
72
+ type TEXT NOT NULL,
73
+ message_index INTEGER NOT NULL,
74
+ context TEXT NOT NULL,
75
+ files_affected TEXT DEFAULT '[]',
76
+ source TEXT NOT NULL,
77
+ matched_rule TEXT,
78
+ depth TEXT,
79
+ opinionation TEXT,
80
+ ai_action TEXT,
81
+ signal TEXT,
82
+ created_at TEXT NOT NULL,
83
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS messages (
87
+ id TEXT PRIMARY KEY,
88
+ session_id TEXT NOT NULL,
89
+ role TEXT NOT NULL,
90
+ content TEXT NOT NULL DEFAULT '',
91
+ tool_uses TEXT,
92
+ sequence_number INTEGER NOT NULL DEFAULT 0,
93
+ tokens_in INTEGER DEFAULT 0,
94
+ tokens_out INTEGER DEFAULT 0,
95
+ model TEXT,
96
+ created_at TEXT NOT NULL,
97
+ FOREIGN KEY (session_id) REFERENCES sessions(id),
98
+ UNIQUE(session_id, sequence_number)
99
+ );
100
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, sequence_number);
101
+
102
+ CREATE TABLE IF NOT EXISTS git_activities (
103
+ id TEXT PRIMARY KEY,
104
+ session_id TEXT NOT NULL,
105
+ type TEXT NOT NULL,
106
+ branch TEXT,
107
+ commit_hash TEXT,
108
+ commit_message TEXT,
109
+ remote TEXT,
110
+ raw_command TEXT NOT NULL,
111
+ message_index INTEGER NOT NULL,
112
+ created_at TEXT NOT NULL,
113
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
114
+ );
115
+ CREATE INDEX IF NOT EXISTS idx_git_activities_branch ON git_activities(branch);
116
+
117
+ CREATE TABLE IF NOT EXISTS pr_reports (
118
+ id TEXT PRIMARY KEY,
119
+ branch TEXT NOT NULL,
120
+ repo TEXT NOT NULL,
121
+ pr_number INTEGER,
122
+ pr_url TEXT,
123
+ commits TEXT DEFAULT '[]',
124
+ session_ids TEXT DEFAULT '[]',
125
+ total_decisions INTEGER DEFAULT 0,
126
+ decision_breakdown TEXT DEFAULT '{}',
127
+ dqs REAL,
128
+ markdown TEXT NOT NULL,
129
+ posted_at TEXT,
130
+ created_at TEXT NOT NULL
131
+ );
132
+ CREATE INDEX IF NOT EXISTS idx_pr_reports_branch ON pr_reports(branch, repo);
133
+ `);
134
+ db = instance;
135
+ }
136
+ export function closeDatabase() {
137
+ if (db) {
138
+ db.close();
139
+ db = null;
140
+ }
141
+ }
142
+ // ─── Sessions ────────────────────────────────────────────────────────────────
143
+ export function insertSession(session) {
144
+ const d = getDb();
145
+ d.prepare(`
146
+ INSERT OR IGNORE INTO sessions (id, project_path, transcript_path, status,
147
+ message_count, started_at, ended_at, created_at)
148
+ VALUES (@id, @project_path, @transcript_path, @status,
149
+ @message_count, @started_at, @ended_at, @created_at)
150
+ `).run(session);
151
+ }
152
+ export function getSession(id) {
153
+ const row = getDb()
154
+ .prepare('SELECT * FROM sessions WHERE id = ?')
155
+ .get(id);
156
+ return row ?? null;
157
+ }
158
+ export function updateSession(id, updates) {
159
+ const fields = Object.keys(updates).filter((k) => k !== 'id');
160
+ if (fields.length === 0)
161
+ return;
162
+ const setClauses = fields.map((f) => `${f} = @${f}`).join(', ');
163
+ getDb()
164
+ .prepare(`UPDATE sessions SET ${setClauses} WHERE id = @id`)
165
+ .run({ id, ...updates });
166
+ }
167
+ export function getRecentSessions(limit = 20) {
168
+ return getDb()
169
+ .prepare('SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?')
170
+ .all(limit);
171
+ }
172
+ // ─── Messages ───────────────────────────────────────────────────────────
173
+ export function insertMessages(messages) {
174
+ const d = getDb();
175
+ const stmt = d.prepare(`
176
+ INSERT OR IGNORE INTO messages (id, session_id, role, content, tool_uses,
177
+ sequence_number, tokens_in, tokens_out, model, created_at)
178
+ VALUES (@id, @session_id, @role, @content, @tool_uses,
179
+ @sequence_number, @tokens_in, @tokens_out, @model, @created_at)
180
+ `);
181
+ const tx = d.transaction(() => {
182
+ for (const m of messages) {
183
+ stmt.run({
184
+ ...m,
185
+ tool_uses: m.tool_uses ?? null,
186
+ model: m.model ?? null,
187
+ });
188
+ }
189
+ });
190
+ tx();
191
+ }
192
+ export function getMessagesBySession(sessionId, limit = 10000, offset = 0) {
193
+ return getDb()
194
+ .prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY sequence_number ASC LIMIT ? OFFSET ?')
195
+ .all(sessionId, limit, offset);
196
+ }
197
+ // ─── Evaluations ─────────────────────────────────────────────────────────────
198
+ export function insertEvaluation(evaluation) {
199
+ getDb().prepare(`
200
+ INSERT INTO evaluations (id, session_id, trigger_type, report_type,
201
+ composite_score, dimension_scores, recommendations, trends, risk_flags,
202
+ raw_evaluation, message_count, message_range_from, message_range_to,
203
+ weight_profile, created_at)
204
+ VALUES (@id, @session_id, @trigger_type, @report_type,
205
+ @composite_score, @dimension_scores, @recommendations, @trends, @risk_flags,
206
+ @raw_evaluation, @message_count, @message_range_from, @message_range_to,
207
+ @weight_profile, @created_at)
208
+ `).run(evaluation);
209
+ }
210
+ export function getLatestEvaluation(sessionId) {
211
+ const d = getDb();
212
+ let row;
213
+ if (sessionId) {
214
+ row = d
215
+ .prepare('SELECT * FROM evaluations WHERE session_id = ? ORDER BY created_at DESC LIMIT 1')
216
+ .get(sessionId);
217
+ }
218
+ else {
219
+ row = d
220
+ .prepare('SELECT * FROM evaluations ORDER BY created_at DESC LIMIT 1')
221
+ .get();
222
+ }
223
+ return row ?? null;
224
+ }
225
+ export function getEvaluationsBySession(sessionId) {
226
+ return getDb()
227
+ .prepare('SELECT * FROM evaluations WHERE session_id = ? ORDER BY created_at ASC')
228
+ .all(sessionId);
229
+ }
230
+ export function getRecentEvaluations(limit = 20) {
231
+ return getDb()
232
+ .prepare('SELECT * FROM evaluations ORDER BY created_at DESC LIMIT ?')
233
+ .all(limit);
234
+ }
235
+ // ─── Decisions ───────────────────────────────────────────────────────────────
236
+ export function insertDecision(decision) {
237
+ getDb().prepare(`
238
+ INSERT OR IGNORE INTO decisions (id, session_id, type, message_index, context,
239
+ files_affected, source, matched_rule, depth, opinionation, ai_action, signal, created_at)
240
+ VALUES (@id, @session_id, @type, @message_index, @context,
241
+ @files_affected, @source, @matched_rule, @depth, @opinionation,
242
+ @ai_action, @signal, @created_at)
243
+ `).run({
244
+ ...decision,
245
+ depth: decision.depth ?? null,
246
+ opinionation: decision.opinionation ?? null,
247
+ ai_action: decision.ai_action ?? null,
248
+ signal: decision.signal ?? null,
249
+ matched_rule: decision.matched_rule ?? null,
250
+ });
251
+ }
252
+ export function getDecisionsBySession(sessionId) {
253
+ return getDb()
254
+ .prepare('SELECT * FROM decisions WHERE session_id = ? ORDER BY message_index')
255
+ .all(sessionId);
256
+ }
257
+ export function getDecisionsBySessions(sessionIds) {
258
+ if (sessionIds.length === 0)
259
+ return [];
260
+ const d = getDb();
261
+ const placeholders = sessionIds.map(() => '?').join(',');
262
+ return d
263
+ .prepare(`SELECT * FROM decisions WHERE session_id IN (${placeholders}) ORDER BY created_at`)
264
+ .all(...sessionIds);
265
+ }
266
+ // ─── Git Activities ──────────────────────────────────────────────────────────
267
+ export function insertGitActivity(activity) {
268
+ getDb().prepare(`
269
+ INSERT OR IGNORE INTO git_activities (id, session_id, type, branch, commit_hash,
270
+ commit_message, remote, raw_command, message_index, created_at)
271
+ VALUES (@id, @session_id, @type, @branch, @commit_hash,
272
+ @commit_message, @remote, @raw_command, @message_index, @created_at)
273
+ `).run({
274
+ ...activity,
275
+ branch: activity.branch ?? null,
276
+ commit_hash: activity.commit_hash ?? null,
277
+ commit_message: activity.commit_message ?? null,
278
+ remote: activity.remote ?? null,
279
+ });
280
+ }
281
+ export function getSessionsByBranch(branch) {
282
+ const rows = getDb()
283
+ .prepare('SELECT DISTINCT session_id FROM git_activities WHERE branch = ? ORDER BY created_at')
284
+ .all(branch);
285
+ return rows.map((r) => r.session_id);
286
+ }
287
+ // ─── Session matching ────────────────────────────────────────────────────────
288
+ export function getSessionsByTimeRange(from, to, projectPath) {
289
+ const d = getDb();
290
+ if (projectPath) {
291
+ return d
292
+ .prepare('SELECT * FROM sessions WHERE started_at <= ? AND (ended_at >= ? OR ended_at IS NULL) AND project_path LIKE ?')
293
+ .all(to, from, `%${projectPath}%`);
294
+ }
295
+ return d
296
+ .prepare('SELECT * FROM sessions WHERE started_at <= ? AND (ended_at >= ? OR ended_at IS NULL)')
297
+ .all(to, from);
298
+ }
299
+ // ─── PR Reports ──────────────────────────────────────────────────────────────
300
+ export function insertPRReport(report) {
301
+ getDb().prepare(`
302
+ INSERT INTO pr_reports (id, branch, repo, pr_number, pr_url, commits, session_ids,
303
+ total_decisions, decision_breakdown, dqs, markdown, posted_at, created_at)
304
+ VALUES (@id, @branch, @repo, @pr_number, @pr_url, @commits, @session_ids,
305
+ @total_decisions, @decision_breakdown, @dqs, @markdown, @posted_at, @created_at)
306
+ `).run({
307
+ ...report,
308
+ pr_number: report.pr_number ?? null,
309
+ pr_url: report.pr_url ?? null,
310
+ dqs: report.dqs ?? null,
311
+ posted_at: report.posted_at ?? null,
312
+ });
313
+ }
314
+ export function getPRReportByBranch(branch, repo) {
315
+ const row = getDb()
316
+ .prepare('SELECT * FROM pr_reports WHERE branch = ? AND repo = ? ORDER BY created_at DESC LIMIT 1')
317
+ .get(branch, repo);
318
+ return row ?? null;
319
+ }
320
+ // ─── Stats ───────────────────────────────────────────────────────────────────
321
+ export function getStats() {
322
+ const d = getDb();
323
+ const sessions = d.prepare('SELECT COUNT(*) as c FROM sessions').get().c;
324
+ const evaluations = d.prepare('SELECT COUNT(*) as c FROM evaluations').get().c;
325
+ const decisions = d.prepare('SELECT COUNT(*) as c FROM decisions').get().c;
326
+ return { sessions, evaluations, decisions };
327
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Decision Detector
3
+ *
4
+ * Scans sorted message pairs (assistant → user) to detect developer decisions
5
+ * using the heuristic classifier. Each detected decision is tagged with depth,
6
+ * opinionation, signal level, and the AI action that prompted it.
7
+ *
8
+ * STANDALONE port — no imports from @promptup/shared or workspace packages.
9
+ */
10
+ import type { MessageRow, DecisionRow } from './shared/types.js';
11
+ export declare function detectDecisions(messages: MessageRow[], sessionId: string): DecisionRow[];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Decision Detector
3
+ *
4
+ * Scans sorted message pairs (assistant → user) to detect developer decisions
5
+ * using the heuristic classifier. Each detected decision is tagged with depth,
6
+ * opinionation, signal level, and the AI action that prompted it.
7
+ *
8
+ * STANDALONE port — no imports from @promptup/shared or workspace packages.
9
+ */
10
+ import { ulid } from 'ulid';
11
+ import { classifyDecision } from './shared/decision-classifier.js';
12
+ export function detectDecisions(messages, sessionId) {
13
+ const decisions = [];
14
+ const sorted = [...messages].sort((a, b) => a.sequence_number - b.sequence_number);
15
+ let prevAssistant = null;
16
+ for (const msg of sorted) {
17
+ if (msg.role === 'assistant') {
18
+ prevAssistant = msg;
19
+ continue;
20
+ }
21
+ if (msg.role !== 'user')
22
+ continue;
23
+ const toolUses = prevAssistant?.tool_uses
24
+ ? JSON.parse(prevAssistant.tool_uses)
25
+ : null;
26
+ const result = classifyDecision(msg.content, prevAssistant?.content ?? null, toolUses);
27
+ if (result) {
28
+ decisions.push({
29
+ id: ulid(),
30
+ session_id: sessionId,
31
+ type: result.type,
32
+ message_index: msg.sequence_number,
33
+ context: result.context,
34
+ files_affected: JSON.stringify(result.filesAffected),
35
+ source: 'plugin',
36
+ matched_rule: result.matchedRule,
37
+ depth: result.depth,
38
+ opinionation: result.opinionation,
39
+ ai_action: result.aiAction,
40
+ signal: result.signal,
41
+ created_at: msg.created_at,
42
+ });
43
+ }
44
+ prevAssistant = null;
45
+ }
46
+ return decisions;
47
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Evaluation engine for the standalone PromptUp plugin.
3
+ *
4
+ * Primary: spawns `claude -p` to get real LLM analysis of the session.
5
+ * Fallback: heuristic pattern matching if Claude Code is unavailable.
6
+ *
7
+ * STANDALONE copy — no imports from @promptup/shared or session-watcher.
8
+ */
9
+ import type { EvalTriggerType, EvaluationRow, MessageRow, WeightProfileKey } from './shared/types.js';
10
+ export declare function evaluateSession(sessionId: string, messages: MessageRow[], triggerType: EvalTriggerType, weightProfile?: WeightProfileKey): Promise<EvaluationRow | null>;