howamiprompting 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -2
- package/bin/howamiprompting.js +11 -0
- package/package.json +46 -4
- package/src/agents.js +160 -0
- package/src/analyze.js +167 -0
- package/src/bank.json +687 -0
- package/src/clean.js +75 -0
- package/src/cli.js +189 -0
- package/src/collect/claude.js +44 -0
- package/src/collect/codex.js +36 -0
- package/src/collect/gemini.js +31 -0
- package/src/collect/generic.js +42 -0
- package/src/collect/index.js +40 -0
- package/src/collect/others.js +79 -0
- package/src/collect/vscode.js +112 -0
- package/src/practice.js +119 -0
- package/src/server.js +97 -0
- package/src/sqlite.js +51 -0
- package/src/stats.js +208 -0
- package/src/store.js +24 -0
- package/src/util.js +89 -0
- package/ui/app.css +227 -0
- package/ui/app.js +464 -0
- package/ui/index.html +55 -0
package/src/clean.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Turn raw records into clean, privacy-safe prompt objects.
|
|
2
|
+
import { toISO } from './util.js';
|
|
3
|
+
|
|
4
|
+
const NOISE = [/^<command-(name|message|args)>/, /^<local-command/, /^Caveat: The messages below/, /^This session is being continued/, /^<system-reminder>/, /^\s*<(environment_context|user_instructions)/];
|
|
5
|
+
|
|
6
|
+
const SECRET_PATTERNS = [
|
|
7
|
+
[/\b(sk-(?:ant-|or-|proj-)?[A-Za-z0-9_\-]{16,})/g, '[REDACTED_KEY]'],
|
|
8
|
+
[/\b(gh[pousr]_[A-Za-z0-9]{20,})/g, '[REDACTED_TOKEN]'],
|
|
9
|
+
[/\b(xox[abpr]-[A-Za-z0-9-]{10,})/g, '[REDACTED_TOKEN]'],
|
|
10
|
+
[/\b(AKIA[0-9A-Z]{16})\b/g, '[REDACTED_AWS]'],
|
|
11
|
+
[/\b(AIza[0-9A-Za-z_\-]{30,})/g, '[REDACTED_KEY]'],
|
|
12
|
+
[/\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}/g, '[REDACTED_JWT]'],
|
|
13
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED_PRIVATE_KEY]'],
|
|
14
|
+
[/((?:password|passwd|pwd|secret|api[_-]?key|token)\s*[:=]\s*)(["']?)[^\s"']{4,}\2/gi, '$1[REDACTED]'],
|
|
15
|
+
[/\b(postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s]+/gi, '$1://[REDACTED]'],
|
|
16
|
+
];
|
|
17
|
+
export function redact(s) { for (const [re, rep] of SECRET_PATTERNS) s = s.replace(re, rep); return s; }
|
|
18
|
+
|
|
19
|
+
const LOG_LINE = /^\s*(at\s+\S+.*\(|at\s+\S+:\d+|File ".*", line \d+|Traceback|\w*(Error|Exception)\b[:\s]|\[\d{1,2}:\d{2}|\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}|(ERROR|WARN|INFO|DEBUG)\b|npm (ERR|error)|\s*\^+\s*$|\s+\d+\s*\|)/;
|
|
20
|
+
const CODE_LINE = /[{};=<>]\s*$|^\s*(import|export|const|let|var|function|def|class|return|if|for|while|public|private|#include|SELECT|INSERT|UPDATE|<\/?\w+[^>]*>)\b/;
|
|
21
|
+
|
|
22
|
+
/** Separate the human's prose from pasted code / logs. */
|
|
23
|
+
export function split(text) {
|
|
24
|
+
let codeLines = 0, logLines = 0;
|
|
25
|
+
let t = text.replace(/```[\s\S]*?(```|$)/g, m => { const n = m.split('\n').length - 2; codeLines += Math.max(1, n); return `\n[pasted code: ${Math.max(1, n)} lines]\n`; });
|
|
26
|
+
const lines = t.split(/\r?\n/);
|
|
27
|
+
const keep = [];
|
|
28
|
+
let run = [], runKind = null;
|
|
29
|
+
const flushRun = () => {
|
|
30
|
+
if (!run.length) return;
|
|
31
|
+
if (run.length >= 3) { keep.push(`[pasted ${runKind}: ${run.length} lines]`); if (runKind === 'log') logLines += run.length; else codeLines += run.length; }
|
|
32
|
+
else keep.push(...run.map(r => r.l));
|
|
33
|
+
run = []; runKind = null;
|
|
34
|
+
};
|
|
35
|
+
for (const l of lines) {
|
|
36
|
+
const kind = LOG_LINE.test(l) ? 'log' : (CODE_LINE.test(l) && !/[.?!]$/.test(l.trim()) && l.split(' ').length < 14) ? 'code' : null;
|
|
37
|
+
if (kind) { if (runKind && runKind !== kind && run.length < 3) runKind = kind; runKind = runKind || kind; run.push({ l }); }
|
|
38
|
+
else { flushRun(); keep.push(l); }
|
|
39
|
+
}
|
|
40
|
+
flushRun();
|
|
41
|
+
t = keep.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
42
|
+
const prose = t.replace(/\[pasted (code|log): \d+ lines\]/g, ' ').replace(/https?:\/\/\S+/g, ' ').replace(/(?:~|\.{1,2})?\/[\w./-]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
43
|
+
return { text: t, prose, codeLines, logLines };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function clean(records, { since } = {}) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const sinceMs = since ? Date.now() - since * 86400000 : 0;
|
|
50
|
+
let interrupts = 0;
|
|
51
|
+
for (const r of records) {
|
|
52
|
+
if (r.kind === 'interrupt') { interrupts++; continue; }
|
|
53
|
+
let raw = String(r.text || '').replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').replace(/\[Image #?\d*\]|\[image\]/gi, '[image]').trim();
|
|
54
|
+
if (!raw || NOISE.some(re => re.test(raw))) continue;
|
|
55
|
+
const ts = toISO(r.ts);
|
|
56
|
+
if (sinceMs && ts && Date.parse(ts) < sinceMs) continue;
|
|
57
|
+
const key = (r.session || '') + '|' + raw.slice(0, 200);
|
|
58
|
+
if (seen.has(key)) continue;
|
|
59
|
+
seen.add(key);
|
|
60
|
+
raw = redact(raw);
|
|
61
|
+
const s = split(raw);
|
|
62
|
+
if (!s.prose && !s.codeLines && !s.logLines) continue;
|
|
63
|
+
let text = s.text;
|
|
64
|
+
if (text.length > 2500) text = text.slice(0, 2000) + ` …[+${text.length - 2000} chars]`;
|
|
65
|
+
out.push({
|
|
66
|
+
id: out.length, tool: r.tool, project: r.project || null, session: String(r.session || ''), ts,
|
|
67
|
+
text, prose: s.prose.slice(0, 2000), words: (s.prose.match(/[A-Za-z0-9']+/g) || []).length,
|
|
68
|
+
codeLines: s.codeLines, logLines: s.logLines, images: (raw.match(/\[image\]/g) || []).length,
|
|
69
|
+
context: r.context ? redact(split(String(r.context)).prose).slice(-300) : '',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
out.sort((a, b) => (a.ts || '').localeCompare(b.ts || ''));
|
|
73
|
+
out.forEach((p, i) => p.id = i);
|
|
74
|
+
return { prompts: out, interrupts };
|
|
75
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { collectAll } from './collect/index.js';
|
|
6
|
+
import { clean } from './clean.js';
|
|
7
|
+
import { computeStats } from './stats.js';
|
|
8
|
+
import { detectAgents, AGENTS } from './agents.js';
|
|
9
|
+
import { analyze, overallFrom } from './analyze.js';
|
|
10
|
+
import { startServer } from './server.js';
|
|
11
|
+
import { saveReport, lastReport } from './store.js';
|
|
12
|
+
|
|
13
|
+
const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
14
|
+
const tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
15
|
+
const c = (code) => s => tty ? `\x1b[${code}m${s}\x1b[0m` : String(s);
|
|
16
|
+
const C = { b: c('1'), dim: c('2'), pink: c('38;5;205'), yellow: c('38;5;220'), mint: c('38;5;49'), blue: c('38;5;69'), red: c('31'), gray: c('90') };
|
|
17
|
+
|
|
18
|
+
const HELP = `
|
|
19
|
+
${C.b('howamiprompting')} — Spotify Wrapped for your AI coding prompts
|
|
20
|
+
|
|
21
|
+
${C.b('Usage')}
|
|
22
|
+
npx howamiprompting [options]
|
|
23
|
+
|
|
24
|
+
${C.b('Options')}
|
|
25
|
+
--agent <id> AI to use for the deep analysis: claude | codex | gemini | anthropic | openrouter | openai | ollama
|
|
26
|
+
--model <name> Model override for that agent (e.g. haiku, sonnet, gpt-5-mini)
|
|
27
|
+
--no-ai Local stats only — nothing leaves your machine
|
|
28
|
+
--sample <n> Prompts sent for deep analysis (default 180)
|
|
29
|
+
--since <days> Only look at the last N days
|
|
30
|
+
--include <file> Also import a chat export (repeatable)
|
|
31
|
+
--roots <dir> Extra folder to search for Aider / SpecStory histories (repeatable)
|
|
32
|
+
--last Re-open your last report without rescanning
|
|
33
|
+
--json <file> Also write the report as JSON
|
|
34
|
+
--port <n> Port for the local report (default: random)
|
|
35
|
+
--no-open Don't open the browser
|
|
36
|
+
-y, --yes Don't ask questions (use the first available agent)
|
|
37
|
+
-v, --version Print version
|
|
38
|
+
|
|
39
|
+
${C.b('Privacy')}
|
|
40
|
+
Everything is read locally. Only with an AI agent are ~180 sampled prompts (secrets redacted)
|
|
41
|
+
sent — through YOUR OWN Claude Code / Codex / Gemini / API key, never to us.
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const o = { include: [], roots: [], ai: true, open: true, sample: 180, yes: false };
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const a = argv[i], next = () => argv[++i];
|
|
48
|
+
if (a === '--agent') o.agent = next();
|
|
49
|
+
else if (a === '--model') o.model = next();
|
|
50
|
+
else if (a === '--no-ai') o.ai = false;
|
|
51
|
+
else if (a === '--sample') o.sample = Math.max(20, Math.min(1000, +next() || 180));
|
|
52
|
+
else if (a === '--since') o.since = +next() || undefined;
|
|
53
|
+
else if (a === '--include') o.include.push(next());
|
|
54
|
+
else if (a === '--roots') o.roots.push(next());
|
|
55
|
+
else if (a === '--last') o.last = true;
|
|
56
|
+
else if (a === '--json') o.json = next();
|
|
57
|
+
else if (a === '--port') o.port = +next() || 0;
|
|
58
|
+
else if (a === '--no-open') o.open = false;
|
|
59
|
+
else if (a === '-y' || a === '--yes') o.yes = true;
|
|
60
|
+
else if (a === '-h' || a === '--help') o.help = true;
|
|
61
|
+
else if (a === '-v' || a === '--version') o.version = true;
|
|
62
|
+
}
|
|
63
|
+
return o;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function spinner(text) {
|
|
67
|
+
if (!tty) { process.stdout.write(' ' + text + '\n'); return { update() {}, stop(t) { if (t) console.log(' ' + t); } }; }
|
|
68
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
69
|
+
let i = 0, cur = text;
|
|
70
|
+
const t0 = Date.now();
|
|
71
|
+
const id = setInterval(() => { process.stdout.write(`\r\x1b[K ${C.pink(frames[i++ % frames.length])} ${cur} ${C.gray(Math.round((Date.now() - t0) / 1000) + 's')}`); }, 90);
|
|
72
|
+
return {
|
|
73
|
+
update(t) { cur = t; },
|
|
74
|
+
stop(final) { clearInterval(id); process.stdout.write('\r\x1b[K'); if (final) console.log(' ' + final); },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ask(q) {
|
|
79
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
80
|
+
return new Promise(r => rl.question(q, a => { rl.close(); r(a.trim()); }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function openBrowser(url) {
|
|
84
|
+
const cmd = process.platform === 'darwin' ? ['open', [url]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', url.replace(/&/g, '^&')]] : ['xdg-open', [url]];
|
|
85
|
+
try { spawn(cmd[0], cmd[1], { stdio: 'ignore', detached: true }).unref(); } catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const bar = (v, w = 20) => { const n = Math.round(v / 100 * w); return C.mint('█'.repeat(n)) + C.gray('░'.repeat(w - n)); };
|
|
89
|
+
|
|
90
|
+
export async function main(argv) {
|
|
91
|
+
const o = parseArgs(argv);
|
|
92
|
+
if (o.help) return console.log(HELP);
|
|
93
|
+
if (o.version) return console.log(VERSION);
|
|
94
|
+
|
|
95
|
+
console.log(`\n ${C.b(C.pink('how am i prompting?'))} ${C.gray('v' + VERSION)}\n ${C.gray('Spotify Wrapped for your AI coding prompts')}\n`);
|
|
96
|
+
|
|
97
|
+
let bundle, prompts = [];
|
|
98
|
+
if (o.last) {
|
|
99
|
+
bundle = lastReport();
|
|
100
|
+
if (!bundle) { console.log(C.red(' No saved report yet — run without --last first.')); process.exit(1); }
|
|
101
|
+
console.log(C.gray(` Re-opening your report from ${new Date(bundle.generatedAt).toLocaleString()}\n`));
|
|
102
|
+
} else {
|
|
103
|
+
// 1. Scan
|
|
104
|
+
console.log(C.b(' 1. Finding your prompts') + C.gray(' (read-only, nothing leaves your machine)'));
|
|
105
|
+
const sp = spinner('Scanning…');
|
|
106
|
+
const found = [];
|
|
107
|
+
const raw = await collectAll({ include: o.include, roots: o.roots }, (name, n, err) => {
|
|
108
|
+
sp.update(`Scanning… ${name}`);
|
|
109
|
+
found.push({ name, n, err: err?.message });
|
|
110
|
+
});
|
|
111
|
+
sp.stop();
|
|
112
|
+
for (const f of found) console.log(` ${f.n > 0 ? C.mint('✔') : C.gray('·')} ${f.name.padEnd(48)} ${f.n > 0 ? C.b(f.n.toLocaleString()) : C.gray(f.n < 0 ? 'error' : 'none')}`);
|
|
113
|
+
const cl = clean(raw, { since: o.since });
|
|
114
|
+
prompts = cl.prompts;
|
|
115
|
+
if (!prompts.length) {
|
|
116
|
+
console.log(C.yellow('\n No prompts found. Supported: Claude Code, Codex, Gemini CLI, Cursor, VS Code Copilot, Windsurf, Cline, Roo, Kilo, Continue, OpenCode, Aider, SpecStory.'));
|
|
117
|
+
console.log(C.gray(' Have an export file? Run: npx howamiprompting --include path/to/chat.md\n'));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const stats = computeStats(prompts, { interrupts: cl.interrupts });
|
|
121
|
+
const t = stats.totals;
|
|
122
|
+
console.log(`\n ${C.b(t.prompts.toLocaleString())} prompts · ${C.b(t.sessions.toLocaleString())} sessions · ${C.b(t.projects)} projects · ${C.b(t.tools)} tools`);
|
|
123
|
+
console.log(` You are ${C.b(C.yellow(stats.persona.emoji + ' ' + stats.persona.name))}\n`);
|
|
124
|
+
|
|
125
|
+
// 2. Agent
|
|
126
|
+
let agent = null;
|
|
127
|
+
if (o.ai) {
|
|
128
|
+
console.log(C.b(' 2. Deep analysis with your own AI'));
|
|
129
|
+
const agents = await detectAgents();
|
|
130
|
+
if (o.agent) {
|
|
131
|
+
agent = agents.find(a => a.id === o.agent) || (AGENTS[o.agent] ? { id: o.agent, label: AGENTS[o.agent].label, privacy: AGENTS[o.agent].privacy } : null);
|
|
132
|
+
if (!agent) console.log(C.yellow(` Unknown agent "${o.agent}".`));
|
|
133
|
+
} else if (!agents.length) {
|
|
134
|
+
console.log(C.gray(' No AI agent found (Claude Code, Codex, Gemini CLI, an API key or Ollama).'));
|
|
135
|
+
console.log(C.gray(' Showing local stats only. Install one and re-run for scores, makeovers & coaching.\n'));
|
|
136
|
+
} else if (agents.length === 1 || o.yes || !process.stdin.isTTY) {
|
|
137
|
+
agent = agents[0];
|
|
138
|
+
} else {
|
|
139
|
+
agents.forEach((a, i) => console.log(` ${C.b(i + 1)}. ${a.label} ${C.gray('— ' + a.detail)}`));
|
|
140
|
+
const ans = await ask(` Which one? ${C.gray('[1]')} `);
|
|
141
|
+
agent = agents[(parseInt(ans, 10) || 1) - 1] || agents[0];
|
|
142
|
+
}
|
|
143
|
+
if (agent && !o.yes && process.stdin.isTTY) {
|
|
144
|
+
const n = Math.min(o.sample, prompts.filter(p => p.words >= 3).length);
|
|
145
|
+
console.log(` ${C.gray('About')} ${C.b(n)} ${C.gray('sampled prompts (secrets redacted) will be sent to')} ${C.b(agent.privacy)}.`);
|
|
146
|
+
console.log(C.gray(` It usually takes 1–4 minutes and uses a little of your ${agent.label} quota.`));
|
|
147
|
+
const ans = (await ask(` Continue? ${C.gray('[Y/n]')} `)).toLowerCase();
|
|
148
|
+
if (ans === 'n' || ans === 'no') agent = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let ai = null, aiError = null;
|
|
153
|
+
if (agent) {
|
|
154
|
+
const sp2 = spinner(`Asking ${agent.label} to read your prompts…`);
|
|
155
|
+
try {
|
|
156
|
+
ai = await analyze(prompts, stats, {
|
|
157
|
+
agent: agent.id, model: o.model, sampleSize: o.sample, concurrency: AGENTS[agent.id].kind === 'cli' ? 2 : 3,
|
|
158
|
+
onStep: (label) => sp2.update(`${agent.label}: ${label}…`),
|
|
159
|
+
});
|
|
160
|
+
sp2.stop(`${C.mint('✔')} Analysed ${ai.sampled} prompts with ${agent.label}`);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
aiError = e.message;
|
|
163
|
+
sp2.stop(`${C.yellow('!')} ${agent.label} analysis failed: ${C.gray(e.message.slice(0, 160))} — showing local stats.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const scores = ai ? ai.scores : { ...stats.heuristic, overall: overallFrom(stats.heuristic) };
|
|
168
|
+
bundle = {
|
|
169
|
+
version: VERSION, generatedAt: new Date().toISOString(),
|
|
170
|
+
agent: agent ? { id: agent.id, label: agent.label } : null, aiError,
|
|
171
|
+
sources: found.filter(f => f.n > 0).map(f => ({ name: f.name, count: f.n })),
|
|
172
|
+
scores, stats, ai,
|
|
173
|
+
};
|
|
174
|
+
saveReport(bundle);
|
|
175
|
+
if (o.json) { fs.writeFileSync(path.resolve(o.json), JSON.stringify(bundle, null, 2)); console.log(C.gray(` Saved ${o.json}`)); }
|
|
176
|
+
|
|
177
|
+
console.log(`\n ${C.b('Your prompting score:')} ${C.b(C.pink(scores.overall))}${C.gray('/100')} ${ai?.level ? C.gray('(' + ai.level + ')') : ''}`);
|
|
178
|
+
for (const k of ['clarity', 'context', 'specificity', 'structure', 'efficiency', 'english']) console.log(` ${k.padEnd(12)} ${bar(scores[k])} ${scores[k]}`);
|
|
179
|
+
if (ai?.headline) console.log(`\n ${C.yellow('“' + ai.headline + '”')}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 3. Report UI
|
|
183
|
+
const agentId = bundle.agent?.id || null;
|
|
184
|
+
const { url } = await startServer({ bundle, prompts, agent: agentId, model: o.model, port: o.port || 0 });
|
|
185
|
+
console.log(`\n ${C.b('3. Your report is live:')} ${C.blue(url)}`);
|
|
186
|
+
console.log(C.gray(' Practice games, prompt makeovers and your share card are in there.'));
|
|
187
|
+
console.log(C.gray(' Press Ctrl+C to quit. ⭐ Like it? github.com/theavijitsarkar/howamiprompting\n'));
|
|
188
|
+
if (o.open) openBrowser(url);
|
|
189
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Claude Code — ~/.claude/projects/<project>/<session>.jsonl
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { HOME, find, isDir, jsonl, projectName } from '../util.js';
|
|
4
|
+
|
|
5
|
+
export const id = 'claude-code';
|
|
6
|
+
export const name = 'Claude Code';
|
|
7
|
+
|
|
8
|
+
function roots() {
|
|
9
|
+
return [process.env.CLAUDE_CONFIG_DIR, path.join(HOME, '.claude'), path.join(HOME, '.config', 'claude')]
|
|
10
|
+
.filter(Boolean).map(r => path.join(r, 'projects')).filter(isDir);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const textOf = c => typeof c === 'string' ? c
|
|
14
|
+
: Array.isArray(c) ? c.filter(p => p && p.type === 'text').map(p => p.text).join('\n') : '';
|
|
15
|
+
|
|
16
|
+
export async function collect() {
|
|
17
|
+
const out = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const root of roots()) {
|
|
20
|
+
const files = await find(root, p => p.endsWith('.jsonl'), { depth: 3 });
|
|
21
|
+
for (const f of files) {
|
|
22
|
+
let lastAssistant = '';
|
|
23
|
+
for await (const o of jsonl(f)) {
|
|
24
|
+
if (o.isSidechain) continue; // sub-agent traffic, not written by the human
|
|
25
|
+
const msg = o.message;
|
|
26
|
+
if (o.type === 'assistant' && msg) { const t = textOf(msg.content); if (t) lastAssistant = t; continue; }
|
|
27
|
+
if (o.type !== 'user' || !msg || msg.role !== 'user' || o.isMeta || o.isCompactSummary) continue;
|
|
28
|
+
if (Array.isArray(msg.content) && msg.content.every(p => p.type === 'tool_result')) continue;
|
|
29
|
+
const text = textOf(msg.content);
|
|
30
|
+
if (!text) continue;
|
|
31
|
+
const key = (o.sessionId || f) + '|' + (o.uuid || text.slice(0, 80));
|
|
32
|
+
if (seen.has(key)) continue;
|
|
33
|
+
seen.add(key);
|
|
34
|
+
out.push({
|
|
35
|
+
tool: name, session: o.sessionId || path.basename(f, '.jsonl'),
|
|
36
|
+
project: projectName(o.cwd) || path.basename(path.dirname(f)).replace(/^-/, '').split('-').pop(),
|
|
37
|
+
ts: o.timestamp || null, text, context: lastAssistant.slice(-400),
|
|
38
|
+
kind: /^\[Request interrupted/.test(text) ? 'interrupt' : undefined,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// OpenAI Codex CLI — ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { HOME, find, isDir, jsonl, projectName } from '../util.js';
|
|
4
|
+
|
|
5
|
+
export const id = 'codex';
|
|
6
|
+
export const name = 'Codex';
|
|
7
|
+
|
|
8
|
+
const textOf = c => typeof c === 'string' ? c
|
|
9
|
+
: Array.isArray(c) ? c.filter(p => p && /text/.test(p.type || 'text') && typeof p.text === 'string').map(p => p.text).join('\n') : '';
|
|
10
|
+
|
|
11
|
+
export async function collect() {
|
|
12
|
+
const root = path.join(process.env.CODEX_HOME || path.join(HOME, '.codex'), 'sessions');
|
|
13
|
+
if (!isDir(root)) return [];
|
|
14
|
+
const out = [];
|
|
15
|
+
const files = await find(root, p => /\.jsonl$/.test(p), { depth: 5 });
|
|
16
|
+
for (const f of files) {
|
|
17
|
+
let session = path.basename(f, '.jsonl'), cwd = null, lastAssistant = '';
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for await (const o of jsonl(f)) {
|
|
20
|
+
const p = o.payload || o;
|
|
21
|
+
if (o.type === 'session_meta') { session = p.id || session; cwd = p.cwd || cwd; continue; }
|
|
22
|
+
if (p.type === 'turn_context' || o.type === 'turn_context') { cwd = p.cwd || cwd; continue; }
|
|
23
|
+
let text = null;
|
|
24
|
+
if (p.type === 'message' && p.role === 'assistant') { const t = textOf(p.content); if (t) lastAssistant = t; continue; }
|
|
25
|
+
if (p.type === 'message' && p.role === 'user') text = textOf(p.content);
|
|
26
|
+
else if (o.type === 'event_msg' && p.type === 'user_message') text = p.message;
|
|
27
|
+
if (!text) continue;
|
|
28
|
+
if (/^\s*<(environment_context|user_instructions|permissions|turn_aborted)/.test(text) || /^# AGENTS\.md/.test(text)) continue;
|
|
29
|
+
const key = text.trim();
|
|
30
|
+
if (seen.has(key)) continue; // response_item + event_msg duplicates
|
|
31
|
+
seen.add(key);
|
|
32
|
+
out.push({ tool: name, session, project: projectName(cwd), ts: o.timestamp || null, text, context: lastAssistant.slice(-400) });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Google Gemini CLI — ~/.gemini/tmp/<project-hash>/{logs.json, chats/*.json}
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { HOME, find, isDir, readJson } from '../util.js';
|
|
4
|
+
|
|
5
|
+
export const id = 'gemini-cli';
|
|
6
|
+
export const name = 'Gemini CLI';
|
|
7
|
+
|
|
8
|
+
export async function collect() {
|
|
9
|
+
const root = path.join(HOME, '.gemini', 'tmp');
|
|
10
|
+
if (!isDir(root)) return [];
|
|
11
|
+
const out = [];
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const push = (r) => { const k = r.session + '|' + r.text.slice(0, 120); if (!seen.has(k)) { seen.add(k); out.push(r); } };
|
|
14
|
+
const files = await find(root, p => p.endsWith('.json'), { depth: 3 });
|
|
15
|
+
for (const f of files) {
|
|
16
|
+
const j = await readJson(f);
|
|
17
|
+
if (!j) continue;
|
|
18
|
+
const project = path.basename(path.dirname(f)) === 'chats' ? path.basename(path.dirname(path.dirname(f))).slice(0, 8) : path.basename(path.dirname(f)).slice(0, 8);
|
|
19
|
+
if (Array.isArray(j)) { // logs.json
|
|
20
|
+
for (const m of j) if (m && m.type === 'user' && typeof m.message === 'string') push({ tool: name, session: m.sessionId || f, project: 'gemini-' + project, ts: m.timestamp, text: m.message, context: '' });
|
|
21
|
+
} else if (Array.isArray(j.messages)) { // chats/session-*.json
|
|
22
|
+
let last = '';
|
|
23
|
+
for (const m of j.messages) {
|
|
24
|
+
const t = typeof m.content === 'string' ? m.content : Array.isArray(m.content) ? m.content.map(x => x.text || '').join('\n') : (m.parts || []).map(x => x.text || '').join('\n');
|
|
25
|
+
if (m.type === 'user' || m.role === 'user') { if (t) push({ tool: name, session: j.sessionId || f, project: 'gemini-' + project, ts: m.timestamp || j.startTime, text: t, context: last.slice(-400) }); }
|
|
26
|
+
else if (t) last = t;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Generic importer for files passed with --include (exports from any chat tool, markdown, JSON, JSONL, plain text)
|
|
2
|
+
const SKIP = ['<command-', '<local-command', '<environment_context', '<user_instructions', '<system-reminder', 'Caveat:', '[Request interrupted', '# AGENTS.md'];
|
|
3
|
+
const keep = t => typeof t === 'string' && t.trim().length > 3 && !SKIP.some(p => t.trim().startsWith(p));
|
|
4
|
+
const textOf = c => typeof c === 'string' ? c : Array.isArray(c) ? c.filter(x => x && (x.type === 'text' || x.type === 'input_text' || (!x.type && typeof x.text === 'string'))).map(x => x.text).join('\n') : (c && typeof c.text === 'string' ? c.text : '');
|
|
5
|
+
|
|
6
|
+
function walk(node, out, d = 0) {
|
|
7
|
+
if (!node || d > 14 || typeof node !== 'object') return;
|
|
8
|
+
if (Array.isArray(node)) return node.forEach(n => walk(n, out, d + 1));
|
|
9
|
+
const role = String(node.role || node.author?.role || node.sender || '').toLowerCase();
|
|
10
|
+
if (['user', 'human'].includes(role) || ['user', 'human', 'user_message'].includes(String(node.type || '').toLowerCase())) {
|
|
11
|
+
const t = textOf(node.content ?? node.text ?? node.message ?? node.content?.parts?.join?.('\n'));
|
|
12
|
+
if (keep(t)) { out.push(t); return; }
|
|
13
|
+
}
|
|
14
|
+
for (const k in node) if (node[k] && typeof node[k] === 'object') walk(node[k], out, d + 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseAny(raw, filename = '') {
|
|
18
|
+
raw = raw.replace(/^/, '').trim();
|
|
19
|
+
const out = [];
|
|
20
|
+
const first = raw.split(/\r?\n/, 1)[0];
|
|
21
|
+
if (/\.jsonl$/i.test(filename) || (first.startsWith('{') && raw.includes('\n{'))) {
|
|
22
|
+
for (const l of raw.split(/\r?\n/)) { try { walk(JSON.parse(l), out); } catch {} }
|
|
23
|
+
if (out.length) return out;
|
|
24
|
+
}
|
|
25
|
+
if (/\.json$/i.test(filename) || raw.startsWith('{') || raw.startsWith('[')) {
|
|
26
|
+
try { walk(JSON.parse(raw), out); if (out.length) return out; } catch {}
|
|
27
|
+
}
|
|
28
|
+
const USER = /^\s*(?:#{1,6}\s*)?(?:_?\*\*_?\s*)?(user|you|human|me|prompt)\b[^\n]{0,30}$/i;
|
|
29
|
+
const BOT = /^\s*(?:#{1,6}\s*)?(?:_?\*\*_?\s*)?(assistant|cursor|claude|ai|model|gemini|agent|codex|copilot|bot|chatgpt|cascade)\b[^\n]{0,30}$/i;
|
|
30
|
+
let mode = null, buf = [], saw = false;
|
|
31
|
+
const flush = () => { if (mode === 'user' && keep(buf.join('\n'))) out.push(buf.join('\n').trim()); buf = []; };
|
|
32
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
33
|
+
const inl = line.match(/^\s*(user|you|human|me)\s*:\s*(.+)$/i);
|
|
34
|
+
if (inl) { flush(); saw = true; mode = 'user'; buf.push(inl[2]); continue; }
|
|
35
|
+
if (USER.test(line)) { flush(); saw = true; mode = 'user'; continue; }
|
|
36
|
+
if (BOT.test(line) || /^\s*(assistant|ai|claude)\s*:/i.test(line)) { flush(); saw = true; mode = 'bot'; continue; }
|
|
37
|
+
if (mode === 'user') buf.push(line);
|
|
38
|
+
}
|
|
39
|
+
flush();
|
|
40
|
+
if (!saw) (/\n\s*\n/.test(raw) ? raw.split(/\n\s*\n/) : raw.split(/\r?\n/)).forEach(p => keep(p) && out.push(p.trim()));
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import * as claude from './claude.js';
|
|
3
|
+
import * as codex from './codex.js';
|
|
4
|
+
import * as gemini from './gemini.js';
|
|
5
|
+
import * as vscode from './vscode.js';
|
|
6
|
+
import { continueDev, opencode, projectFiles } from './others.js';
|
|
7
|
+
import { readText } from '../util.js';
|
|
8
|
+
import { parseAny } from './generic.js';
|
|
9
|
+
|
|
10
|
+
export const SOURCES = [
|
|
11
|
+
{ name: 'Claude Code', run: () => claude.collect() },
|
|
12
|
+
{ name: 'Codex', run: () => codex.collect() },
|
|
13
|
+
{ name: 'Gemini CLI', run: () => gemini.collect() },
|
|
14
|
+
{ name: 'Cursor · Copilot · Windsurf · Cline · Roo · Kilo', run: () => vscode.collect() },
|
|
15
|
+
{ name: 'Continue', run: () => continueDev() },
|
|
16
|
+
{ name: 'OpenCode', run: () => opencode() },
|
|
17
|
+
{ name: 'Aider · SpecStory', run: o => projectFiles(o.roots || []) },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** Collect raw prompt records from every source. onProgress(sourceName, count|error). */
|
|
21
|
+
export async function collectAll(opts = {}, onProgress = () => {}) {
|
|
22
|
+
const all = [];
|
|
23
|
+
for (const s of SOURCES) {
|
|
24
|
+
try {
|
|
25
|
+
const recs = await s.run(opts);
|
|
26
|
+
all.push(...recs);
|
|
27
|
+
onProgress(s.name, recs.length);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
onProgress(s.name, -1, e);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const f of opts.include || []) {
|
|
33
|
+
const t = await readText(path.resolve(f));
|
|
34
|
+
if (t == null) { onProgress('file ' + f, -1, new Error('unreadable')); continue; }
|
|
35
|
+
const recs = parseAny(t, f).map(text => ({ tool: 'Imported', session: path.basename(f), project: null, ts: null, text, context: '' }));
|
|
36
|
+
all.push(...recs);
|
|
37
|
+
onProgress(path.basename(f), recs.length);
|
|
38
|
+
}
|
|
39
|
+
return all;
|
|
40
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Continue.dev, OpenCode, Aider and SpecStory (Cursor history saved inside projects)
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { HOME, find, isDir, readJson, readText, projectName } from '../util.js';
|
|
5
|
+
|
|
6
|
+
export async function continueDev() {
|
|
7
|
+
const dir = path.join(HOME, '.continue', 'sessions');
|
|
8
|
+
if (!isDir(dir)) return [];
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.json') && f !== 'sessions.json')) {
|
|
11
|
+
const j = await readJson(path.join(dir, f));
|
|
12
|
+
let last = '';
|
|
13
|
+
for (const h of j?.history || []) {
|
|
14
|
+
const m = h.message || h;
|
|
15
|
+
const t = typeof m.content === 'string' ? m.content : Array.isArray(m.content) ? m.content.map(p => p.text || '').join('\n') : '';
|
|
16
|
+
if (m.role === 'user' && t) out.push({ tool: 'Continue', session: j.sessionId || f, project: projectName(j.workspaceDirectory), ts: j.dateCreated || null, text: t, context: last.slice(-400) });
|
|
17
|
+
else if (t) last = t;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function opencode() {
|
|
24
|
+
const base = path.join(process.env.XDG_DATA_HOME || path.join(HOME, '.local', 'share'), 'opencode', 'storage');
|
|
25
|
+
const msgDir = path.join(base, 'message'), partDir = path.join(base, 'part');
|
|
26
|
+
if (!isDir(msgDir) || !isDir(partDir)) return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
const files = await find(msgDir, p => p.endsWith('.json'), { depth: 2 });
|
|
29
|
+
const msgs = [];
|
|
30
|
+
for (const f of files) { const m = await readJson(f); if (m?.id) msgs.push(m); }
|
|
31
|
+
msgs.sort((a, b) => (a.time?.created || 0) - (b.time?.created || 0));
|
|
32
|
+
const last = new Map();
|
|
33
|
+
for (const m of msgs) {
|
|
34
|
+
const pd = path.join(partDir, m.id);
|
|
35
|
+
if (!isDir(pd)) continue;
|
|
36
|
+
let text = '';
|
|
37
|
+
for (const pf of fs.readdirSync(pd)) { const p = await readJson(path.join(pd, pf)); if (p?.type === 'text' && !p.synthetic) text += (text ? '\n' : '') + (p.text || ''); }
|
|
38
|
+
if (!text) continue;
|
|
39
|
+
if (m.role === 'user') out.push({ tool: 'OpenCode', session: m.sessionID, project: projectName(m.path?.cwd || m.path?.root), ts: m.time?.created, text, context: (last.get(m.sessionID) || '').slice(-400) });
|
|
40
|
+
else last.set(m.sessionID, text);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Aider + SpecStory files live inside projects, so we search likely code folders (bounded). */
|
|
46
|
+
export async function projectFiles(extraRoots = []) {
|
|
47
|
+
const roots = [...new Set([...extraRoots, ...['code', 'Code', 'projects', 'Projects', 'dev', 'Developer', 'src', 'work', 'repos', 'git', 'GitHub', 'Sites', 'workspace', 'Documents', 'Desktop'].map(d => path.join(HOME, d)), HOME])].filter(isDir);
|
|
48
|
+
const out = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
for (const r of roots) {
|
|
51
|
+
const files = await find(r, (p, n) => n === '.aider.chat.history.md' || (/\.md$/.test(n) && p.includes(path.join('.specstory', 'history'))), { depth: r === HOME ? 3 : 5, limit: 3000, skipDirs: new Set([...SKIP_NO_DOT]) });
|
|
52
|
+
for (const f of files) {
|
|
53
|
+
if (seen.has(f)) continue; seen.add(f);
|
|
54
|
+
const t = await readText(f, 50 * 1024 * 1024);
|
|
55
|
+
if (!t) continue;
|
|
56
|
+
if (f.endsWith('.aider.chat.history.md')) {
|
|
57
|
+
const project = projectName(path.dirname(f));
|
|
58
|
+
let session = 0, cur = [];
|
|
59
|
+
const flush = () => { if (cur.length) out.push({ tool: 'Aider', session: project + '-' + session, project, ts: null, text: cur.join('\n'), context: '' }); cur = []; };
|
|
60
|
+
for (const line of t.split(/\r?\n/)) {
|
|
61
|
+
if (line.startsWith('# aider chat started at')) { flush(); session++; continue; }
|
|
62
|
+
if (line.startsWith('#### ')) cur.push(line.slice(5)); else flush();
|
|
63
|
+
}
|
|
64
|
+
flush();
|
|
65
|
+
} else {
|
|
66
|
+
const project = projectName(path.dirname(path.dirname(path.dirname(f))));
|
|
67
|
+
const parts = t.split(/^_\*\*(User|Assistant)\*\*_\s*$/m);
|
|
68
|
+
let last = '';
|
|
69
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
70
|
+
const body = (parts[i + 1] || '').replace(/^\s*---\s*$/gm, '').trim();
|
|
71
|
+
if (parts[i] === 'User' && body) out.push({ tool: 'Cursor (SpecStory)', session: path.basename(f), project, ts: null, text: body, context: last.slice(-400) });
|
|
72
|
+
else last = body;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
const SKIP_NO_DOT = ['node_modules', '.git', 'Library', '.Trash', 'vendor', 'dist', 'build', '.next', '.venv', 'venv', '__pycache__', 'Pods', 'DerivedData', '.npm', '.cargo', '.rustup', 'go', '.gradle', '.m2', 'Applications', 'Music', 'Movies', 'Pictures', '.docker', 'AppData', 'snap', '.cache', '.local', '.vscode', '.cursor', 'Downloads'];
|